docs: record the REAL GC<->BBA EXI protocol; current header format is wrong
Read from the two drivers that will actually talk to this device: libogc (libdb/uIP/bba.c) and Swiss (cube/patches/bba/bba.c). The implemented 2-byte header with a 2-bit length field does not match either. The real protocol has two address spaces selected by bit 7 of byte 0, and no length field at all -- the data phase runs until CS: COMMAND (6-bit reg) one 4-byte readwrite; response in byte 3 REGISTER (16-bit reg) [0x80|w<<6, reg>>8, reg&0xFF, 0x00] + data phase NCRA, IR, NWAYS, RWP, RRP and PAR all live in the register space. The mismatch is destructive rather than merely non-functional: Swiss reading NWAYS sends 80 00 31 00, which the current decoder reads as 'write 0x31 to NCRA' -- and bit 0 of 0x31 is RESET. Every register read would reset the device. The device-ID query is the one case where the two formats coincide, which is exactly why enumeration worked on hardware and nothing else would have. Also explains the 22-bit response latency measured on console: the trailing 0x00 byte in the real header is the device's prefetch window, and the current format spends those bits on the length field, leaving zero slack. Adopting the real format is required for compatibility anyway and recovers ~296 ns of the ~815 ns deficit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
- Getting this wrong means the GC never enumerates the device.
|
||||||
- CS is active **low**, delineates each transaction.
|
- 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 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)
|
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
|
### Device ID Query
|
||||||
On power-on the GC writes `0x0000` (2 bytes) then reads 4 bytes.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user