Rework the EXI decoder to the real GC<->BBA protocol

Replaces the invented 2-byte header (addr split across both bytes, 2-bit
length field) with the protocol libogc and Swiss actually speak:

  command  (bit7=0, 6-bit reg)   [reg, 00, 00, data]  one 4-byte transfer
  register (bit7=1, 16-bit reg)  [0x80|w<<6, ah, al, 00] + data until CS

This was not an optimisation: the old decoder read Swiss's NWAYS read
(80 00 31 00) as 'write 0x31 to NCRA', and bit 0 of 0x31 is RESET, so every
register read would have reset the device.

Changes:
  * 16-bit address; no length field -- register reads and writes now stream
    until CS, like the SPRAM path already did
  * RHDR_LO starts the SPRAM fetch as soon as the address completes, using
    the trailing header byte as the prefetch window the protocol provides
  * command space serves EXI regs 0x02/0x03 (interrupt mask/status), the only
    two Swiss uses
  * EXI_GetID gets its own path: it offers just ONE header byte of warning, so
    the first ID byte is pushed the instant byte 0 identifies the query

Two timing subtleties, both found by simulation and commented in place:
  * MISO is driven LIVE from the tx FIFO head, so a byte pushed while the pad
    byte is on the wire appears in the pad slot.  A filler byte is parked
    there (MISO is don't-care through the header) so the first real response
    lands in data slot 0.
  * the prefetch must be one-shot; re-firing while waiting for the pad byte
    consumed the register stream early and shifted reads by a byte.

All testbenches updated to the real format and passing: bba_register_file
(7), bba_top (8 incl. the full W5100->SPRAM->GC loop and TX), exi_j4,
exi_capture, spi_mode3_slave, tx_frame_drain, spram_arbiter,
rx_frame_assembler, w5100_parallel_master.  gc_bba_test rebuilt against the
new protocol.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 11:58:52 +02:00
parent f2762f613d
commit 2458884160
6 changed files with 319 additions and 131 deletions
+21 -9
View File
@@ -89,27 +89,39 @@ if __name__ == "__main__":
return miso
async def exi_read(ctx, pads, addr, length):
"""EXI immediate read on `pads`: header, clock-idle gap, data phase."""
"""Register-space read, REAL protocol: 4-byte header then data."""
clk, mosi, cs_n = pads
hdr0 = (addr >> 6) & 0x7F
hdr1 = ((addr & 0x3F) << 2) | ((length - 1) & 0x3)
ctx.set(cs_n, 0)
ctx.set(clk, 1)
await ctx.tick("capture").repeat(HALF)
await spi_byte(ctx, pads, hdr0)
await spi_byte(ctx, pads, hdr1)
await ctx.tick("capture").repeat(HALF * 12) # header-decode gap
await spi_byte(ctx, pads, 0x80)
await spi_byte(ctx, pads, (addr >> 8) & 0xFF)
await spi_byte(ctx, pads, addr & 0xFF)
await spi_byte(ctx, pads, 0x00) # turnaround byte
result = [await spi_byte(ctx, pads, 0x00) for _ in range(length)]
ctx.set(cs_n, 1)
await ctx.tick("capture").repeat(HALF)
return result
async def exi_devid(ctx, pads):
"""EXI_GetID: write 0x0000 (2 bytes), clock 4 read bytes."""
clk, mosi, cs_n = pads
ctx.set(cs_n, 0)
ctx.set(clk, 1)
await ctx.tick("capture").repeat(HALF)
await spi_byte(ctx, pads, 0x00)
await spi_byte(ctx, pads, 0x00)
out = [await spi_byte(ctx, pads, 0x00) for _ in range(4)]
ctx.set(cs_n, 1)
await ctx.tick("capture").repeat(HALF)
return out
async def testbench(ctx):
await ctx.tick("capture").repeat(20)
# T1: enumerate over the SP1 pads while J4 is unplugged (idle high).
unplug(ctx, top.j4)
dev = await exi_read(ctx, top.sp1, 0x0000, 4)
dev = await exi_devid(ctx, top.sp1)
print(f"T1 enumerate via SP1 (J4 idle): {[f'0x{b:02X}' for b in dev]}")
if dev != DEVICE_ID:
errors.append(f"T1 SP1 device ID: got {dev}")
@@ -117,7 +129,7 @@ if __name__ == "__main__":
# T2: enumerate over the J4 pads while SP1 is unplugged (idle high).
unplug(ctx, top.sp1)
dev = await exi_read(ctx, top.j4, 0x0000, 4)
dev = await exi_devid(ctx, top.j4)
print(f"T2 enumerate via J4 (SP1 idle): {[f'0x{b:02X}' for b in dev]}")
if dev != DEVICE_ID:
errors.append(f"T2 J4 device ID: got {dev}")
@@ -125,7 +137,7 @@ if __name__ == "__main__":
# T3: back to SP1, to prove using J4 left no sticky state behind.
unplug(ctx, top.j4)
dev = await exi_read(ctx, top.sp1, 0x0000, 4)
dev = await exi_devid(ctx, top.sp1)
print(f"T3 enumerate via SP1 again: {[f'0x{b:02X}' for b in dev]}")
if dev != DEVICE_ID:
errors.append(f"T3 SP1 re-enumerate: got {dev}")