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 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 13:00:49 +02:00
parent 6f151cd1ff
commit afc10d7e5c
2 changed files with 27 additions and 5 deletions
+6 -2
View File
@@ -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