6f151cd1ff
Reading one byte and getting 0x00 cannot distinguish 'decode is wrong' from 'reply is late' -- a one-byte window has nowhere for a late answer to show up. These two probes clock EIGHT data bytes and print every slot, so the position of 0x17 (NWAYS) or 0x04 (device ID) gives the latency directly, the same way bit-shift analysis turned 00 00 00 10 into 'correct, 22 bits late'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
428 lines
15 KiB
C
428 lines
15 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: one-shot, needs a console power cycle");
|
|
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_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; }
|
|
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}},
|
|
};
|
|
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, 5},
|
|
{"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;
|
|
}
|
|
}
|