"Swiss + libogc init sequences and transfer shapes" is 49 characters and
wrapped by exactly one. The menu row is 28 columns of prefix --
" " + name(%-14s) + " " + "pp/ff/kk" + " " -- and the console is 76 wide
(console_init starts at x=20 on a 640-pixel framebuffer at 8 pixels per
character), leaving 48.
Now "Swiss + libogc init and transfer shapes", 39 characters, in line with the
41 of the longest original. Added a comment above g_suites with the 48-column
budget and where it comes from, so the next suite does not rediscover it by
wrapping on a console.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Third contract checked against libogc rather than our notes. __bba_link_tx is
four transactions -- read NWAYS, ONE register-space write of the whole frame to
0x0048, read NCRA, write NCRA = 0x04 -- with no TWD_LO/TWD_HI anywhere. So the
device has to count the bytes it was handed.
The NCRA ST fallback queued tx_frame_len, which only TWD sets, so any driver
that skips TWD would have transmitted a stale or zero length. BBARegisterFile
now keeps txdata_cnt, incremented per byte accepted at 0x0048 and cleared
whenever a frame is queued from either path, and the ST path queues that.
T11 drives TX in libogc's shape with no TWD write and checks both the queued
length and the drained bytes. Mutation-tested: switching the ST path back to
tx_frame_len queues length 0, which is exactly the bug. gc_bba_test gains the
hardware counterpart, "TX without TWD", which deliberately omits the TWD writes
that t_tx makes.
The TWD path stays and is still preferred when present: writing the length
before streaming is what lets TXFrameDrain consume concurrently and keeps
tx_bytes a rate-matching buffer rather than whole-frame storage. Swiss writes
TWD, libogc does not, so both are supported.
Also recorded: libogc streams the payload with EXI_Dma (32-byte-aligned bulk
after PPCDCacheStoreAsync, then EXI_ImmEx for the remainder). CLAUDE.md said
EXI DMA writes are avoided for the GC's 1-bit-shift bug; that is true of Swiss,
not libogc. Nothing in the gateware changes -- a DMA write is just bytes
clocked in -- but the path will be exercised, and if that bug is real it
appears as corrupted TX frames that are not fixable here.
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>
The assembler wrote 00 00 len[11:8] len[7:0] with bytes 0 and 1 zero. The real
format, read out of libogc's own BBA driver, is a PACKED 32-BIT LITTLE-ENDIAN
word: next_packet_ptr in bits 0-11, packet_len in bits 12-23, status in bits
24-31. Under that driver our descriptor for a 342-byte frame decoded as a
12-byte frame with a next-pointer of 0 into the reserved page, so Swiss and
anything else built on libogc would have initialised perfectly and then
misparsed every received frame.
Found by disassembling /opt/devkitpro/libogc/lib/cube/libbba.a (gcif.o,
bba_interrupt) rather than by guessing:
lwbrx r9,0,r9 # 4 bytes as a little-endian word
rlwinm r4,r9,20,20,31 # packet_len = (word >> 12) & 0xFFF
addi r4,r4,-4 # payload = packet_len - 4
cmpwi r4,1540
srwi r9,r9,24 # status = word >> 24
and. r9,r9,r18 # r18 = 0x..84 -> errors are 0x80 | 0x04
and the low 12 bits are written straight back to RRP (0x0018/0x0019), which is
why next_packet_ptr has to be there at all -- it is the driver's read pointer.
The length semantics and addressing were already correct (descriptor at
page<<8, body at +4, length including the descriptor); only the packing was
wrong and next_packet_ptr was never written.
RXFrameAssembler now computes the next page BEFORE writing the descriptor
(CALC_NEXT, formerly ADVANCE_RWP, moved ahead of the backfill) and writes bytes
0-3 as WRITE_D0..WRITE_D3.
WRITE_D3 fixes a latent bug this exposed: WRITE_PLACEHOLDER issues four writes
but the first lands twice, so desc_base+3 was NEVER written. That was harmless
while the length write covered byte 3. As the status byte it would have been
whatever the previous frame left in that cell, and a stale 0x80 or 0x04 makes
the driver discard the frame.
Readers updated to decode identically: netboot (which also now takes RRP from
the descriptor's own next_packet_ptr, as libogc does, instead of recomputing
the page span) and t_dma, including its page map. netboot's +0/+1 offset probe
is removed -- the underrun that shifted descriptors is fixed and confirmed on
console, and under a packed format a +1 read is not a shifted descriptor but
garbage.
The assembler's testbench now decodes with libogc_decode(), reproducing the
instruction sequence above, so the tests check the DRIVER'S algorithm rather
than our own packing. Every module testbench and bba_top at EXI_HALF=1/2/8
pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ID tests guarded on g_ncra_reset_done, which is software state standing in
for hardware state. The two come apart exactly when a build is delivered over
the wire: netboot replaces the DOL and restarts the app with the flag clear,
while the FPGA keeps running with id_phase already spent by the previous run.
The app then queries address 0, gets NCRA back -- correct behaviour -- and
reported a FAILURE for it. That is why the ID tests would sometimes skip and
sometimes fail for the same expected condition.
Both tests now ask the hardware instead. HIPR and NWAYS are hardwired
constants in the register file and do not depend on id_phase, so if both read
correctly the EXI path is demonstrably fine and a wrong answer at address 0 can
only be a spent id_phase -> SKIP, with "reflash FPGA" in the message. If they
are wrong too, something real is broken and the test still FAILS.
The flag is kept only as a fast path for a reset issued during the same run,
with a comment saying it can be stale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
t_rx reported "no frame in 2 s (RWP still 0x000F)" on a healthy adapter after
the Ethernet suite had been run a few times. Page 15 is the giveaway: the app
never writes RRP, so ambient broadcast traffic fills the 15-page ring until
free = (1 + 15 - 15) mod 15 = 1, below _MAX_FRAME_PAGES (6). RBFI then drops
every frame -- correctly, that is what it is for -- RWP stops advancing
permanently, and a test that waits for RWP to move can never pass again for the
rest of the session.
t_rx now catches RRP up to RWP before waiting, declaring what is already in the
ring consumed. That is what a real driver does once it has read the frames out
and what netboot already does at start-up. It also leaves exactly the newly
received frame unconsumed at RRP, which is where the SPRAM/DMA suite reads, so
t_dma still has something to parse.
No gateware change: the drop behaviour was correct and the bug was in the test
app's failure to consume.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
t_dma said "p1 len 0x0002 implausible (RWP=11)", which names the symptom but
not the cause: a length alone cannot distinguish an unwritten descriptor from a
shifted read from a frame written at a page other than the one RRP points at.
On failure it now walks all 15 ring pages, classifies each descriptor ('-'
all-zero/never written, 'o' plausible length, 'x' non-zero but implausible,
'?' read failed) and prints that map plus the 4 raw descriptor bytes at the RRP
page. One run then shows which of those it is.
Diagnostic only; no change to the pass/fail criterion or to any gateware.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
t_dma reported "descriptor len implausible (ring empty?)" and skipped on a
board that had just PASSED the RX test, so a frame had certainly arrived --
RWP advanced and RI fired. The test was looking in the wrong place.
RWP/RRP are PAGE pointers into the 15-page ring, so the next unconsumed
frame's descriptor lives at RRP*256. Reading SPRAM_BASE (0x0100)
unconditionally only works while nothing has been consumed and the ring has
not wrapped; once RRP has moved, 0x100 holds a stale or overwritten page.
t_dma now reads RRP, streams from RRP*256, and distinguishes the cases it used
to lump together:
- RRP == RWP -> genuinely empty, SKIP, and says to send it a frame
- page outside 1-15 -> FAIL, with the page number
- implausible len -> FAIL, and reports RWP too so it is diagnosable
The old message could not tell "no frame yet" from "looked in the wrong
place", which is exactly why the skip was mysterious.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The whole gc_bba_test suite now passes on a real GameCube: Identity (device
ID 04 02 02 00, HIPR, NWAYS), Registers, Reset/control, EXI integrity (speed
sweep ok=[1,1,1,1,1,1], per-speed dump clean at all six indices, raw reg dump
0x17 in slot 0), Ethernet, and SPRAM/DMA -- at the real 27 MHz EXI rate.
The one reported failure was t_raw_id, and it was not a fault: an earlier
suite had already issued the NCRA reset that clears id_phase, after which
address 0 correctly reads back NCRA. t_devid already guarded for this; t_raw_id
did not, so a full run cried wolf. It now returns SKIP like t_devid, and both
messages say reflash/reset the FPGA rather than power-cycle the console, which
is what actually restores id_phase.
CLAUDE.md records the device-ID fix and, importantly, that simulation CANNOT
reproduce the bug it fixed -- bba_top passed the ID at EXI_HALF=1 beforehand,
because sim's sync-to-EXI latency ratio does not match silicon closely enough
to miss a 296 ns window. Anyone "simplifying" the ID back into the register
file will keep a green suite and break hardware.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing ~9 ns from the MISO path (EXI_J4 merge, the CS mux, then the whole
fabric route via an SB_IO output register) left the hardware result BYTE
IDENTICAL: 0B 80 at index 5, ok=[1,1,1,1,1,0]. A ~9 ns reduction against an
18.5 ns half-period budget cannot change nothing if propagation were the
cause, so it is not, despite being speed-dependent.
t_raw_sweep reads the same 8 slots at EVERY speed index and reports the first
two bytes per index, so the shift can be seen as a curve rather than a single
point: a shift that appears or grows with clock rate is a timing problem, one
that is constant across indices is structural. Run against a PHASE_METER
bitstream it reads the bit engine's byte phase directly at each speed, with
the register file and both byte FIFOs out of the path.
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>
'highest good = 0' meant both 'only index 0 works' and 'nothing works', which
are opposite diagnoses -- the first says oversampling, the second rules it
out. Now reports the per-index map and says NONE explicitly.
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>