From afc10d7e5ca72feaea59d4d02551377e4220de46 Mon Sep 17 00:00:00 2001 From: Dennis Brentjes Date: Sat, 3 Oct 2026 13:00:49 +0200 Subject: [PATCH] fix(spi): sample MOSI on the RISING edge -- real Mode 3, not Mode 2 The docs call this 'Mode 3 (CPOL=1, CPHA=1)' and then describe sampling MOSI on the FALLING edge. Idle-high plus sample-on-falling is Mode 2. Real Mode 3 samples on the second edge of each cycle, which with an idle-high clock is the RISING one, and that is what the GameCube does. Sampling on the same edge the GC drives MOSI latched the PREVIOUS bit, so every byte arrived one bit late in both directions, at every clock speed: the 0x80 register-space header byte was read as 0x00, which decodes as a command-space device-ID query -- so NWAYS reads returned the device ID and the speed sweep failed at every index. Hardware raw dump, first transaction after an FPGA reset: 00 00 02 01 01 00 00 00 02 01 01 00 = 04 02 02 00 >> 1 The Pi probe never caught it because that script was written to match this device, and every simulation testbench drives MOSI many capture ticks before toggling the clock, so MOSI is stable at BOTH edges and the convention cannot be distinguished. The bba_top master now models a true Mode 3 master (drive on falling, sample on rising) so the suite can tell the difference. SPIMode3Slave(mode2=True) restores the old behaviour for the Pi/J4 probe. Known remaining issue: at EXI_HALF=1 (27 MHz, 2x oversampling) reads are still shifted, because the 2-stage input FFSynchronizer costs 2 capture ticks which at 27 MHz is a whole bit. That is a separate fault and only bites at 2x. Co-Authored-By: Claude Opus 5 --- exi_bba/bba_top.py | 8 ++++++-- exi_bba/spi_mode3_slave.py | 24 +++++++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/exi_bba/bba_top.py b/exi_bba/bba_top.py index 6ad4808..a684a6b 100644 --- a/exi_bba/bba_top.py +++ b/exi_bba/bba_top.py @@ -422,13 +422,17 @@ if __name__ == "__main__": async def spi_byte(ctx, mosi_val): """Drive one EXI Mode-3 byte; return the assembled MISO byte.""" + # Real Mode 3 master (CPOL=1, CPHA=1), which is what the GameCube is: + # drive MOSI on the FALLING edge, sample MISO on the RISING edge. + # The old version drove MOSI then fell -- a Mode 2 master -- which is + # what the Pi probe did and why it never exposed the mismatch. miso = 0 for bit in range(7, -1, -1): + ctx.set(dut.exi_clk, 0) # falling: master drives ctx.set(dut.exi_mosi, (mosi_val >> bit) & 1) - ctx.set(dut.exi_clk, 0) # falling: slave samples MOSI await ctx.tick("capture").repeat(HALF) + ctx.set(dut.exi_clk, 1) # rising: both sample miso = (miso << 1) | ctx.get(dut.exi_miso) - ctx.set(dut.exi_clk, 1) # rising await ctx.tick("capture").repeat(HALF) return miso diff --git a/exi_bba/spi_mode3_slave.py b/exi_bba/spi_mode3_slave.py index 3ae4bfe..991cd06 100644 --- a/exi_bba/spi_mode3_slave.py +++ b/exi_bba/spi_mode3_slave.py @@ -27,11 +27,14 @@ class SPIMode3Slave(Elaboratable): tx_load : 1-cycle pulse requesting the next TX byte from upstream """ - def __init__(self, domain="capture"): + def __init__(self, domain="capture", mode2=False): # Clock domain this byte engine runs in. Split-domain design puts the # bit engine in a fast `capture` domain (54 MHz) so it can oversample # a 27 MHz EXI clock ~3×; the register file lives in a slower domain. self._domain = domain + # False = real Mode 3 (sample MOSI on RISING) -- what the GameCube + # needs. True = the old Mode-2 behaviour the Pi probe was built on. + self._mode2 = mode2 self.spi_clk = Signal(init=1) # idles HIGH self.spi_mosi = Signal() @@ -75,8 +78,23 @@ class SPIMode3Slave(Elaboratable): rising_clk = Signal() cs_fall = Signal() cs_rise = Signal() - m.d.comb += falling_clk.eq(~clk_s & clk_prev) - m.d.comb += rising_clk .eq( clk_s & ~clk_prev) + # ⚠️ EDGE CONVENTION. The docs call this "Mode 3 (CPOL=1, CPHA=1)" but + # then describe sampling MOSI on the FALLING edge -- that combination + # is Mode 2. Real Mode 3 samples on the SECOND edge of each cycle, + # which with an idle-high clock is the RISING one. + # + # Hardware 2026-10-03: the GameCube drives MOSI on the falling edge, so + # sampling there latches the PREVIOUS bit -- every byte arrived one bit + # late in both directions at every clock speed (header 0x80 was read as + # 0x00, and the reply came back as 04 02 02 00 >> 1 = 02 01 01 00). + # `mode2=True` restores the old behaviour, which is what the Pi probe + # on J4 was written to match. + if self._mode2: + sample_edge, shift_edge = ~clk_s & clk_prev, clk_s & ~clk_prev + else: + sample_edge, shift_edge = clk_s & ~clk_prev, ~clk_s & clk_prev + m.d.comb += falling_clk.eq(sample_edge) # "sample MOSI / advance bit" + m.d.comb += rising_clk .eq(shift_edge) # "shift MISO / re-arm" m.d.comb += cs_fall .eq(~cs_s & cs_prev) m.d.comb += cs_rise .eq( cs_s & ~cs_prev) m.d.comb += self.frame_start.eq(cs_fall)