Skip to content

AxiLiteCrossbarI2cMux: deselect TCA9548 channels between transactions#1420

Merged
ruck314 merged 3 commits into
pre-releasefrom
bittware-qsfp-40G
May 18, 2026
Merged

AxiLiteCrossbarI2cMux: deselect TCA9548 channels between transactions#1420
ruck314 merged 3 commits into
pre-releasefrom
bittware-qsfp-40G

Conversation

@ruck314
Copy link
Copy Markdown
Contributor

@ruck314 ruck314 commented May 14, 2026

Summary

  • Insert an explicit deselect-all write (0x00 to the TCA9548) before each channel-select write in AxiLiteCrossbarI2cMux.
  • Adds two FSM states: DESELECT_S (waits for the 0x00 write to ack, loads target channel mask, pulses i2cRstL again) and MUX_RST_S (mirrors RST_S but goes straight to MUX_S so the deselect path doesn't re-enter DESELECT_S).
  • New FSM flow: IDLE_S → RST_S → DESELECT_S → MUX_RST_S → MUX_S → XBAR_S → IDLE_S.

Why

On the BittWare XUP-VV8 (VU13P), every AXIL transaction to TCA9548 channels 5 and 7 (QSFP slots 1 and 3) returned RESP=2 SLVERR and cascaded into hundreds of masked I2C failures, while channels 4 and 6 (slots 0 and 2) worked. The failure originally looked QSFP+ specific because slot 0 had a QSFP28 and slots 1/3 had QSFP+ modules — but a QSFP+ in slot 2 worked fine, proving the differential was the TCA9548 channel mask, not the module type.

The TCA9548 channel-select register is a single byte that should be atomically overwritten by each new mask. In practice the previous channel left enough sticky state on certain channel pairs that the next transaction NACK'd or returned an undefined response. Forcing a 0x00 write between transactions gives every channel a clean deassert before the new channel asserts.

Validation

Tested on a BittWare XUP-VV8 (VU13P) with:

  • Slot 0: QSFP28 (Identifier = 0x11)
  • Slot 1, 2, 3: QSFP+ (Identifier = 0x0D)
Test Before After
Slot 1 burst, 300 sequential reads 1/300 ok 300/300 ok
Slot 3 burst, 300 sequential reads 1/300 ok 300/300 ok
Slots 0, 2 burst, 300 reads each 29/30 ok 300/300 ok
Qsfp[1].ReadDevice() (originally failing) SLVERR cascade OK, ErrorCount = 0
All four Qsfp[i].ReadDevice() n/a all OK, ErrorCount = 0
Sustained throughput n/a ~940 txn/s

Compatibility

The change is additive — every TCA9548-fronted AXIL transaction picks up one extra single-byte I2C write to deassert all channels before the new channel-select. Existing surf users get the same cleaner handshake whether they need it or not; cost is one byte (~90 µs at 100 kHz SCL) per AXIL transaction.

Test plan

  • Build BittWareXupVv8DmaLoopback firmware on Vivado 2025.2
  • Load via updatePcieFpga.py, reboot, confirm BittWareXupVv8DmaLoopback Hardware Type 0x002 enumerates
  • Sustained Qsfp[i].Identifier.get() × 30 per slot — zero failures
  • Qsfp[i].ReadDevice() (recursive, exercises _UpperPageProxy) on all 4 slots — zero failures
  • PCA9555 (TCA9548 channel 3) reads/writes still functional under same firmware
  • Regression-test on other surf users that already had working TCA9548 paths to confirm no behavioral change (added latency only)

ruck314 added 2 commits May 14, 2026 21:03
Each AXIL transaction previously wrote a single byte to the I2C MUX
channel-select register and assumed the device would atomically replace
the prior channel. On the BittWare XUP-VV8 this leaves enough sticky
state on TCA9548 channels 5 and 7 (QSFP slots 1 and 3) that the second
I2C transaction targeting those slots returns RESP=2 SLVERR and every
subsequent access cascades. Channels 4 and 6 (slots 0, 2) happen to
tolerate the same sequence, which made the failure look QSFP+ specific
when the actual differential is the TCA9548 channel mask.

Insert an explicit deselect-all write (0x00) before the channel-select
write. 0x00 is the documented "no channel selected" state for every
device in I2cMuxPkg.vhd (TCA9548, PCA9547, PCA9544A, PCA9546A, PCA9540B),
so the fix applies uniformly regardless of which decode map is used.

Two new states are added to the existing FSM:

  DESELECT_S  - waits for the 0x00 write to ack, then loads the saved
                target channel mask and pulses i2cRstL low again.
  MUX_RST_S   - mirrors RST_S but transitions straight to MUX_S so the
                deselect path does not re-enter DESELECT_S.

The chanMask record field carries the target channel mask through the
deselect-write phase. Flow is now:

  IDLE_S -> RST_S -> DESELECT_S -> MUX_RST_S -> MUX_S -> XBAR_S -> IDLE_S

Validated on a BittWare XUP-VV8 (VU13P) with QSFP28 in slot 0 and QSFP+
modules in slots 1, 2, 3:

  Before: slots 1 and 3 fail every transaction after the first one;
          Qsfp[1].ReadDevice() cascades into hundreds of SLVERRs.
  After:  300/300 ok on every slot in a sustained burst (~940 txn/s),
          Qsfp[i].ReadDevice() succeeds on all 4 slots, ErrorCount = 0.

The change is additive for existing surf users - the deselect step is a
single extra I2C byte write per AXIL transaction and gives every
MUX-fronted bus the same clean channel-select handshake regardless of
whether the previous downstream device left the bus in a marginal state.
@ruck314 ruck314 force-pushed the bittware-qsfp-40G branch from 3010ea3 to d67ab28 Compare May 15, 2026 04:04
@ruck314 ruck314 requested a review from scompa18 May 15, 2026 04:04
@ruck314 ruck314 marked this pull request as ready for review May 15, 2026 04:04
@ruck314
Copy link
Copy Markdown
Contributor Author

ruck314 commented May 15, 2026

I regressed tested this code on KCU1500, which uses the i2cRstL port and confirmed that works as well with this new patch for the AXI-Lite I2C MUX

@ruck314 ruck314 merged commit d8b569f into pre-release May 18, 2026
5 checks passed
@ruck314 ruck314 deleted the bittware-qsfp-40G branch May 18, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants