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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user