Files
rebbarb/gc_bba_test/source/tests.c
T
Roflin b3685237b2 Support BOTH command-space forms: Swiss's 4-byte and libogc's 2-byte
Reading Swiss's own driver (cube/patches/bba/bba.c) shows the previous commit
fixed libogc and BROKE Swiss.  The two disagree, and both are real:

  Swiss   bba_cmd_out8: exi_imm_read_write(0x40<<24 | reg<<24 | val, 4)
          -> ONE 4-byte transfer [0x40|reg, 00, 00, val], and bba_cmd_in8
             returns the LOW byte, so a read response must be in BYTE 3.
  libogc2 bba_cmd_outsnosel: req = (reg<<8)|0x4000;
          EXI_ImmEx(&req, 2, WRITE); EXI_ImmEx(val, len, WRITE)
          -> THREE bytes, value in byte 2, and a read takes byte 2.
  stock libogc: identical to libogc2 (read out of libbba.a).

They share bytes 0 and 1 and differ only in how many bytes follow, so one
decoder serves both: take the LAST data byte before CS rise as the write value
(Swiss sends [00, val], libogc sends [val] -- last is val either way), and push
a read response TWICE so bytes 2 and 3 both carry it.  Swiss clocks 4 bytes and
pops both; libogc clocks 3 and leaves one, which the next transaction's header
pops drain -- a 2-byte header gives exactly one drain slot, so the leftover is
bounded at one and cannot accumulate.

Critically this means NO auto-increment of cmd_reg across the data phase: that
applied Swiss's padding 00 to the register and val to reg+1, which is precisely
how the libogc-only version broke Swiss.

T12 covers the Swiss form and asserts bytes 2 AND 3; T10 keeps the libogc form.
Both mutation-tested: re-adding the auto-increment fails T12 with 0x00 in both
slots, and pushing the response once fails T12's byte 3.  gc_bba_test gains
"cmd-space Swiss" alongside "cmd-space write", with bba_cmd_write4/read4.

Also corrects two things in CLAUDE.md now checkable against three drivers:
- NO driver writes TWD -- not Swiss, not libogc, not libogc2.  The "GC sets
  TWD before streaming" note was self-confirming: t_tx writes TWD because we
  wrote t_tx.  The counted TXDATA length is the primary path, not a fallback.
- Swiss's exi_dma_write rounds the length UP to 32 bytes (OSRoundUp32B), so
  TXDATA gets up to 31 trailing bytes past the frame and a counted length sends
  that much extra.  Real hardware must do the same since Swiss works on it;
  trailing padding past the IP total length is ignored.  A 1514-byte frame
  rounds to 1536 though, over the 1518 maximum.

Both drivers trigger TX with NCRA ST1, which the existing any-ST test accepts.

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

741 lines
28 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 ───────────────────────── */
/* Is the EXI link demonstrably healthy apart from address 0?
*
* Needed because `id_phase` is a ONE-SHOT in the FPGA, cleared by the first
* NCRA reset, and g_ncra_reset_done only tracks resets THIS RUN of the test
* app issued. That flag is software state standing in for hardware state, and
* the two come apart as soon as 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 ID tests
* then query address 0, get NCRA back -- correct behaviour -- and report a
* FAILURE for it. Reflashing the FPGA is the only thing that restores it.
*
* 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 fine and a
* wrong answer at address 0 can only be a spent id_phase. If they are wrong
* too, something real is broken and the test must still fail. */
static int iface_alive(void)
{
return bba_r8(REG_HIPR) == HIPR_PRESENT &&
bba_r8(REG_NWAYS) == NWAYS_LINKUP;
}
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; }
if (b[0]==0x04 && b[1]==0x02 && b[2]==0x02 && b[3]==0x00) {
snprintf(m, n, "%02X %02X %02X %02X (want 04 02 02 00)",
b[0], b[1], b[2], b[3]);
return 0;
}
if (iface_alive()) {
snprintf(m, n, "%02X %02X %02X %02X: id_phase spent, reflash FPGA",
b[0], b[1], b[2], b[3]);
return 2;
}
snprintf(m, n, "%02X %02X %02X %02X (want 04 02 02 00)", b[0], b[1], b[2], b[3]);
return 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;
/* Fast path for a reset this run issued. Note this flag can be STALE
* (see iface_alive): it is clear after a netbooted update even though the
* FPGA's id_phase is long gone, which is why the result is also checked
* against iface_alive() below rather than trusted to this. */
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);
if (at == 0)
return 0;
/* Same discrimination as t_devid: a spent id_phase is not a fault. */
return iface_alive() ? 2 : 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;
}
}
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)
{
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}},
};
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 sequences 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;
}
}