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>
Second protocol mismatch found the same way as the descriptor: by
disassembling libogc's own driver instead of trusting our notes.
COMMAND SPACE IS A 2-BYTE HEADER + DATA PHASE, not a single 4-byte transfer.
CLAUDE.md said [0x40|reg, 00, 00, data] and the decoder followed it, waiting
for a fourth byte and consuming the DATA byte as header byte 2 -- so every
command-space WRITE was silently dropped. libogc's __bba_init writes command
register 2 (its interrupt mask) at both the start and the end of init, so none
of that took effect. What it actually sends:
EXI_Imm (len=2, write) -> 42 00 the whole header, 0x4000 | (reg << 8)
EXI_Sync
EXI_ImmEx(len=1, write) -> 00 the data
Three bytes between CS fall and CS rise. The device-ID read survived the wrong
decode only because SPIMode3Slave answers it on a fast path independent of this
FSM -- which is precisely why no test caught it: the only command-space traffic
under test bypassed the decoder being tested.
CHDR_2/CHDR_3 are replaced by CMD_WRITE, which takes the data byte straight
after header byte 1 and auto-increments the command register so a multi-byte
command write behaves like the register-space path. cmd_wreg latches the
target because the write applies a cycle later, by which time cmd_reg has
moved on. bba_register_file T10 writes command register 2 and reads it back,
and is mutation-tested: restoring the 4-byte decode fails it.
Also replaces the GUESSED init sequence in CLAUDE.md with the real transaction
list. The old one was wrong in three ways that matter: libogc READS the MAC
out of PAR0-5 rather than writing one (so our reset value is what the console
uses), IMR is 0xDF not 0x86, and it writes RWP (0x0016/17) and RHBP (0x001A/1B)
which the register file does not accept. The latter is harmless today -- RHBP
is 0x000F, matching our hardcoded 15-page ring -- but a driver configuring a
smaller ring would be ignored while the assembler kept writing outside it.
New gc_bba_test suite 7 "libogc init" replays the sequence on hardware:
cmd-space write (2-byte header, read back), MAC readable (one 6-byte PAR read,
Nintendo OUI), and init replay (all 30 writes in order, then NWAYS/HIPR/NCRA.SR
must still read correctly -- the old guessed header format turned a NWAYS read
into an NCRA reset, so a misdecoded write is a real risk).
bba.c gains bba_cmd_write/bba_cmd_read in libogc's shape.
Every module testbench and bba_top at EXI_HALF=1/2/8 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two coupled fixes, both exposed by batching TXDATA writes in the test app.
1. eff_addr advanced on EVERY data byte of a register-space write. But
TXDATA (0x48) is a FIFO PORT, not a register: a whole frame is written to
that one address. So an 8-byte write put byte 0 in TXDATA and bytes 1..7
into 0x49+, which the switch silently ignores. The frame was an eighth of
its declared length, TXFrameDrain blocked waiting for the rest, and TI
never set (hardware: txto=16, "no ACK after 20 tries").
SWISS WOULD HAVE HIT THIS. It writes an entire frame with bba_outs to
BBA_WRTXFIFOD -- one transaction, one address -- so its TX would have
delivered ONE BYTE per frame. The path was never exercised because the
test app wrote TXDATA one byte per EXI transaction, giving every byte a
fresh header at 0x48. Every other register must still advance, so that
bba_outs can write PAR0-5 or MAR0-7 as a block; the exception is TXDATA
only.
2. tx_bytes was 16 deep. SPI cannot back-pressure -- there is no way to tell
the GC to wait -- so BBARegisterFile silently drops bytes when w_rdy is
low, which makes the depth the only thing between a drain stall and a
corrupted frame, not a buffering preference. 16 bytes is ~4.7 us at the
EXI byte rate, and TXFrameDrain shares the W5100 bus arbiter with RX and
the socket-3 UDP test, so any longer stall truncated a frame mid-flight.
With (1) fixed, Swiss streams up to 1514 bytes in a single transaction, so
16 could never have held.
Now 2048: a whole maximum-size frame plus margin, so a frame cannot be
truncated even if the drain makes no progress for the entire transaction.
Costs 3 of 27 free BRAMs (ICESTORM_RAM 3/30 -> 6/30, LC 68% -> 70%).
Dennis spotted that the 8-byte workaround could not survive whole-frame
writes, which is what prompted fixing the depth rather than shipping it.
bba_tx_bytes() consequently streams 64-byte chunks (bba_write()'s buffer
limit), so a 60-byte ACK is ONE transaction instead of sixty.
T8 covers both and is mutation-tested: restoring the increment gives
"1/8 bytes MISMATCH -- got [17], want [17, 34, 51, ...]". It now writes 64
bytes so it exercises the deepened FIFO too.
--seeds 8: clk 27.6-29.4 MHz, EXI 51.2-64.7 MHz, 8/8 PASS. Best seed 1
(clk 29.35, EXI 60.80), flashed.
STILL NO FLOW CONTROL on the TX path: a frame larger than 2048 would drop
silently rather than error. Fine for ethernet's 1518 ceiling; revisit if the
multiworld socket work ever wants larger payloads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a2e8e5c batched TXDATA writes into 64-byte chunks and broke ACKs outright --
"no ACK after 20 tries". My regression, and CLAUDE.md warns about exactly
this: tx_bytes is 16 deep and BBARegisterFile silently drops bytes when
w_rdy is low, because SPI cannot back-pressure.
Within a single transaction the bytes arrive back-to-back at EXI speed, far
faster than TXFrameDrain starts consuming, so a 64-byte chunk overran the FIFO,
the frame went out truncated, and no valid ACK ever reached the host.
Byte-by-byte worked only because the per-byte EXI overhead happened to pace
the drain -- which is also why it was slow.
Chunk is now 8, half the FIFO depth: it cannot overflow even if the drain
consumes nothing for the whole transaction, while still cutting a 60-byte ACK
from ~60 transactions to 8. Noted in both the source and CLAUDE.md not to
raise it without deepening tx_bytes or giving the TX path real back-pressure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Self-update is proven end to end on console: 300076 bytes in 294 blocks,
verify OK, /boot.dol boots, and new builds are now delivered this way instead
of by SD card.
bba_tx_bytes() wrote TXDATA byte by byte with bba_w8(), i.e. a full
EXI_Lock / EXI_Select / header / EXI_Deselect / EXI_Unlock PER BYTE -- about
sixty transactions to send one 60-byte ACK. Hardware showed txto=0, so every
ACK did reach the W5100 with TI set; the ~1/3 of blocks the host retried were
not transmit failures but ACKs that missed the host's timeout. The data phase
of a register-space write runs until CS, so there was never a reason to pay
the per-byte cost. It now streams in 64-byte chunks, one transaction each
(64 is bba_write()'s buffer limit, not a protocol limit). t_tx() benefits
equally.
CLAUDE.md records the facility, including that it CANNOT be driven from the
devcontainer -- raw ethernet is link-local and the container has a veth, so
the sender must run on a host holding the physical NIC.
Also records the +1 descriptor offset as OPEN, with what has been ruled out:
not overflow (lapped=0 under RBFI), not the two dropped-write handshake bugs
(fixed and mutation-tested), not write-port contention (ETH has absolute
priority so the port is never stalled; T5 models a stall anyway and the layout
holds), and not the late-response leak (guarded; ratio unchanged at
desc@+0=100 +1=343).
And names the test nobody has written: a bba_top-level EXI DMA read from the
ring WHILE the ethernet side writes into it. ETH preempts EXI reads, so only
that overlap can delay a response past its transaction. Write it before
guessing again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reading one byte and getting 0x00 cannot distinguish 'decode is wrong' from
'reply is late' -- a one-byte window has nowhere for a late answer to show up.
These two probes clock EIGHT data bytes and print every slot, so the position
of 0x17 (NWAYS) or 0x04 (device ID) gives the latency directly, the same way
bit-shift analysis turned 00 00 00 10 into 'correct, 22 bits late'.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
First on-console run: the Identity suite failed with the device ID reading
00 00 00 10 instead of 04 02 02 00. Bit analysis shows that is an EXACT
match for the correct stream delayed by 22 bits -- the device answers
correctly, just too late for a 32-clock window to catch more than its first
set bit. Not corruption, and not a broken link: response latency.
At EXI index 5 (~27 MHz) 22 bit-times is ~815 ns, which is about what the CDC
chain costs (rx AsyncFIFO capture->exi, register-file decode, tx AsyncFIFO
exi->capture, SPI engine) at a 24 MHz exi clock. The design expects the GC to
hold the clock idle between the header write and the data read so the register
file can prefetch the response; libogc's inter-call overhead alone is not
enough at full speed.
Adds bba_gap_us (default 16 us, busy-waited with CS asserted since we hold the
EXI lock) and a Gap sweep test that reports the minimum clean gap at the
current speed -- which measures the latency rather than guessing at it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
libogc's EXI functions return '1 on success, <=0 on error', but every call
site tested '== 0'. A -1 error return therefore read as success, so a failed
EXI_Lock or EXI_Select would fall through and the subsequent transfer would
produce garbage reported as a misleading test failure rather than an EXI
error. All six call sites now test <= 0.
Also retries EXI_Lock for ~100 ms: it fails outright rather than blocking when
another subsystem holds the channel, and Swiss or a libogc driver may hold it
briefly -- without the retry a transient collision surfaces as a test failure.
Builds clean with devkitPPC 16.1.0 / GCC 16, no warnings: 172 KB .dol.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gc_bba_test/ builds a .dol that drives the adapter over raw EXI from the
console and reports pass/fail per test on the TV, navigable with the pad.
Uses libogc's EXI API against EXI0:2 (Serial Port 1) rather than its bba
network driver: driving the register file directly gives each check its own
verdict, so a failure points at a specific register instead of at
'networking'. Six suites - identity, registers, reset/control, EXI link
integrity (including an EXI clock-index sweep), ethernet TX/RX, SPRAM/DMA
streaming.
Encodes the hardware lessons from bring-up so the app cannot mislead:
* the device ID is a one-shot cleared by the first NCRA reset, so it is
reported SKIP on a re-run rather than FAIL
* TI means the frame reached the W5100, NOT the wire - the TX test says so
* register reads clock exactly the requested length (over-clocking
desynchronises the next transaction)
* TXDATA is written one byte per transaction (eff_addr auto-increments)
.devcontainer/Dockerfile gains the devkitPro gamecube-dev group. devkitPro
publishes no .deb on GitHub, so this uses their installer script against
apt.devkitpro.org -- which is 403 from inside the running container's egress
but reachable from a host-side docker build. Rebuild the container to get it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>