Fix command-space writes, and replay libogc's real init sequence

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>
This commit is contained in:
2026-10-10 10:42:17 +02:00
parent d83f7beef8
commit 0aa1fa884a
5 changed files with 324 additions and 31 deletions
+110
View File
@@ -389,6 +389,109 @@ static int t_rx(char *m, size_t n)
return 1;
}
/* ─────────────────── suite 7: libogc init sequence ─────────────────── */
/* These replay what libogc's own BBA driver does, in its real transaction
* shapes, rather than what our docs said it does. The whole sequence was
* read out of /opt/devkitpro/libogc/lib/cube/libbba.a (gcif.o, __bba_init) --
* see CLAUDE.md. Two of the mismatches it exposed were silent:
* - command space is a 2-byte header + data phase, not a 4-byte transfer,
* so every command-space write was being dropped; and
* - libogc READS the MAC out of PAR0-5 rather than writing it. */
static int t_cmd_write(char *m, size_t n)
{
int rc, rb;
/* Command register 2 is libogc's interrupt mask: it writes 0x00
* (IRMASKALL) at the start of init and 0xF8 (IRMASKNONE) at the end. */
if ((rc = bba_cmd_write(0x02, 0xF8)) < 0) {
snprintf(m, n, "cmd write failed: EXI error %d", rc); return 1;
}
if ((rb = bba_cmd_read(0x02)) < 0) {
snprintf(m, n, "cmd read failed: EXI error %d", rb); return 1;
}
if (rb != 0xF8) {
snprintf(m, n, "wrote 0xF8, read back 0x%02X "
"(2-byte cmd header not honoured?)", rb);
return 1;
}
/* Put it back the way libogc leaves it at the start of init. */
bba_cmd_write(0x02, 0x00);
rb = bba_cmd_read(0x02);
snprintf(m, n, "0xF8 then 0x00 read back (0x%02X)", rb);
return rb == 0x00 ? 0 : 1;
}
static int t_mac_is_readable(char *m, size_t n)
{
u8 mac[6];
/* libogc does ONE 6-byte register read of 0x0020 and uses whatever it
* finds as the console's MAC -- it does not write one. So PAR0-5 must
* hold a sane address out of reset, and a 6-byte read must work. */
if (bba_read(REG_PAR0, mac, 6) < 0) {
snprintf(m, n, "6-byte PAR read failed"); return 1;
}
snprintf(m, n, "%02X:%02X:%02X:%02X:%02X:%02X%s",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
(mac[0] == 0x00 && mac[1] == 0x09 && mac[2] == 0xBF)
? " (Nintendo OUI)" : " - NOT a Nintendo OUI");
if (mac[0] == 0xFF || (mac[0] | mac[1] | mac[2] | mac[3] | mac[4] | mac[5]) == 0)
return 1; /* all-ones or all-zero is unusable */
return (mac[0] == 0x00 && mac[1] == 0x09 && mac[2] == 0xBF) ? 0 : 1;
}
static int t_init_sequence(char *m, size_t n)
{
/* The register writes libogc issues, in order, with the values it uses.
* Most are store-only (PHY/analog) and we ignore them; what matters is
* that none of them is misdecoded into something harmful -- the old
* guessed header format turned a NWAYS read into an NCRA reset, so this
* is a real risk, not a theoretical one. */
static const struct { u16 a; u8 v; } seq[] = {
{0x0060, 0x00},
{0x0000, 0x01}, /* NCRA reset */
{0x0000, 0x00},
{0x0058, 0x80}, {0x0059, 0x00}, {0x005A, 0x03}, {0x005B, 0x83},
{0x005C, 0x32}, {0x005D, 0xFE}, {0x005E, 0x1F}, {0x005F, 0x1F},
{0x0001, 0x52}, {0x0060, 0x74},
{0x0014, 0x00}, {0x0015, 0x06}, {0x0050, 0x80},
{0x000C, 0x00}, {0x000D, 0x00}, {0x000A, 0x01}, {0x000B, 0x00},
{0x001A, 0x0F}, {0x001B, 0x00}, /* RHBP = page 15 */
{0x0016, 0x01}, {0x0017, 0x00}, /* RWP = page 1 */
{0x0018, 0x01}, {0x0019, 0x00}, /* RRP = page 1 */
{0x0032, 0x08},
{0x0000, 0x08}, /* NCRA SR: start receive */
{0x0009, 0xFF}, /* IR: clear all */
{0x0008, 0xDF}, /* IMR (NOT 0x86) */
};
unsigned i;
int v;
for (i = 0; i < sizeof(seq) / sizeof(seq[0]); i++) {
if (bba_w8(seq[i].a, seq[i].v) < 0) {
snprintf(m, n, "write %u (0x%04X=0x%02X) failed",
i, seq[i].a, seq[i].v);
return 1;
}
}
g_ncra_reset_done = 1;
/* The sentinels must still read correctly afterwards -- if any write in
* that list had been misdecoded, the most likely casualty is the state
* these two report. */
if ((v = bba_r8(REG_NWAYS)) != NWAYS_LINKUP) {
snprintf(m, n, "NWAYS 0x%02X after init (want 0x%02X)",
v, NWAYS_LINKUP);
return 1;
}
if ((v = bba_r8(REG_HIPR)) != HIPR_PRESENT) {
snprintf(m, n, "HIPR 0x%02X after init (want 0x%02X)",
v, HIPR_PRESENT);
return 1;
}
v = bba_r8(REG_NCRA);
snprintf(m, n, "%u writes replayed; NCRA=0x%02X NWAYS/HIPR ok",
(unsigned)(sizeof(seq) / sizeof(seq[0])), v);
return (v & 0x08) ? 0 : 1; /* SR must still be set */
}
/* ─────────────────────── suite 6: SPRAM / DMA ──────────────────────── */
static int t_dma(char *m, size_t n)
@@ -518,6 +621,12 @@ static test_t s_spram[] = {
{"Stream length", "reads >= 0x100 stream until CS, length-independent",t_dma_stream_len, T_PENDING, {0}},
};
static test_t s_init[] = {
{"cmd-space write","2-byte header + data phase, as libogc sends it", t_cmd_write, T_PENDING, {0}},
{"MAC readable", "libogc READS the MAC from PAR0-5; it must be sane", t_mac_is_readable, T_PENDING, {0}},
{"init replay", "libogc's 30 init writes, in order, do no harm", t_init_sequence, T_PENDING, {0}},
};
suite_t g_suites[] = {
{"Identity", "device enumerates and reports itself", s_identity, 3},
{"Registers", "register file is readable and writable", s_regs, 4},
@@ -525,6 +634,7 @@ suite_t g_suites[] = {
{"EXI integrity", "the EXI link is error-free, and how fast", s_exi, 6},
{"Ethernet", "frames get to and from the W5100", s_eth, 3},
{"SPRAM/DMA", "bulk reads stream correctly from the ring", s_spram, 2},
{"libogc init", "the real driver's init sequence and shapes", s_init, 3},
};
const int g_nsuites = sizeof(g_suites) / sizeof(g_suites[0]);