diff --git a/CLAUDE.md b/CLAUDE.md index 59407b6..37c851f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -633,16 +633,58 @@ The full design specification lives in `docs/gc_bba_fpga_design.md`. - Getting this wrong means the GC never enumerates the device. - CS is active **low**, delineates each transaction. -### EXI Transaction Header (2 bytes before data) +### EXI Transaction Header + +⚠️ **THE IMPLEMENTED FORMAT IS WRONG AND MUST BE REPLACED (found 2026-10-03).** +What `exi_bba/bba_register_file.py` implements today is: ``` Byte 0: [7]=write_flag [6:0]=addr[12:6] Byte 1: [7:2]=addr[5:0] [1:0]=xfer_len-1 (0=1B … 3=4B) ``` -Full address = 13 bits → 0x0000–0x1FFF. +That was designed from the spec's own guess. The design doc hedged on it +(§"ID query": *"Alternatively, read the Dolphin source for the exact byte +sequence…"*) and that verification never happened. + +**The REAL protocol**, read from the two drivers that actually run — libogc +(`libdb/uIP/bba.c`) and Swiss (`cube/patches/bba/bba.c`) — has TWO address +spaces, selected by bit 7 of the first byte, and **no length field at all**: + +``` +COMMAND space (6-bit reg) one 4-byte READWRITE transfer + write: [0x40|reg, 00, 00, data] + read : [reg, 00, 00, xx ] <- response is the 4th byte +REGISTER space (16-bit reg) 4-byte header, then a SEPARATE data phase + read : [0x80, reg>>8, reg&0xFF, 0x00] then N bytes read (Swiss uses DMA) + write: [0xC0, reg>>8, reg&0xFF, 0x00] then N bytes written +``` +NCRA, IR, NWAYS, RWP, RRP, PAR… all live in the REGISTER space +(`bba_in8`/`bba_out8`/`bba_ins`/`bba_outs`). Only the device-ID query uses a +short header, which is the single case where the two formats coincide — which +is exactly why enumeration appeared to work and nothing else would have. + +**The mismatch is destructive, not merely non-functional.** A `NWAYS` read +arrives as `80 00 31 00`; the current decoder reads that as *write 0x31 to +NCRA*, and bit 0 of 0x31 is RESET. Every register read Swiss performs would +reset the device. + +**The trailing `0x00` byte is the device's prefetch window**, and that is the +root of the latency problem below: in the real protocol the address is complete +one full byte before the data phase, giving ~8 bit-times (≈296 ns at 27 MHz) +to fetch the response. The format above spends those bits on the length field, +so the address is not known until the final header bit and the device gets +**zero** slack — which is why the first on-console run returned the device ID +22 bit-times late. + +**Data-phase length is whatever the GC clocks** (it streams until CS), so the +register path must stream like the SPRAM path already does — the 4-byte cap +imposed by the 2-bit length field has to go. `bba_ins` reads with **EXI DMA**; +DMA *reads* are fine (the GC's DMA-*write* engine is the buggy one). ### Device ID Query On power-on the GC writes `0x0000` (2 bytes) then reads 4 bytes. -Must return: `0x04 0x02 0x02 0x00`. +Must return: `0x04 0x02 0x02 0x00`. **Hardware-confirmed 2026-10-03** on a +real console — this is the one path that works with the current (wrong) +header format, because `00 00` decodes to address 0 either way. --- diff --git a/docs/exi_protocol_real.md b/docs/exi_protocol_real.md new file mode 100644 index 0000000..5e737e4 --- /dev/null +++ b/docs/exi_protocol_real.md @@ -0,0 +1,101 @@ +# The real GC↔BBA EXI protocol (verified 2026-10-03) + +Read directly from the two drivers that will actually talk to this device: + +- **libogc** — `libdb/uIP/bba.c` (devkitPro/libogc) +- **Swiss** — `cube/patches/bba/bba.c` (emukidid/swiss-gc) + +Swiss is the one that matters; it pokes the EXI hardware registers directly +rather than going through libogc's EXI API. + +## Two address spaces, selected by bit 7 of byte 0 + +``` +COMMAND space — 6-bit register, ONE 4-byte read/write transfer + read [ reg&0x3F, 00, 00, xx ] response is byte 3 + write [0x40|reg&0x3F, 00, 00, data] + +REGISTER space — 16-bit register, 4-byte header THEN a separate data phase + read [0x80, reg>>8, reg&0xFF, 0x00] + N bytes read + write [0xC0, reg>>8, reg&0xFF, 0x00] + N bytes written +``` + +byte 0 bit 7 = register space, bit 6 = write. + +Swiss source: + +```c +uint8_t bba_cmd_in8(uint8_t reg) + { return exi_imm_read_write((reg & 0x3F) << 24, 4); } +void bba_cmd_out8(uint8_t reg, uint8_t val) + { exi_imm_read_write(0x40 << 24 | (reg & 0x3F) << 24 | val, 4); } + +uint8_t bba_in8(uint16_t reg) + { exi_imm_read_write(0x80 << 24 | reg << 8, 4); return exi_imm_read(1); } +void bba_out8(uint16_t reg, uint8_t val) + { exi_imm_read_write(0xC0 << 24 | reg << 8, 4); exi_imm_write(val << 24, 1); } + +void bba_ins(uint16_t reg, void *val, uint32_t len) + { exi_imm_read_write(0x80 << 24 | reg << 8, 4); exi_dma_read(val, len, false); } +``` + +All the BBA registers this project cares about — NCRA (0x00), IR (0x09), +RWP (0x16), RRP (0x18), PAR (0x20), NWAYS (0x31) — are accessed through +`bba_in8`/`bba_out8`/`bba_ins`/`bba_outs`, i.e. the **register space**. + +## Three consequences for the gateware + +**1. There is no length field.** The data phase runs for as long as the GC +clocks it, ending at CS. The register path must stream until CS exactly as the +SPRAM path (≥0x100) already does. The current 2-bit length field caps register +reads at 4 bytes, which is wrong — `bba_ins` reads arbitrary lengths, and with +DMA. + +**2. The trailing `0x00` byte is the prefetch window.** The address is complete +after byte 2 of a 4-byte header, leaving one whole byte — ~8 bit-times, ≈296 ns +at 27 MHz — before the data phase starts. The command space is even more +generous: the register is known after byte 0, giving ~24 bit-times before the +response is needed in byte 3. The protocol was designed to give the device time +to fetch. + +**3. EXI DMA reads are used.** `bba_ins` does `exi_dma_read`. DMA *reads* are +fine; it is the GC's DMA-*write* engine that has the 1-bit-shift bug Swiss +avoids. + +## Why the current implementation is not just wrong but dangerous + +`exi_bba/bba_register_file.py` implements a 2-byte header with the address +split across both bytes and a 2-bit length in the low bits of byte 1. Feeding +it Swiss's traffic: + +| Swiss intends | sends | current decoder sees | +|---|---|---| +| read NWAYS (0x31) | `80 00 31 00` | **write 0x31 to NCRA** — bit 0 = RESET | +| read IR (0x09) | `80 00 09 00` | **write 0x09 to NCRA** — bit 0 = RESET | +| write NCRA | `C0 00 00 00` | write 0x00 to addr 0x1000 | + +Every register read Swiss performs would reset the device. + +The device-ID query is the sole case where the two formats agree (`00 00` +decodes to address 0 either way), which is why enumeration worked on hardware +while nothing else could have. + +## Relationship to the response-latency problem + +First on-console run (2026-10-03, EXI index 5) returned the device ID **22 +bit-times late** — `00 00 00 10` is a bit-exact match for `04 02 02 00` shifted +by 22. ~815 ns, which is about what the CDC chain costs (rx AsyncFIFO +capture→exi, register-file FSM decode, tx AsyncFIFO exi→capture, SPI engine) at +a 24 MHz exi clock. + +The design doc demands data "ready on MISO from the very first clock edge of +the data phase". That looked impossible until the real format turned up: the +protocol hands the device a turnaround byte, and the current header format +spends it on a length field. Adopting the real format therefore does double +duty — it is required for compatibility anyway, and it recovers ~296 ns +(register space) or ~593 ns (command space) of the ~815 ns deficit. + +The remainder still needs latency work: the lightweight ping-pong TX CDC +(already documented as the capture-timing lever), FSM trimming before the first +push, and possibly serving hot registers from a capture-domain shadow so the +response path crosses no clock domain at all.