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>
This commit is contained in:
2026-10-09 10:53:59 +02:00
parent 7587636b48
commit a5ca8b0cda
2 changed files with 61 additions and 22 deletions
+52 -21
View File
@@ -82,6 +82,15 @@ The `exi_bba/` module tree is **fully implemented** with simulation testbenches.
All modules elaborate without errors and pass their unit tests, including
`bba_top` at the real 27 MHz EXI rate (`EXI_HALF=1`).
✅ **FULL TEST SUITE PASSES ON A REAL CONSOLE (2026-10-09)** — every suite in
`gc_bba_test`: 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. The EXI front end runs at the real 27 MHz hardware rate.
(`Raw ID dump` reports SKIP once an earlier suite has issued the NCRA reset
that clears `id_phase` — reflash or reset the FPGA to restore it.)
**Timing closes on EVERY seed (2026-10-03).** `python -m exi_bba.synth
--seeds 8`:
@@ -310,31 +319,53 @@ Side effect: EXI-domain fmax rose 41.6–49.7 → **62.1–65.4 MHz**, because t
setup path into the IOB register became posedge→posedge against a full period
instead of posedge→negedge against a half. Same doubling, seen internally.
### ⚠️ OPEN: command-space (device-ID) response is marginal at 27 MHz
### ✅ Device ID answered by the BIT ENGINE (fixed 2026-10-09)
`t_raw_id` at index 5 returns `00 04 02 02 00 02 02 02` — the ID is **correct
but one byte late**. Cause is latency, not phase: a command-space header is
only 2 bytes, so the response must be in the tx FIFO by the **last rising edge
of the header transfer** (one byte time, 296 ns at 27 MHz). The clock pauses
after that edge, so the inter-transfer gap CANNOT help — there are no edges
during it for anything to update. The round trip is rx `AsyncFIFO` (~125 ns) +
one sync cycle + tx `AsyncFIFO` (~110 ns) ≈ 280–320 ns against 296 ns, i.e.
**marginal by construction**. `HEADER0` already pushes ID byte 0 in the same
cycle it consumes byte 0, so the FSM side cannot be made earlier.
`bba_devid` returned `00 04 02 02` on a console — the ID correct but **one
byte late** — and it failed in its REAL shape (16 µs `gap()`, exactly four
bytes clocked), so it was not a probe artefact. Register-space reads were
fine throughout; only command space was affected.
Register-space reads have the same one-byte-time budget and DO work, so the
command-space path is only slightly slower — consistent with a marginal miss.
**Cause was latency, not phase.** A command-space header is two bytes, so the
reply must be in the tx FIFO by the **last rising edge of the header
transfer** — one byte time, 296 ns at 27 MHz. The round trip is rx `AsyncFIFO`
(~125 ns) + a sync cycle + tx `AsyncFIFO` (~110 ns) ≈ 280–320 ns: marginal by
construction. `HEADER0` already pushed in the same cycle it consumed byte 0,
so the FSM side was already as early as possible.
Note `t_raw_id` is harsher than anything the GC does: it omits the `gap()`
that `bba_devid` uses AND clocks 8 bytes for a 4-byte query (the documented
over-clocking artefact — hence the trailing `02 02 02`). Establish whether
`bba_devid` (Identity suite) passes before treating this as fatal.
⚠️ **The inter-transfer gap cannot help, and this is the non-obvious part.**
The load happens on that last rising edge and the clock is PAUSED afterwards,
so no edge exists during the gap for anything to update. That is why
`bba_devid`'s 16 µs gap made no difference, and why "give it more idle time"
is not a fix. **Pre-pushing at CS-fall does not work either:** the engine's
unconditional per-byte pops would eat it, and the header length is not known
until byte 0 has been decoded — which is the latency in question.
Fix direction if it is real: shorten the CDC round trip, e.g. replace the tx
`AsyncFIFO` with a 2-deep ping-pong CDC (see the lightweight-TX-CDC note).
Pre-pushing the ID at CS-fall does NOT work — the engine's unconditional
per-byte pops would eat it, and the header length is not known until byte 0
has been decoded.
**Fix: take the round trip out of the path.** The device ID is a hardwired
constant and belongs to the physical interface, not the register file, and
`byte 0 == 0x00` identifies the query exactly (bit 7 clear = command space,
bit 6 clear = read, reg 0). `SPIMode3Slave` recognises it and answers
`04 02 02 00` from the EXI clock domain with **no CDC at all**. The only thing
it needs from `sync` is `id_phase`, a single bit that changes once, crossed
with an `FFSynchronizer`.
The register file's ID path was **removed**, not kept as a fallback: left in,
it pushes bytes a 2-byte header has too few pops to drain — exactly how an
earlier version made the *next* enumeration come back a slot late.
Tests: `spi_mode3_slave` **T7** (reply is `04 02 02 00` with the tx FIFO
deliberately feeding `0x00`, so only the fast path can answer) and **T8** (it
is GATED — with `id_arm` low, `0x5A` comes through from the FIFO; without this
the fast path would hijack address 0 forever and the GC could never read NCRA
back after its own reset). `bba_register_file` **T1** asserts the FSM pushes
NOTHING for an ID query.
⚠️ **Simulation cannot reproduce this bug.** `bba_top` passed the device ID at
`EXI_HALF=1` *before* the fix, because the sync domain's latency relative to
the EXI clock in simulation does not match silicon closely enough to miss a
296 ns window. T7/T8 prove the fast path works; they do not prove it is
necessary. Do not "simplify" the ID back into the register file because the
suite stays green — it will, and hardware will not.
### ⚠️ TX bit phase MUST come from the rising-edge counter (fixed 2026-10-03)
+9 -1
View File
@@ -16,7 +16,7 @@ static int t_devid(char *m, size_t n)
u8 b[4];
int rc;
if (g_ncra_reset_done) {
snprintf(m, n, "skipped: one-shot, needs a console power cycle");
snprintf(m, n, "skipped: id_phase is a one-shot; reflash/reset the FPGA");
return 2;
}
if ((rc = bba_devid(b)) < 0) { snprintf(m, n, "EXI error %d", rc); return 1; }
@@ -238,6 +238,14 @@ static int t_raw_id(char *m, size_t n)
u8 b[8];
int i, at = -1, rc;
u16 h = 0x0000;
/* Same one-shot as t_devid. Without this guard a full run reports a
* FAILURE here purely because an earlier suite issued the NCRA reset
* that clears id_phase -- address 0 then reads back NCRA, which is
* correct behaviour, not a fault. */
if (g_ncra_reset_done) {
snprintf(m, n, "skipped: id_phase is a one-shot; reflash/reset the FPGA");
return 2;
}
if ((rc = begin_raw()) < 0) { snprintf(m, n, "EXI error %d", rc); return 1; }
EXI_Imm(BBA_EXI_CHAN, &h, 2, EXI_WRITE, NULL);
EXI_Sync(BBA_EXI_CHAN);