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)