Files
rebbarb/gc_bba_test/source/tests.c
T
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

467 lines
17 KiB
C

#include "tests.h"
#include "bba.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <ogc/lwp_watchdog.h>
int g_ncra_reset_done = 0;
static void msleep(u32 ms) { usleep(ms * 1000); }
/* ───────────────────────── suite 1: identity ───────────────────────── */
static int t_devid(char *m, size_t n)
{
u8 b[4];
int rc;
if (g_ncra_reset_done) {
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; }
snprintf(m, n, "%02X %02X %02X %02X (want 04 02 02 00)", b[0], b[1], b[2], b[3]);
return (b[0]==0x04 && b[1]==0x02 && b[2]==0x02 && b[3]==0x00) ? 0 : 1;
}
static int t_hipr(char *m, size_t n)
{
int v = bba_r8(REG_HIPR);
if (v < 0) { snprintf(m, n, "EXI error %d", v); return 1; }
snprintf(m, n, "0x%02X (want 0x%02X)", v, HIPR_PRESENT);
return v == HIPR_PRESENT ? 0 : 1;
}
static int t_nways(char *m, size_t n)
{
int v = bba_r8(REG_NWAYS);
if (v < 0) { snprintf(m, n, "EXI error %d", v); return 1; }
snprintf(m, n, "0x%02X (want 0x%02X = 100M full duplex)", v, NWAYS_LINKUP);
return v == NWAYS_LINKUP ? 0 : 1;
}
/* ───────────────────────── suite 2: registers ──────────────────────── */
static int rw_check(u16 addr, const u8 *pat, u32 len, const u8 *restore,
char *m, size_t n, const char *what)
{
u8 got[4];
int rc;
if ((rc = bba_write(addr, pat, len)) < 0) { snprintf(m, n, "write failed %d", rc); return 1; }
if ((rc = bba_read(addr, got, len)) < 0) { snprintf(m, n, "read failed %d", rc); return 1; }
if (restore) bba_write(addr, restore, len);
if (memcmp(got, pat, len) != 0) {
snprintf(m, n, "%s readback %02X %02X %02X %02X", what,
got[0], len>1?got[1]:0, len>2?got[2]:0, len>3?got[3]:0);
return 1;
}
snprintf(m, n, "%s wrote+read back %u bytes OK", what, (unsigned)len);
return 0;
}
static int t_par_lo(char *m, size_t n)
{
static const u8 pat[4] = {0xDE,0xAD,0xBE,0xEF};
static const u8 def[4] = {0x00,0x09,0xBF,0x00};
return rw_check(REG_PAR0, pat, 4, def, m, n, "PAR0-3");
}
static int t_par_hi(char *m, size_t n)
{
static const u8 pat[2] = {0x5A,0xA5};
static const u8 def[2] = {0x00,0x01};
return rw_check(REG_PAR4, pat, 2, def, m, n, "PAR4-5");
}
static int t_imr(char *m, size_t n)
{
int v;
if (bba_w8(REG_IMR, 0x86) < 0) { snprintf(m, n, "write failed"); return 1; }
v = bba_r8(REG_IMR);
snprintf(m, n, "0x%02X (want 0x86)", v);
return v == 0x86 ? 0 : 1;
}
static int t_ir_w1c(char *m, size_t n)
{
int before, after;
before = bba_r8(REG_IR);
if (before < 0) { snprintf(m, n, "read failed"); return 1; }
if (bba_w8(REG_IR, 0xFF) < 0) { snprintf(m, n, "write failed"); return 1; }
after = bba_r8(REG_IR);
snprintf(m, n, "0x%02X -> write 0xFF -> 0x%02X (want 0x00)", before, after);
return after == 0x00 ? 0 : 1;
}
/* ─────────────────────── suite 3: reset & control ──────────────────── */
static int t_ncra_reset(char *m, size_t n)
{
int i, v;
if (bba_w8(REG_NCRA, NCRA_RESET) < 0) { snprintf(m, n, "write failed"); return 1; }
g_ncra_reset_done = 1;
for (i = 0; i < 50; i++) {
v = bba_r8(REG_NCRA);
if (v < 0) { snprintf(m, n, "read failed"); return 1; }
if (!(v & NCRA_RESET)) {
snprintf(m, n, "self-cleared after %d poll(s)", i);
/* the W5100 re-inits behind this: RSTn pulse + 83 ms settle x2 */
msleep(400);
return 0;
}
msleep(5);
}
snprintf(m, n, "RESET bit never cleared (NCRA=0x%02X)", v);
return 1;
}
static int t_ncra_sr(char *m, size_t n)
{
int v;
if (bba_w8(REG_NCRA, NCRA_SR) < 0) { snprintf(m, n, "write failed"); return 1; }
v = bba_r8(REG_NCRA);
snprintf(m, n, "0x%02X (want 0x%02X, start-receive set)", v, NCRA_SR);
return v == NCRA_SR ? 0 : 1;
}
/* ──────────────────── suite 4: EXI link integrity ──────────────────── */
static int t_repeat(char *m, size_t n)
{
int i, bad = 0;
for (i = 0; i < 200; i++)
if (bba_r8(REG_NWAYS) != NWAYS_LINKUP) bad++;
snprintf(m, n, "%d/200 reads wrong at EXI speed index %u", bad, (unsigned)bba_speed);
return bad == 0 ? 0 : 1;
}
static int t_speed_sweep(char *m, size_t n)
{
u32 saved = bba_speed, s;
u32 best = 0;
int any = 0;
char buf[40];
int off = 0;
buf[0] = 0;
for (s = 0; s <= EXI_SPEED32MHZ; s++) {
int i, bad = 0;
bba_speed = s;
for (i = 0; i < 40; i++)
if (bba_r8(REG_NWAYS) != NWAYS_LINKUP) bad++;
off += snprintf(buf + off, sizeof(buf) - off, "%s%u", s ? "," : "",
(unsigned)(bad ? 0 : 1));
if (!bad) { best = s; any = 1; }
}
bba_speed = saved;
/* `best` of 0 is ambiguous (index 0 works vs nothing works), so report
* the per-index map and say NONE explicitly. */
if (!any)
snprintf(m, n, "ok=[%s] NONE work - not an oversampling issue", buf);
else
snprintf(m, n, "ok=[%s] highest good idx=%u (5=27MHz)", buf,
(unsigned)best);
return (any && best >= EXI_SPEED32MHZ) ? 0 : 1;
}
static int t_gap_sweep(char *m, size_t n)
{
static const u32 gaps[] = {0, 1, 2, 4, 8, 16, 32};
u32 saved = bba_gap_us;
int i, g, best = -1;
for (g = 0; g < (int)(sizeof(gaps)/sizeof(gaps[0])); g++) {
int bad = 0;
bba_gap_us = gaps[g];
for (i = 0; i < 30; i++)
if (bba_r8(REG_NWAYS) != NWAYS_LINKUP) bad++;
if (!bad) { best = (int)gaps[g]; break; }
}
bba_gap_us = saved;
if (best < 0) {
snprintf(m, n, "no gap up to 32us gave clean reads at speed idx %u",
(unsigned)bba_speed);
return 1;
}
snprintf(m, n, "min clean header->data gap = %d us at speed idx %u",
best, (unsigned)bba_speed);
return 0;
}
static int t_raw_sweep(char *m, size_t n)
{
/* The SAME 8-slot read at EVERY speed index, reported as the first two
* bytes per index. This is the measurement that distinguishes the two
* remaining explanations for a one-bit MISO lag:
* - a shift that APPEARS or GROWS with clock rate is a timing problem;
* - a shift that is constant across indices is structural.
* Run it against a PHASE_METER bitstream and every byte should read 80
* (aligned) / 40 (one bit late) / 20 (two bits late), with the register
* file and both byte FIFOs out of the path entirely. */
u32 saved = bba_speed, s;
int off = 0, bad = 0;
m[0] = 0;
for (s = 0; s <= EXI_SPEED32MHZ; s++) {
u8 b[8];
bba_speed = s;
if (bba_read(REG_NWAYS, b, sizeof(b)) < 0) {
off += snprintf(m + off, n - off, "%u:ERR ", (unsigned)s);
bad++;
continue;
}
off += snprintf(m + off, n - off, "%u:%02X%02X ",
(unsigned)s, b[0], b[1]);
if (b[0] != NWAYS_LINKUP)
bad++;
}
bba_speed = saved;
return bad ? 1 : 0;
}
static int t_raw_reg(char *m, size_t n)
{
/* Clock EIGHT data bytes for a one-byte register read and show them all.
* A correct device puts NWAYS (0x17) in slot 0. If it shows up later the
* reply is merely LATE and the slot index gives the latency directly; if
* it never appears the decode is wrong, not slow. */
u8 b[8];
int i, at = -1;
if (bba_read(REG_NWAYS, b, sizeof(b)) < 0) { snprintf(m, n, "EXI error"); return 1; }
for (i = 0; i < 8; i++) if (b[i] == NWAYS_LINKUP) { at = i; break; }
snprintf(m, n, "%02X %02X %02X %02X %02X %02X %02X %02X | 0x17 %s%d",
b[0],b[1],b[2],b[3],b[4],b[5],b[6],b[7],
at < 0 ? "absent" : "at slot ", at < 0 ? 0 : at);
return at == 0 ? 0 : 1;
}
static int t_raw_id(char *m, size_t n)
{
/* Same for the device ID: 2-byte header, then clock 8 instead of 4. */
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);
memset(b, 0, sizeof(b));
EXI_ImmEx(BBA_EXI_CHAN, b, sizeof(b), EXI_READ);
end_raw();
for (i = 0; i < 8; i++) if (b[i] == 0x04) { at = i; break; }
snprintf(m, n, "%02X %02X %02X %02X %02X %02X %02X %02X | 0x04 %s%d",
b[0],b[1],b[2],b[3],b[4],b[5],b[6],b[7],
at < 0 ? "absent" : "at slot ", at < 0 ? 0 : at);
return at == 0 ? 0 : 1;
}
/* ───────────────────────── suite 5: ethernet ───────────────────────── */
static u8 g_mac[6] = {0x00,0x09,0xBF,0x00,0x00,0x01};
static const u8 SRC_IP[4] = {10,0,0,99};
static const u8 DST_IP[4] = {10,0,0,50};
static int t_mac_read(char *m, size_t n)
{
u8 a[4], b[2];
if (bba_read(REG_PAR0, a, 4) < 0 || bba_read(REG_PAR4, b, 2) < 0) {
snprintf(m, n, "PAR read failed"); return 1;
}
memcpy(g_mac, a, 4); memcpy(g_mac + 4, b, 2);
snprintf(m, n, "%02X:%02X:%02X:%02X:%02X:%02X",
g_mac[0],g_mac[1],g_mac[2],g_mac[3],g_mac[4],g_mac[5]);
return (g_mac[0]==0x00 && g_mac[1]==0x09 && g_mac[2]==0xBF) ? 0 : 1;
}
static u32 build_arp(u8 *f)
{
memset(f, 0, 60);
memset(f, 0xFF, 6); /* dst: broadcast */
memcpy(f + 6, g_mac, 6); /* src MAC */
f[12] = 0x08; f[13] = 0x06; /* ethertype ARP */
f[14] = 0x00; f[15] = 0x01; /* HTYPE ethernet */
f[16] = 0x08; f[17] = 0x00; /* PTYPE IPv4 */
f[18] = 6; f[19] = 4; /* HLEN / PLEN */
f[20] = 0x00; f[21] = 0x01; /* OPER request */
memcpy(f + 22, g_mac, 6); /* SHA */
memcpy(f + 28, SRC_IP, 4); /* SPA */
memcpy(f + 38, DST_IP, 4); /* TPA (THA stays zero) */
return 60;
}
static int t_tx(char *m, size_t n)
{
u8 f[60];
u32 len = build_arp(f);
int i, ir = 0;
bba_w8(REG_IR, 0xFF); /* clear IR */
bba_w8(REG_TWD_LO, (u8)(len & 0xFF)); /* frame length */
bba_w8(REG_TWD_HI, (u8)((len >> 8) & 0xFF));
if (bba_tx_bytes(f, len) < 0) { snprintf(m, n, "TXDATA stream failed"); return 1; }
bba_w8(REG_NCRA, NCRA_SR | NCRA_ST0); /* fire */
for (i = 0; i < 60; i++) {
ir = bba_r8(REG_IR);
if (ir >= 0 && (ir & IR_TI)) {
snprintf(m, n, "%u-byte ARP sent, TI set after %d poll(s)", (unsigned)len, i);
return 0;
}
msleep(10);
}
/* TI only means TXFrameDrain handed the frame to the W5100 -- it is NOT
* proof the chip put it on the wire. Absence of TI is still a clear
* failure though. */
snprintf(m, n, "TI never set (IR=0x%02X) - W5100 may be wedged", ir);
return 1;
}
static int t_rx(char *m, size_t n)
{
u8 r0[2], r1[2];
int i, ir;
if (bba_read(REG_RWP_LO, r0, 2) < 0) { snprintf(m, n, "RWP read failed"); return 1; }
for (i = 0; i < 200; i++) { /* up to ~2 s */
if (bba_read(REG_RWP_LO, r1, 2) < 0) { snprintf(m, n, "RWP read failed"); return 1; }
ir = bba_r8(REG_IR);
if (memcmp(r0, r1, 2) != 0 || (ir >= 0 && (ir & IR_RI))) {
snprintf(m, n, "RWP 0x%02X%02X -> 0x%02X%02X, RI=%d",
r0[1], r0[0], r1[1], r1[0], (ir & IR_RI) ? 1 : 0);
return 0;
}
msleep(10);
}
snprintf(m, n, "no frame in 2 s (RWP still 0x%02X%02X)", r0[1], r0[0]);
return 1;
}
/* ─────────────────────── suite 6: SPRAM / DMA ──────────────────────── */
static int t_dma(char *m, size_t n)
{
u8 buf[64];
u32 dlen;
if (bba_stream(SPRAM_BASE, buf, sizeof(buf)) < 0) {
snprintf(m, n, "stream read failed"); return 1;
}
/* 4-byte ring descriptor; its length field INCLUDES the descriptor. */
dlen = ((u32)buf[2] << 8) | buf[3];
if (dlen > 4 && dlen <= 1518) {
snprintf(m, n, "descriptor len 0x%04X = %u frame bytes, etype %02X%02X",
(unsigned)dlen, (unsigned)(dlen - 4), buf[16], buf[17]);
return 0;
}
snprintf(m, n, "descriptor len 0x%04X implausible (ring empty?)", (unsigned)dlen);
return 2; /* skip rather than fail: needs a received frame first */
}
static int t_dma_stream_len(char *m, size_t n)
{
u8 a[16], b[48];
if (bba_stream(SPRAM_BASE, a, sizeof(a)) < 0 ||
bba_stream(SPRAM_BASE, b, sizeof(b)) < 0) {
snprintf(m, n, "stream read failed"); return 1;
}
/* Reads >= 0x100 stream until CS, so the same address must give the same
* leading bytes regardless of how many we clock. */
if (memcmp(a, b, sizeof(a)) != 0) {
snprintf(m, n, "16B and 48B reads disagree - streaming is length-sensitive");
return 1;
}
snprintf(m, n, "16B and 48B reads agree; streams until CS as designed");
return 0;
}
/* ───────────────────────────── registry ────────────────────────────── */
static test_t s_identity[] = {
{"Device ID", "EXI link + enumeration: addr 0 returns 04 02 02 00", t_devid, T_PENDING, {0}},
{"HIPR", "BBA-present sentinel reads 0x01", t_hipr, T_PENDING, {0}},
{"NWAYS", "link-status sentinel reads 0x17", t_nways, T_PENDING, {0}},
};
static test_t s_regs[] = {
{"PAR0-3 w/r", "MAC low bytes are writable and read back", t_par_lo, T_PENDING, {0}},
{"PAR4-5 w/r", "MAC high bytes are writable and read back", t_par_hi, T_PENDING, {0}},
{"IMR", "interrupt mask is writable (GC init writes 0x86)", t_imr, T_PENDING, {0}},
{"IR write-1-clr","interrupt flags clear on write-1", t_ir_w1c, T_PENDING, {0}},
};
static test_t s_reset[] = {
{"NCRA reset", "RESET bit self-clears; W5100 re-inits behind it", t_ncra_reset, T_PENDING, {0}},
{"NCRA SR", "start-receive bit holds", t_ncra_sr, T_PENDING, {0}},
};
static test_t s_exi[] = {
{"200x read", "no bit errors over 200 register reads", t_repeat, T_PENDING, {0}},
{"Speed sweep", "which EXI clock indices are reliable (0..5)", t_speed_sweep, T_PENDING, {0}},
{"Gap sweep", "min header->data idle gap the device needs", t_gap_sweep, T_PENDING, {0}},
{"Raw reg dump", "8 slots of a NWAYS read - WHERE does 0x17 land?", t_raw_reg, T_PENDING, {0}},
{"Raw ID dump", "8 slots of the device-ID query - where is 0x04?", t_raw_id, T_PENDING, {0}},
{"Per-speed dump","first 2 slots at EVERY speed - does the shift move?",t_raw_sweep, T_PENDING, {0}},
};
static test_t s_eth[] = {
{"MAC readback", "PAR0-5 hold a Nintendo-OUI MAC", t_mac_read, T_PENDING, {0}},
{"TX ARP", "60-byte frame reaches the W5100 and TI fires", t_tx, T_PENDING, {0}},
{"RX frame", "a frame arrives and RWP/RI advance", t_rx, T_PENDING, {0}},
};
static test_t s_spram[] = {
{"DMA descriptor","ring descriptor at 0x100 is well-formed", t_dma, T_PENDING, {0}},
{"Stream length", "reads >= 0x100 stream until CS, length-independent",t_dma_stream_len, 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},
{"Reset/control", "NCRA reset and control bits behave", s_reset, 2},
{"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},
};
const int g_nsuites = sizeof(g_suites) / sizeof(g_suites[0]);
void tests_run_test(test_t *t)
{
int rc;
t->msg[0] = 0;
rc = t->run(t->msg, sizeof(t->msg));
t->status = (rc == 0) ? T_PASS : (rc == 2 ? T_SKIP : T_FAIL);
}
void tests_run_suite(suite_t *s)
{
int i;
for (i = 0; i < s->ntests; i++) tests_run_test(&s->tests[i]);
}
void tests_run_all(void)
{
int i;
for (i = 0; i < g_nsuites; i++) tests_run_suite(&g_suites[i]);
}
void tests_reset_status(void)
{
int i, j;
for (i = 0; i < g_nsuites; i++)
for (j = 0; j < g_suites[i].ntests; j++) {
g_suites[i].tests[j].status = T_PENDING;
g_suites[i].tests[j].msg[0] = 0;
}
}
void tests_tally(int *pass, int *fail, int *skip, int *pending)
{
int i, j;
*pass = *fail = *skip = *pending = 0;
for (i = 0; i < g_nsuites; i++)
for (j = 0; j < g_suites[i].ntests; j++)
switch (g_suites[i].tests[j].status) {
case T_PASS: (*pass)++; break;
case T_FAIL: (*fail)++; break;
case T_SKIP: (*skip)++; break;
default: (*pending)++; break;
}
}