Take the TX bit phase from the rising-edge counter, not a falling-edge one

The bit engine had TWO independent phase references: rx_bit counting rising
edges and tx_bit counting falling edges.  Those only stay in step while the
host emits exactly as many falls as rises, and it does not -- a transfer that
begins on a RISING edge (CS asserted while CLK is already low, or the clock
gating between two back-to-back host transfers) delivers a rise with no
matching fall.  tx_bit then slips one bit and never recovers.

RX never notices, because the rising count always equals the host's bit count.
So the header decodes perfectly, the right register is selected, and ONLY MISO
is wrong -- which is what made this look like an output-edge or output-delay
problem for two rounds of debugging.

Measured on a GameCube: every MISO byte shifted right by 2 bits, identically
at every EXI speed index.  NWAYS read 05 C0 (= 0x17 with two leading zero
bits), device ID 01 00 80 80, speed sweep ok=[0,0,0,0,0,0].

Two diagnoses were wrong before this one, and both were ruled out by evidence
rather than argument:
  - NOT propagation delay.  Index 0 is 1 MHz, a 1000 ns bit time; no output
    path fails there.  Speed-independence is the tell.
  - NOT the output edge.  The old rising-edge build and the new falling-edge
    build returned BYTE-IDENTICAL 05 C0.  Flipping the edge would have moved
    it; it did not, so the slip is upstream of the output stage.

What settled it was a measurement.  PHASE_METER=1 builds a diagnostic,
non-functional bitstream whose engine transmits a constant 0x80 per byte
instead of the FIFO head, taking the register file and both byte FIFOs out of
the MISO path so the host reads the engine's byte PHASE directly: 0x80
aligned, 0x40 one bit late, 0x20 two bits late.  Hardware read 0x20 in all
eight slots, localising the fault to the bit engine in a single run.

Fix: the shift register loads on rx_bit == 0.  One phase reference, on the
edge the host counts, so the two cannot drift.

Regression test (spi_mode3_slave T5) asserts CS while CLK is LOW so the
transaction begins on a rising edge.  The old counter fails it by exactly one
bit: A5 3C comes back as 52 9E.  EVERY earlier testbench asserted CS with CLK
high, so falls and rises always matched and the slip was structurally
invisible -- the same blind spot as the Mode 2/3 mix-up, which was invisible
to a master that parked MOSI early.  Vary the starting edge in SPI tests.

Timing after the fix, EXI_J4=1, --seeds 8: clk 26.9-31.1 MHz, EXI 45.8-61.7
MHz, 8/8 PASS.  Best seed 4 (clk 31.07, EXI 56.70), flashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 19:04:52 +02:00
parent 91c3bf0ece
commit d35688390e
5 changed files with 122 additions and 12 deletions
+3 -2
View File
@@ -72,9 +72,10 @@ class ExiCapture(Elaboratable):
tx_rdy : tx_fifo has room
"""
def __init__(self, rx_depth=4, tx_depth=2):
def __init__(self, rx_depth=4, tx_depth=2, phase_meter=False):
self._rx_depth = rx_depth
self._tx_depth = tx_depth
self._phase_meter = phase_meter # diagnostic, see SPIMode3Slave
# Physical SPI (capture domain, wired to pins by BBATop)
self.spi_clk = Signal(init=1)
@@ -129,7 +130,7 @@ class ExiCapture(Elaboratable):
cd_fr.clk.eq(self.spi_clk),
]
spi = SPIMode3Slave()
spi = SPIMode3Slave(phase_meter=self._phase_meter)
m.submodules.spi = spi
# RX bytes cross exiclk -> exi; TX bytes cross exi -> exiclk_n.