#include "tests.h" #include "bba.h" #include #include #include #include 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_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}}, {"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}, {"libogc init", "the real driver's init sequence and shapes", s_init, 4}, }; 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; } }