Support BOTH command-space forms: Swiss's 4-byte and libogc's 2-byte

Reading Swiss's own driver (cube/patches/bba/bba.c) shows the previous commit
fixed libogc and BROKE Swiss.  The two disagree, and both are real:

  Swiss   bba_cmd_out8: exi_imm_read_write(0x40<<24 | reg<<24 | val, 4)
          -> ONE 4-byte transfer [0x40|reg, 00, 00, val], and bba_cmd_in8
             returns the LOW byte, so a read response must be in BYTE 3.
  libogc2 bba_cmd_outsnosel: req = (reg<<8)|0x4000;
          EXI_ImmEx(&req, 2, WRITE); EXI_ImmEx(val, len, WRITE)
          -> THREE bytes, value in byte 2, and a read takes byte 2.
  stock libogc: identical to libogc2 (read out of libbba.a).

They share bytes 0 and 1 and differ only in how many bytes follow, so one
decoder serves both: take the LAST data byte before CS rise as the write value
(Swiss sends [00, val], libogc sends [val] -- last is val either way), and push
a read response TWICE so bytes 2 and 3 both carry it.  Swiss clocks 4 bytes and
pops both; libogc clocks 3 and leaves one, which the next transaction's header
pops drain -- a 2-byte header gives exactly one drain slot, so the leftover is
bounded at one and cannot accumulate.

Critically this means NO auto-increment of cmd_reg across the data phase: that
applied Swiss's padding 00 to the register and val to reg+1, which is precisely
how the libogc-only version broke Swiss.

T12 covers the Swiss form and asserts bytes 2 AND 3; T10 keeps the libogc form.
Both mutation-tested: re-adding the auto-increment fails T12 with 0x00 in both
slots, and pushing the response once fails T12's byte 3.  gc_bba_test gains
"cmd-space Swiss" alongside "cmd-space write", with bba_cmd_write4/read4.

Also corrects two things in CLAUDE.md now checkable against three drivers:
- NO driver writes TWD -- not Swiss, not libogc, not libogc2.  The "GC sets
  TWD before streaming" note was self-confirming: t_tx writes TWD because we
  wrote t_tx.  The counted TXDATA length is the primary path, not a fallback.
- Swiss's exi_dma_write rounds the length UP to 32 bytes (OSRoundUp32B), so
  TXDATA gets up to 31 trailing bytes past the frame and a counted length sends
  that much extra.  Real hardware must do the same since Swiss works on it;
  trailing padding past the IP total length is ignored.  A 1514-byte frame
  rounds to 1536 though, over the 1518 maximum.

Both drivers trigger TX with NCRA ST1, which the existing any-ST test accepts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 11:01:33 +02:00
parent 71ec6a2b37
commit b3685237b2
5 changed files with 223 additions and 52 deletions
+58 -19
View File
@@ -1065,25 +1065,51 @@ REGISTER space (16-bit reg) 4-byte header, then a SEPARATE data phase
write: [0xC0, reg>>8, reg&0xFF, 0x00] then N bytes written
```
⚠️ **Command space is a 2-byte header + data phase — corrected 2026-10-10.**
This file previously said "one 4-byte READWRITE transfer,
`[0x40|reg, 00, 00, data]`", and the decoder followed it: it waited for a
fourth byte, consumed the DATA byte as header byte 2, and **silently dropped
every command-space write.** libogc's `__bba_init` writes command register 2
(its interrupt mask) at both the start and the end of init, so none of that
would have taken effect. The real shape, from the disassembly:
⚠️ **COMMAND SPACE HAS TWO FORMS AND BOTH ARE REAL — settled 2026-10-10 by
reading all three drivers.** Register space is the same everywhere; command
space is not:
```
EXI_Imm (len=2, write) -> 42 00 the WHOLE header
EXI_Sync
EXI_ImmEx(len=1, write) -> 00 the data
```
| contract | Swiss patch | libogc (stock) | libogc2 |
|---|---|---|---|
| RX descriptor | packed LE bitfield | packed LE bitfield | packed LE bitfield |
| register space | 4-byte header + data phase | same | same |
| **command space** | **ONE 4-byte transfer**, value/response in **byte 3** | 2-byte header + data phase, **byte 2** | 2-byte header + data phase, **byte 2** |
| TWD on TX | **never written** | never written | never written |
| TX trigger | NCRA **ST1** | NCRA ST1 (`0x04`) | NCRA ST1 |
Three bytes between CS fall and CS rise, not four. `0x4200 = 0x4000 |
(reg << 8)`. The device-ID read survived the wrong decode only because
`SPIMode3Slave` answers it on a fast path independent of the FSM — which is
exactly why no test caught it. `bba_register_file` **T10** now writes command
register 2 and reads it back, and is mutation-tested against the 4-byte decode.
Sources, so this can be re-checked rather than re-guessed:
- Swiss — `cube/patches/bba/bba.c`, `bba_cmd_out8`:
`exi_imm_read_write(0x40<<24 | (reg&0x3F)<<24 | val, 4)`, and `bba_cmd_in8`
returns the **low byte** of that 32-bit result.
- libogc2 — `lwip/arch/gc/netif/gcif.c`, `bba_cmd_outsnosel`:
`req = (reg<<8)|0x4000; EXI_ImmEx(&req, 2, WRITE); EXI_ImmEx(val, len, WRITE)`.
- stock libogc — the same, read out of `libbba.a` (`gcif.o`, `__bba_init`):
`EXI_Imm(len=2)` of `42 00`, then `EXI_ImmEx(len=1)` of the data.
**How one decoder serves both:** they share bytes 0 and 1, and differ only in
how many bytes follow. So the device takes the **LAST data byte before CS
rise** as the write value (Swiss sends `[00, val]`, libogc sends `[val]` —
last is `val` either way), and **pushes a read response TWICE** so byte 2 and
byte 3 both carry it. Swiss clocks 4 bytes and pops both copies; libogc clocks
3 and leaves one, which the next transaction's header pops drain — a 2-byte
header gives exactly one drain slot, so the leftover is bounded at one and
cannot accumulate.
⚠️ **There must be NO auto-increment of `cmd_reg` across the data phase.** It
would apply Swiss's padding `0x00` to the register and `val` to `reg+1`. This
is how the libogc-only version broke Swiss.
History worth keeping, because the failure mode repeated: this file first had
Swiss's 4-byte form only, so libogc's writes were dropped (the decoder waited
for a fourth byte and ate the data byte as header byte 2). It was then
"fixed" to libogc's form only, which would have dropped Swiss's writes. Only
reading all three drivers showed that neither is "the" driver. The device-ID
read masked the original bug throughout, because `SPIMode3Slave` answers it on
a fast path independent of this FSM.
Tests: `bba_register_file` **T10** (libogc form) and **T12** (Swiss form,
asserting bytes 2 AND 3), both mutation-tested — re-adding the auto-increment
fails T12, and pushing the response once fails T12's byte 3.
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 — the single case where the two formats coincide.
@@ -1250,8 +1276,13 @@ parallel master is the default — see "W5100 vs W5500".)
| 2 | read reg `0x0000` (NCRA) |
| 3 | write reg `0x0000` = `0x04` (NCRA ST) |
⚠️ **There is no `TWD_LO`/`TWD_HI` write anywhere**, so the device has to
COUNT the bytes it was handed. `BBARegisterFile` now keeps `txdata_cnt`,
⚠️ **No driver writes `TWD_LO`/`TWD_HI` — not Swiss, not libogc, not
libogc2** (Swiss: `bba_output` is `bba_outs(BBA_WRTXFIFOD, data, size)` then
NCRA ST1; libogc2: `bba_outsregister(BBA_WRTXFIFOD)` then NCRA ST1). So the
device has to COUNT the bytes it was handed. The TWD path is OUR test app's
convention, not a driver's — which is why the "GC sets TWD before streaming"
note in the TWD fix was self-confirming: `t_tx` writes TWD because we wrote
`t_tx`. `BBARegisterFile` now keeps `txdata_cnt`,
incremented per byte accepted at `0x0048`, and the NCRA ST fallback queues
**that** rather than `tx_frame_len`. Previously it queued `tx_frame_len`,
which a driver that skips TWD never sets — so the frame would have gone out
@@ -1265,6 +1296,14 @@ before streaming is what lets `TXFrameDrain` consume concurrently and keeps
`tx_bytes` a rate-matching buffer instead of whole-frame storage (see the TWD
fix note). Swiss writes TWD; libogc does not. Support both.
⚠️ **Swiss rounds the DMA length UP to 32 bytes** (`exi_dma_write` sets
`EXI[..][2] = OSRoundUp32B(len)`), so TXDATA receives up to 31 trailing bytes
beyond the frame. With a counted length the frame goes out that much longer.
Real hardware must behave the same way, since Swiss works on it — trailing
padding past the IP total length is ignored by receivers. Worth knowing before
treating a slightly-too-long TX frame as a bug. Note a 1514-byte frame rounds
to 1536, over the 1518 maximum.
⚠️ **libogc streams the payload with `EXI_Dma`** — the 32-byte-aligned bulk via
`EXI_Dma` after a `PPCDCacheStoreAsync`, then `EXI_ImmEx` for the remainder.
This file says elsewhere that EXI DMA *writes* are avoided because the GC's