Commit Graph

13 Commits

Author SHA1 Message Date
Roflin d83f7beef8 Fix the RX ring descriptor to the format libogc actually parses
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>
2026-10-10 10:27:45 +02:00
Roflin 5ee1b9d07b gc_bba_test: tell a spent id_phase from a real fault using the hardware
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>
2026-10-10 10:17:41 +02:00
Roflin 2b0cbdd809 gc_bba_test: free the RX ring before waiting for a frame
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>
2026-10-10 10:14:19 +02:00
Roflin e20b34b5e9 gc_bba_test: report where ring frames actually are when t_dma fails
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>
2026-10-10 10:09:27 +02:00
Roflin 19044307bf Make the DMA descriptor test follow RRP instead of hardcoding 0x100
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>
2026-10-09 10:55:15 +02:00
Roflin a5ca8b0cda Skip the raw ID dump once id_phase is consumed; record the full console pass
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>
2026-10-09 10:53:59 +02:00
Roflin a5b4d5bafd Add a per-speed raw dump to the GC test app
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>
2026-10-09 10:23:41 +02:00
Roflin 6f151cd1ff gc_bba_test: add raw slot dumps to locate the response
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>
2026-10-03 12:46:46 +02:00
Roflin e682fded4a gc_bba_test: disambiguate the speed sweep result
'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>
2026-10-03 12:43:11 +02:00
Roflin 2458884160 Rework the EXI decoder to the real GC<->BBA protocol
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>
2026-10-03 11:58:52 +02:00
Roflin abc61f3e66 gc_bba_test: add header->data idle gap; device replies 22 bit-times late
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>
2026-10-03 11:37:00 +02:00
Roflin d8725392a5 gc_bba_test: fix inverted EXI error checks; builds clean
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>
2026-10-03 11:24:47 +02:00
Roflin ccf4eb4996 Add on-console GameCube test suite + devkitPPC in the devcontainer
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>
2026-10-03 10:59:37 +02:00