Files
Roflin 90a583dd46 Remove the device-ID one-shot: it made Swiss see no adapter at all
id_phase was set at reset and cleared permanently by the GC's first NCRA reset,
after which a command-space read of register 0 returned 0x00000000.  Swiss
calls EXI_GetID from bba_exists() AND again from bba_requires_init(), long
after init, and matches the result against EXI_MX98730EC_ID -- so with the
one-shot spent no case matched and Swiss reported NO ADAPTER, not a broken one.
Running gc_bba_test was enough to spend it, so the adapter would "disappear"
after any test session.

The justification in the comment was wrong.  It claimed the fast path would
otherwise hijack address 0 so the GC could never read NCRA back -- but NCRA is
REGISTER space (0x80/0xC0 header) while the fast path requires byte 0 == 0x00,
i.e. COMMAND space.  They were never ambiguous, and no driver reads command
register 0 for anything else (libogc2 reads 0x01/0x03/0x0B/0x0F, Swiss 0x03).
Real hardware answers EXI_GetID whenever asked; that is how EXI enumerates
devices.

id_arm is now tied high, and the id_phase output, the FFSynchronizer that
crossed it into the pin domain, and the BBATop wiring are all gone.  The port
stays only so the PHASE_METER build can disarm the fast path, which would
otherwise send ID bytes instead of its constant 0x80.

bba_top T1b reads the device ID AFTER an NCRA reset, which T1 (before any
reset) structurally cannot catch.  Mutation-tested: disarming the fast path
returns 00 00 00 00 for both, exactly what Swiss saw.

gc_bba_test: the ID tests no longer skip.  A wrong ID is a FAILURE again, with
no iface_alive() discrimination and no ordering requirement between suites --
the ID is readable at any point in a run.  g_ncra_reset_done and iface_alive()
are removed outright rather than left as a dead flag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 14:33:12 +02:00

710 lines
27 KiB
C

#include "tests.h"
#include "bba.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <ogc/lwp_watchdog.h>
static void msleep(u32 ms) { usleep(ms * 1000); }
/* ───────────────────────── suite 1: identity ───────────────────────── */
/* The device ID is answerable AT ANY TIME, so these tests no longer skip.
*
* It used to be a one-shot in the FPGA -- `id_phase`, cleared by the GC's
* first NCRA reset -- so these tests had to guess whether a wrong answer meant
* a spent one-shot or a real fault, and they ran in a fixed order to get a
* reading at all. That was a deviation from real hardware, which answers
* EXI_GetID whenever the GC asks (it is how EXI enumerates devices). The
* deviation broke Swiss outright: it calls EXI_GetID from bba_exists() and
* again from bba_requires_init() long after init, and with the one-shot spent
* got 0x00000000, i.e. NO ADAPTER AT ALL.
*
* With that removed, a wrong ID is simply a FAILURE again -- no skip, no
* iface_alive() discrimination, and no ordering requirement between suites. */
static int t_devid(char *m, size_t n)
{
u8 b[4];
int rc;
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; }
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;
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; }
/* Catch RRP up to RWP before waiting, i.e. declare everything already in
* the ring consumed -- which is what a real driver does once it has read
* the frames out, and what netboot does at start-up.
*
* ⚠️ Without this the test fails on a HEALTHY adapter. This app never
* writes RRP, so ambient broadcast traffic fills the 15-page ring, `free`
* falls below _MAX_FRAME_PAGES (6) and RBFI then drops EVERY frame --
* correctly, to avoid lapping. RWP stops advancing permanently and this
* test reports "no frame in 2 s (RWP still 0x000F)" for the rest of the
* session. Page 15 is the giveaway: free = (1 + 15 - 15) mod 15 = 1.
*
* Freeing the ring here also leaves exactly the newly received frame
* unconsumed at RRP, which is where the SPRAM/DMA suite reads. */
if (bba_w8(REG_RRP_LO, r0[0]) < 0) {
snprintf(m, n, "RRP write 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 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_cmd_write_swiss(char *m, size_t n)
{
int rc, rb;
/* Swiss sends ONE 4-byte transfer with the value as byte 3 and reads the
* response from byte 3; libogc (and libogc2) send a 2-byte header plus a
* data phase, value in byte 2. Both are real drivers. The device takes
* the LAST data byte as the value and pushes a read response twice, so
* bytes 2 and 3 both carry it -- this test is the Swiss half. */
if ((rc = bba_cmd_write4(0x02, 0x5A)) < 0) {
snprintf(m, n, "Swiss cmd write failed: EXI error %d", rc); return 1;
}
if ((rb = bba_cmd_read4(0x02)) < 0) {
snprintf(m, n, "Swiss cmd read failed: EXI error %d", rb); return 1;
}
if (rb != 0x5A) {
snprintf(m, n, "Swiss 4-byte form: wrote 0x5A, byte 3 = 0x%02X", rb);
bba_cmd_write4(0x02, 0x00);
return 1;
}
bba_cmd_write4(0x02, 0x00);
snprintf(m, n, "4-byte form ok (byte 3 = 0x%02X)", rb);
return 0;
}
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_tx_no_twd(char *m, size_t n)
{
u8 f[60];
u32 len = build_arp(f);
int i, ir = 0;
/* libogc's __bba_link_tx writes NO TWD at all: read NWAYS, one
* register-space write of the whole frame to 0x0048, read NCRA, write
* NCRA = 0x04. So the device must count the bytes it was handed. The
* NCRA ST fallback used to queue the TWD value, which a driver that
* skips TWD never sets -- the frame went out with a stale or zero
* length. Deliberately does NOT write TWD_LO/TWD_HI. */
bba_w8(REG_IR, 0xFF);
if (bba_tx_bytes(f, len) < 0) {
snprintf(m, n, "TXDATA stream failed"); return 1;
}
bba_w8(REG_NCRA, NCRA_SR | NCRA_ST0);
for (i = 0; i < 60; i++) {
ir = bba_r8(REG_IR);
if (ir >= 0 && (ir & IR_TI)) {
snprintf(m, n, "%u B sent with NO TWD, TI after %d poll(s)",
(unsigned)len, i);
return 0;
}
msleep(10);
}
snprintf(m, n, "TI never set (IR=0x%02X) without TWD", ir);
return 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;
}
}
/* 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)
{
u8 buf[64], rp[2], wp[2];
u32 dlen, page, addr, desc, nextp;
if (bba_read(REG_RRP_LO, rp, 2) < 0) { snprintf(m, n, "RRP read failed"); return 1; }
if (bba_read(REG_RWP_LO, wp, 2) < 0) { snprintf(m, n, "RWP read failed"); return 1; }
page = ((u32)rp[1] << 8) | rp[0];
/* RWP/RRP are PAGE pointers into the 15-page ring, so the next
* unconsumed frame's descriptor is at RRP*256 -- NOT at SPRAM_BASE.
* Reading 0x100 unconditionally only happens to work while nothing has
* been consumed and the ring has not wrapped, which is why this test
* reported "ring empty?" on a board that had just passed the RX test. */
if (rp[0] == wp[0] && rp[1] == wp[1]) {
snprintf(m, n, "ring empty: RRP==RWP==%u (send it a frame first)",
(unsigned)page);
return 2;
}
if (page < 1 || page > 15) {
snprintf(m, n, "RRP page %u outside ring pages 1-15", (unsigned)page);
return 1;
}
addr = page << 8;
if (bba_stream((u16)addr, buf, sizeof(buf)) < 0) {
snprintf(m, n, "stream read at 0x%04X failed", (unsigned)addr);
return 1;
}
/* The 4-byte ring descriptor is a PACKED 32-BIT LITTLE-ENDIAN WORD, the
* format libogc's own driver reads:
* bits 0-11 next_packet_ptr (page), 12-23 packet_len (INCLUDING these
* 4 bytes), 24-31 status (0x80 and 0x04 are errors).
* Decoded here exactly as bba_interrupt does. */
desc = (u32)buf[0] | ((u32)buf[1] << 8) |
((u32)buf[2] << 16) | ((u32)buf[3] << 24);
dlen = (desc >> 12) & 0xFFF;
nextp = desc & 0xFFF;
if (((desc >> 24) & 0x84) == 0 && dlen > 4 && dlen <= 1518 + 4) {
snprintf(m, n, "p%u len %u B, next p%u, etype %02X%02X",
(unsigned)page, (unsigned)(dlen - 4), (unsigned)nextp,
buf[16], buf[17]);
return 0;
}
/* Diagnostic: the length alone does not say WHERE the frames are. Walk
* all 15 ring pages and classify each descriptor, so one run shows
* whether the ring is empty, written at a different page than RRP says,
* or holding genuinely malformed descriptors:
* '-' all-zero (never written) 'o' plausible frame length
* 'x' non-zero but implausible '?' read failed
* Plus the raw 4 descriptor bytes at the RRP page. */
{
char map[16];
u32 pg;
u8 d[4];
for (pg = 1; pg <= 15; pg++) {
u32 dl;
if (bba_stream((u16)(pg << 8), d, 4) < 0) {
map[pg - 1] = '?';
continue;
}
u32 w = (u32)d[0] | ((u32)d[1] << 8) |
((u32)d[2] << 16) | ((u32)d[3] << 24);
dl = (w >> 12) & 0xFFF;
if (w == 0) map[pg - 1] = '-';
else if (((w >> 24) & 0x84) == 0 &&
dl > 4 && dl <= 1518 + 4) map[pg - 1] = 'o';
else map[pg - 1] = 'x';
}
map[15] = 0;
snprintf(m, n, "p%u len 0x%04X raw %02X %02X %02X %02X RWP=%u map %s",
(unsigned)page, (unsigned)dlen,
buf[0], buf[1], buf[2], buf[3],
(unsigned)(((u32)wp[1] << 8) | wp[0]), map);
}
return 1;
}
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}},
};
static test_t s_init[] = {
{"cmd-space write","2-byte header + data phase, as libogc sends it", t_cmd_write, T_PENDING, {0}},
{"cmd-space Swiss","4-byte transfer, value in byte 3, as Swiss sends", t_cmd_write_swiss, 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}},
{"TX without TWD","libogc never writes TWD; length must be counted", t_tx_no_twd, T_PENDING, {0}},
};
/* ⚠️ `what` is at most 48 characters. The menu row in main.c is
* " " + name(%-14s) + " " + "pp/ff/kk" + " " + what
* = 28 columns of prefix, and the console is 76 wide (console_init starts at
* x=20 on a 640-pixel framebuffer, 8 pixels per character). At 49 the last
* character wraps. Keep these around 41, the longest of the originals. */
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},
{"driver shapes", "Swiss + libogc init and transfer shapes", s_init, 5},
};
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;
}
}