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>