From ccf4eb4996de0825d631a5f8af65536950c02647 Mon Sep 17 00:00:00 2001 From: Dennis Brentjes Date: Sat, 3 Oct 2026 10:59:37 +0200 Subject: [PATCH] Add on-console GameCube test suite + devkitPPC in the devcontainer gc_bba_test/ builds a .dol that drives the adapter over raw EXI from the console and reports pass/fail per test on the TV, navigable with the pad. Uses libogc's EXI API against EXI0:2 (Serial Port 1) rather than its bba network driver: driving the register file directly gives each check its own verdict, so a failure points at a specific register instead of at 'networking'. Six suites - identity, registers, reset/control, EXI link integrity (including an EXI clock-index sweep), ethernet TX/RX, SPRAM/DMA streaming. Encodes the hardware lessons from bring-up so the app cannot mislead: * the device ID is a one-shot cleared by the first NCRA reset, so it is reported SKIP on a re-run rather than FAIL * TI means the frame reached the W5100, NOT the wire - the TX test says so * register reads clock exactly the requested length (over-clocking desynchronises the next transaction) * TXDATA is written one byte per transaction (eff_addr auto-increments) .devcontainer/Dockerfile gains the devkitPro gamecube-dev group. devkitPro publishes no .deb on GitHub, so this uses their installer script against apt.devkitpro.org -- which is 403 from inside the running container's egress but reachable from a host-side docker build. Rebuild the container to get it. Co-Authored-By: Claude Opus 5 --- .devcontainer/Dockerfile | 29 +++ gc_bba_test/Makefile | 71 ++++++++ gc_bba_test/README.md | 67 +++++++ gc_bba_test/source/bba.c | 105 +++++++++++ gc_bba_test/source/bba.h | 74 ++++++++ gc_bba_test/source/main.c | 188 +++++++++++++++++++ gc_bba_test/source/tests.c | 358 +++++++++++++++++++++++++++++++++++++ gc_bba_test/source/tests.h | 42 +++++ 8 files changed, 934 insertions(+) create mode 100644 gc_bba_test/Makefile create mode 100644 gc_bba_test/README.md create mode 100644 gc_bba_test/source/bba.c create mode 100644 gc_bba_test/source/bba.h create mode 100644 gc_bba_test/source/main.c create mode 100644 gc_bba_test/source/tests.c create mode 100644 gc_bba_test/source/tests.h diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b0a53dc..6b899cf 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -2,6 +2,7 @@ FROM debian:sid-slim RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ + curl \ git \ git-lfs \ sudo \ @@ -49,6 +50,34 @@ RUN pip install --no-cache-dir -r /tmp/requirements.txt # FT2232H channel-B UART directly over libusb for bring-up diagnostics. RUN pip install --no-cache-dir pyftdi pyserial +# ── GameCube homebrew toolchain (devkitPPC + libogc) ──────────────────────── +# Needed to build the on-console BBA test app in `gc_bba_test/` into a .dol. +# devkitPro does NOT publish a .deb on GitHub -- the Debian package only exists +# in their own apt repo -- so this uses their maintained installer script, which +# adds the repo, imports the key and installs dkp-pacman. `gamecube-dev` is a +# package GROUP (devkitPPC, libogc, libfat, tools incl. elf2dol). +# +# NOTE this pulls from apt.devkitpro.org / pkg.devkitpro.org. Those hosts are +# blocked (HTTP 403) from inside the running container's network egress, but a +# host-side `docker build` reaches them fine -- which is why this lives in the +# Dockerfile rather than being installed at runtime. Adds roughly 1.5-2 GB. +RUN curl -fsSL https://apt.devkitpro.org/install-devkitpro-pacman -o /tmp/install-devkitpro-pacman \ + && chmod +x /tmp/install-devkitpro-pacman \ + && yes | /tmp/install-devkitpro-pacman \ + && dkp-pacman -S --noconfirm gamecube-dev \ + && dkp-pacman -Scc --noconfirm \ + && rm -f /tmp/install-devkitpro-pacman + +ENV DEVKITPRO=/opt/devkitpro +ENV DEVKITPPC=/opt/devkitpro/devkitPPC +ENV PATH="$DEVKITPPC/bin:$DEVKITPRO/tools/bin:$PATH" + +# Same /etc/profile PATH-reset problem as the venv above: login shells drop the +# ENV PATH, and devkitPro's Makefiles resolve $(DEVKITPPC)/bin from the +# environment, so export all three for login shells too. +RUN printf 'export DEVKITPRO=/opt/devkitpro\nexport DEVKITPPC=/opt/devkitpro/devkitPPC\nPATH="$DEVKITPPC/bin:$DEVKITPRO/tools/bin:$PATH"\nexport PATH\n' \ + > /etc/profile.d/20-devkitpro.sh + RUN useradd -m -u 1000 -s /bin/bash vscode \ && echo 'vscode ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/vscode \ && chmod 0440 /etc/sudoers.d/vscode diff --git a/gc_bba_test/Makefile b/gc_bba_test/Makefile new file mode 100644 index 0000000..cd7f538 --- /dev/null +++ b/gc_bba_test/Makefile @@ -0,0 +1,71 @@ +#--------------------------------------------------------------------------------- +# re-bba-rb on-console test suite — GameCube (.dol), devkitPPC + libogc +# +# make build gc_bba_test.dol +# make clean +# +# Needs devkitPro with the `gamecube-dev` group installed (DEVKITPPC set). +# Run the .dol from Swiss (SD Gecko / SD2SP2 / USB Gecko) on a console with the +# re-bba-rb adapter in Serial Port 1. +#--------------------------------------------------------------------------------- +ifeq ($(strip $(DEVKITPPC)),) +$(error "DEVKITPPC is not set. Install devkitPro's gamecube-dev group, or rebuild the devcontainer.") +endif + +include $(DEVKITPPC)/gamecube_rules + +TARGET := $(notdir $(CURDIR)) +BUILD := build +SOURCES := source +DATA := data +INCLUDES := + +CFLAGS := -g -O2 -Wall -Wextra $(MACHDEP) $(INCLUDE) +CXXFLAGS:= $(CFLAGS) +LDFLAGS := -g $(MACHDEP) -Wl,-Map,$(notdir $@).map + +LIBS := -logc -lm +LIBDIRS := + +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- +export OUTPUT := $(CURDIR)/$(TARGET) +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CXXFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +sFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.S))) + +export LD := $(CC) +export OFILES := $(CFILES:.c=.o) $(CXXFILES:.cpp=.o) $(sFILES:.s=.o) $(SFILES:.S=.o) +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) -I$(LIBOGC_INC) +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) -L$(LIBOGC_LIB) + +.PHONY: $(BUILD) clean + +$(BUILD): + @[ -d $@ ] || mkdir -p $@ + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +clean: + @echo clean ... + @rm -fr $(BUILD) $(OUTPUT).elf $(OUTPUT).dol $(OUTPUT).dol.map + +#--------------------------------------------------------------------------------- +else + +DEPENDS := $(OFILES:.o=.d) + +$(OUTPUT).dol: $(OUTPUT).elf +$(OUTPUT).elf: $(OFILES) + +-include $(DEPENDS) + +endif +#--------------------------------------------------------------------------------- diff --git a/gc_bba_test/README.md b/gc_bba_test/README.md new file mode 100644 index 0000000..9dca3f0 --- /dev/null +++ b/gc_bba_test/README.md @@ -0,0 +1,67 @@ +# re-bba-rb — on-console test suite (GameCube) + +A `.dol` you run from Swiss that drives the adapter in Serial Port 1 and +reports pass/fail per test on the TV. + +## Build + +Needs devkitPro's `gamecube-dev` group (devkitPPC + libogc). The devcontainer +installs it — if `DEVKITPPC` is unset, **rebuild the container**, since the +toolchain is added in `.devcontainer/Dockerfile`. + +```bash +make # -> gc_bba_test.dol +``` + +Copy the `.dol` to your SD card (SD Gecko / SD2SP2) and launch it from Swiss. + +## Controls + +| Button | | +|---|---| +| D-pad up/down | move | +| A | open a suite / rerun the selected test | +| X | run the selected suite | +| START | run everything | +| Y | clear all results | +| B | back | + +## What it tests + +| Suite | Proves | +|---|---| +| Identity | device enumerates: `04 02 02 00`, HIPR, NWAYS | +| Registers | PAR0–5, IMR writable and read back; IR is write-1-clear | +| Reset/control | NCRA RESET self-clears, SR holds | +| EXI integrity | 200 reads with no bit errors; which EXI clock indices work | +| Ethernet | MAC readback, 60-byte ARP transmit (TI), frame receive (RWP/RI) | +| SPRAM/DMA | ring descriptor is well-formed; reads ≥ 0x100 stream until CS | + +## Things that will bite you + +**The device ID is a ONE-SHOT.** Address 0 returns `04 02 02 00` only until +the first NCRA reset clears `id_phase` in the gateware; after that it reads +back NCRA. That is correct — it matches the GC's own init order — but it means +the Identity test can only pass once per *console power cycle*. Run it before +the Reset suite. On a re-run the app reports SKIP with an explanation rather +than a misleading failure. + +**TI is not proof of transmission.** It only means `TXFrameDrain` handed the +frame to the W5100. A frame can set TI and never reach the wire. The TX test +says so; to confirm the wire, capture on a PC or watch the RX test pick up a +reply. + +**The RX test needs traffic.** It waits ~2 s for any frame. On an isolated +link with nothing talking it will fail legitimately — give the PC an address on +the same subnet (the ARP the TX test sends is aimed at `10.0.0.50`, so +`sudo ip addr add 10.0.0.50/24 dev ` makes the PC answer it and both tests +pass together). + +**Never over-clock a register read.** Asking for N bytes and clocking more +leaves stale prefetched bytes queued and desynchronises the *next* transaction +— a deterministic every-Nth-read corruption that looks like a signal-integrity +fault. `bba_read()` clocks exactly `len`; don't "round up" for convenience. + +**TXDATA takes one byte per transaction.** The register file's effective +address auto-increments per data byte, so a 4-byte write to 0x48 would spill +into 0x49–0x4B. `bba_tx_bytes()` does it correctly. diff --git a/gc_bba_test/source/bba.c b/gc_bba_test/source/bba.c new file mode 100644 index 0000000..de518ba --- /dev/null +++ b/gc_bba_test/source/bba.c @@ -0,0 +1,105 @@ +#include "bba.h" +#include + +u32 bba_speed = EXI_SPEED32MHZ; /* nominal name; ~27 MHz in reality */ + +/* Build the 2-byte EXI transaction header. + * byte0: [7]=write [6:0]=addr[12:6] + * byte1: [7:2]=addr[5:0] [1:0]=len-1 + * Returned packed big-endian in a u16 so EXI_Imm(...,2,...) clocks byte0 first + * (the GameCube is big-endian, so the high byte goes out first). */ +static u16 hdr(u16 addr, u32 len, int write) +{ + u8 b0 = (write ? 0x80 : 0x00) | ((addr >> 6) & 0x7F); + u8 b1 = (u8)(((addr & 0x3F) << 2) | ((len - 1) & 0x03)); + return ((u16)b0 << 8) | b1; +} + +static int begin(void) +{ + if (EXI_Lock(BBA_EXI_CHAN, BBA_EXI_DEV, NULL) == 0) return -1; + if (EXI_Select(BBA_EXI_CHAN, BBA_EXI_DEV, bba_speed) == 0) { + EXI_Unlock(BBA_EXI_CHAN); + return -2; + } + return 0; +} + +static void end(void) +{ + EXI_Deselect(BBA_EXI_CHAN); + EXI_Unlock(BBA_EXI_CHAN); +} + +static int put_hdr(u16 addr, u32 len, int write) +{ + u16 h = hdr(addr, len, write); + if (EXI_Imm(BBA_EXI_CHAN, &h, 2, EXI_WRITE, NULL) == 0) return -3; + if (EXI_Sync(BBA_EXI_CHAN) == 0) return -4; + return 0; +} + +int bba_read(u16 addr, void *buf, u32 len) +{ + int rc; + if (len < 1 || len > 4) return -10; + if ((rc = begin()) < 0) return rc; + if ((rc = put_hdr(addr, len, 0)) < 0) { end(); return rc; } + /* The real GC driver pauses the clock here while the device decodes the + * header and prefetches the response into its TX FIFO. Separate EXI_Imm + * calls give us that gap for free. */ + memset(buf, 0, len); + if (EXI_ImmEx(BBA_EXI_CHAN, buf, len, EXI_READ) == 0) { end(); return -5; } + end(); + return 0; +} + +int bba_write(u16 addr, const void *buf, u32 len) +{ + int rc; + u8 tmp[4]; + if (len < 1 || len > 4) return -10; + memcpy(tmp, buf, len); + if ((rc = begin()) < 0) return rc; + if ((rc = put_hdr(addr, len, 1)) < 0) { end(); return rc; } + if (EXI_ImmEx(BBA_EXI_CHAN, tmp, len, EXI_WRITE) == 0) { end(); return -6; } + end(); + return 0; +} + +int bba_r8(u16 addr) +{ + u8 v; + int rc = bba_read(addr, &v, 1); + return rc < 0 ? rc : (int)v; +} + +int bba_w8(u16 addr, u8 val) +{ + return bba_write(addr, &val, 1); +} + +int bba_stream(u16 addr, void *buf, u32 len) +{ + int rc; + if (len < 1) return -10; + if ((rc = begin()) < 0) return rc; + /* Length field is capped at 2 bits; the device ignores it for addresses + * >= 0x0100 and streams until CS rises, so ask for 4 and clock as many + * bytes as we actually want. */ + if ((rc = put_hdr(addr, 4, 0)) < 0) { end(); return rc; } + memset(buf, 0, len); + if (EXI_ImmEx(BBA_EXI_CHAN, buf, len, EXI_READ) == 0) { end(); return -5; } + end(); + return 0; +} + +int bba_tx_bytes(const u8 *buf, u32 len) +{ + u32 i; + for (i = 0; i < len; i++) { + int rc = bba_w8(REG_TXDATA, buf[i]); + if (rc < 0) return rc; + } + return 0; +} diff --git a/gc_bba_test/source/bba.h b/gc_bba_test/source/bba.h new file mode 100644 index 0000000..fdd310a --- /dev/null +++ b/gc_bba_test/source/bba.h @@ -0,0 +1,74 @@ +/* bba.h — raw EXI transport to the re-bba-rb adapter in Serial Port 1. + * + * Deliberately NOT libogc's bba network driver: this talks to the register + * file directly so every check gets its own pass/fail and a failure points at + * a specific register rather than at "networking". + */ +#ifndef BBA_H +#define BBA_H + +#include + +/* Serial Port 1 == EXI channel 0, device 2. */ +#define BBA_EXI_CHAN EXI_CHANNEL_0 +#define BBA_EXI_DEV EXI_DEVICE_2 + +/* Register map (see CLAUDE.md "Key Registers"). */ +#define REG_NCRA 0x00 +#define REG_IMR 0x08 +#define REG_IR 0x09 +#define REG_RWP_LO 0x16 +#define REG_RWP_HI 0x17 +#define REG_RRP_LO 0x18 +#define REG_PAR0 0x20 +#define REG_PAR4 0x24 +#define REG_NWAYS 0x31 +#define REG_TWD_LO 0x34 +#define REG_TWD_HI 0x35 +#define REG_HIPR 0x3A +#define REG_TXDATA 0x48 +#define SPRAM_BASE 0x0100 + +/* NCRA bits */ +#define NCRA_RESET 0x01 +#define NCRA_ST0 0x02 +#define NCRA_SR 0x08 + +/* IR bits */ +#define IR_RI 0x02 +#define IR_TI 0x04 + +/* Expected constants */ +#define NWAYS_LINKUP 0x17 +#define HIPR_PRESENT 0x01 + +/* EXI clock index, EXI_SPEED1MHZ(0) .. EXI_SPEED32MHZ(5). The nominal 32 MHz + * index is really ~27 MHz on hardware (see CLAUDE.md); the capture domain + * oversamples it 2x at 54 MHz. Settable so the suite can sweep it. */ +extern u32 bba_speed; + +/* All return 0 on success, negative on an EXI-layer failure. */ + +/* Immediate read of `len` (1..4) bytes from `addr`. + * Clocks EXACTLY `len` bytes: over-clocking a register read leaves stale + * prefetched bytes queued in the gateware and desynchronises the NEXT + * transaction (hardware-confirmed 2026-09-30). Never pad this. */ +int bba_read(u16 addr, void *buf, u32 len); + +/* Immediate write of `len` (1..4) bytes to `addr`. */ +int bba_write(u16 addr, const void *buf, u32 len); + +/* Convenience single-byte helpers. bba_r8 returns the byte, or <0 on error. */ +int bba_r8(u16 addr); +int bba_w8(u16 addr, u8 val); + +/* SPRAM / DMA-style read: any address >= 0x0100 streams until CS deasserts, + * so `len` may exceed 4 and the header's 2-bit length field is ignored. */ +int bba_stream(u16 addr, void *buf, u32 len); + +/* Stream `len` bytes into the TXDATA port. One byte per EXI transaction: + * the register file's effective address auto-increments per data byte, so a + * multi-byte write to 0x48 would spill into 0x49+ instead of the FIFO. */ +int bba_tx_bytes(const u8 *buf, u32 len); + +#endif diff --git a/gc_bba_test/source/main.c b/gc_bba_test/source/main.c new file mode 100644 index 0000000..3e437c6 --- /dev/null +++ b/gc_bba_test/source/main.c @@ -0,0 +1,188 @@ +/* re-bba-rb on-console test suite — GameCube + * + * Put the adapter in Serial Port 1, run this from Swiss. + * + * D-pad up/down move + * A open suite / rerun the selected test + * X run the selected suite + * START run everything + * Y clear all results + * B back + */ +#include +#include +#include +#include +#include +#include "tests.h" +#include "bba.h" + +static void *xfb = NULL; +static GXRModeObj *rmode = NULL; + +#define C_RESET "\x1b[37;0m" +#define C_HEAD "\x1b[36;1m" +#define C_SEL "\x1b[30;47m" /* black on white */ +#define C_PASS "\x1b[32;1m" +#define C_FAIL "\x1b[31;1m" +#define C_SKIP "\x1b[33;1m" +#define C_DIM "\x1b[37;0m" + +static const char *st_tag(tstatus_t s) +{ + switch (s) { + case T_PASS: return C_PASS "PASS" C_RESET; + case T_FAIL: return C_FAIL "FAIL" C_RESET; + case T_SKIP: return C_SKIP "SKIP" C_RESET; + default: return C_DIM " -- " C_RESET; + } +} + +static void suite_tally(const suite_t *s, int *p, int *f, int *k) +{ + int i; *p = *f = *k = 0; + for (i = 0; i < s->ntests; i++) + switch (s->tests[i].status) { + case T_PASS: (*p)++; break; + case T_FAIL: (*f)++; break; + case T_SKIP: (*k)++; break; + default: break; + } +} + +static void cls(void) { printf("\x1b[2J\x1b[1;1H"); } + +static void banner(void) +{ + printf(C_HEAD " re-bba-rb -- on-console test suite" C_RESET "\n"); + printf(C_DIM " GameCube Serial Port 1 (EXI0:2), clock index %u\n" C_RESET, + (unsigned)bba_speed); + printf(" ------------------------------------------------------------\n"); +} + +static void draw_main(int sel, const char *status) +{ + int i, p, f, k, tp, tf, tk, tpend; + cls(); + banner(); + for (i = 0; i < g_nsuites; i++) { + suite_tally(&g_suites[i], &p, &f, &k); + printf("%s %-14s" C_RESET " %s%2d" C_RESET "/%s%2d" C_RESET "/%s%2d" C_RESET + " %s%s\n", + i == sel ? C_SEL : " ", g_suites[i].name, + C_PASS, p, C_FAIL, f, C_SKIP, k, + C_DIM, g_suites[i].what); + } + tests_tally(&tp, &tf, &tk, &tpend); + printf(" ------------------------------------------------------------\n"); + printf(" total " C_PASS "%d passed" C_RESET " " C_FAIL "%d failed" C_RESET + " " C_SKIP "%d skipped" C_RESET " %d not run\n", tp, tf, tk, tpend); + if (tf) { + printf(C_FAIL " failing:" C_RESET); + for (i = 0; i < g_nsuites; i++) { + int j; + for (j = 0; j < g_suites[i].ntests; j++) + if (g_suites[i].tests[j].status == T_FAIL) + printf(" %s/%s", g_suites[i].name, g_suites[i].tests[j].name); + } + printf("\n"); + } + printf("\n" C_DIM " A open X run suite START run all Y clear\n" C_RESET); + if (status && *status) printf("\n %s\n", status); +} + +static void draw_suite(int si, int sel, const char *status) +{ + suite_t *s = &g_suites[si]; + int i, p, f, k; + cls(); + banner(); + suite_tally(s, &p, &f, &k); + printf(C_HEAD " %s" C_RESET " -- %s\n", s->name, s->what); + printf(" " C_PASS "%d passed" C_RESET " " C_FAIL "%d failed" C_RESET + " " C_SKIP "%d skipped" C_RESET " of %d\n\n", p, f, k, s->ntests); + for (i = 0; i < s->ntests; i++) { + printf("%s %s %-16s" C_RESET "\n", + i == sel ? C_SEL : " ", st_tag(s->tests[i].status), s->tests[i].name); + printf(C_DIM " %s\n" C_RESET, s->tests[i].what); + if (s->tests[i].msg[0]) + printf(" -> %s\n", s->tests[i].msg); + } + printf("\n" C_DIM " A rerun this test X run whole suite B back\n" C_RESET); + if (status && *status) printf("\n %s\n", status); +} + +int main(void) +{ + int screen = 0; /* 0 = main, 1 = suite detail */ + int sel = 0, ssel = 0, cur = 0; + char status[80] = ""; + + VIDEO_Init(); + PAD_Init(); + rmode = VIDEO_GetPreferredMode(NULL); + xfb = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode)); + console_init(xfb, 20, 20, rmode->fbWidth, rmode->xfbHeight, + rmode->fbWidth * VI_DISPLAY_PIX_SZ); + VIDEO_Configure(rmode); + VIDEO_SetNextFramebuffer(xfb); + VIDEO_SetBlack(FALSE); + VIDEO_Flush(); + VIDEO_WaitVSync(); + if (rmode->viTVMode & VI_NON_INTERLACE) VIDEO_WaitVSync(); + + snprintf(status, sizeof(status), "ready - press START to run everything"); + draw_main(sel, status); + + for (;;) { + u16 down; + VIDEO_WaitVSync(); + PAD_ScanPads(); + down = PAD_ButtonsDown(0); + if (!down) continue; + + if (down & PAD_BUTTON_START) { + snprintf(status, sizeof(status), "running all suites..."); + if (screen) draw_suite(cur, ssel, status); else draw_main(sel, status); + tests_run_all(); + snprintf(status, sizeof(status), "done"); + screen = 0; + draw_main(sel, status); + continue; + } + + if (screen == 0) { + int n = g_nsuites; + if (down & PAD_BUTTON_DOWN) sel = (sel + 1) % n; + if (down & PAD_BUTTON_UP) sel = (sel + n - 1) % n; + if (down & PAD_BUTTON_A) { screen = 1; cur = sel; ssel = 0; status[0] = 0; + draw_suite(cur, ssel, status); continue; } + if (down & PAD_BUTTON_X) { snprintf(status, sizeof(status), "running %s...", + g_suites[sel].name); + draw_main(sel, status); + tests_run_suite(&g_suites[sel]); + snprintf(status, sizeof(status), "%s done", + g_suites[sel].name); } + if (down & PAD_BUTTON_Y) { tests_reset_status(); + snprintf(status, sizeof(status), "results cleared"); } + draw_main(sel, status); + } else { + suite_t *s = &g_suites[cur]; + if (down & PAD_BUTTON_DOWN) ssel = (ssel + 1) % s->ntests; + if (down & PAD_BUTTON_UP) ssel = (ssel + s->ntests - 1) % s->ntests; + if (down & PAD_BUTTON_B) { screen = 0; status[0] = 0; + draw_main(sel, status); continue; } + if (down & PAD_BUTTON_A) { snprintf(status, sizeof(status), "running %s...", + s->tests[ssel].name); + draw_suite(cur, ssel, status); + tests_run_test(&s->tests[ssel]); + status[0] = 0; } + if (down & PAD_BUTTON_X) { snprintf(status, sizeof(status), "running %s...", s->name); + draw_suite(cur, ssel, status); + tests_run_suite(s); + status[0] = 0; } + draw_suite(cur, ssel, status); + } + } + return 0; +} diff --git a/gc_bba_test/source/tests.c b/gc_bba_test/source/tests.c new file mode 100644 index 0000000..00a9a53 --- /dev/null +++ b/gc_bba_test/source/tests.c @@ -0,0 +1,358 @@ +#include "tests.h" +#include "bba.h" +#include +#include +#include + +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_read(0x0000, b, 4)) < 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; + 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; + } + bba_speed = saved; + snprintf(m, n, "idx0..5 ok=[%s]; highest good = %u", buf, (unsigned)best); + return best >= EXI_SPEED32MHZ ? 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}}, +}; +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, 2}, + {"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; + } +} diff --git a/gc_bba_test/source/tests.h b/gc_bba_test/source/tests.h new file mode 100644 index 0000000..1dc1f2c --- /dev/null +++ b/gc_bba_test/source/tests.h @@ -0,0 +1,42 @@ +#ifndef TESTS_H +#define TESTS_H + +#include +#include + +typedef enum { T_PENDING = 0, T_PASS, T_FAIL, T_SKIP } tstatus_t; + +#define TMSG_LEN 72 + +typedef struct { + const char *name; + const char *what; /* one line: what this proves */ + int (*run)(char *msg, size_t n); /* 0 = pass, 1 = fail, 2 = skip */ + tstatus_t status; + char msg[TMSG_LEN]; +} test_t; + +typedef struct { + const char *name; + const char *what; + test_t *tests; + int ntests; +} suite_t; + +extern suite_t g_suites[]; +extern const int g_nsuites; + +/* Set once an NCRA reset has been issued this session. The device ID is a + * ONE-SHOT: reads of address 0 return it only until the first NCRA reset + * clears id_phase in the gateware, after which address 0 reads back NCRA. + * That is correct behaviour (it matches the GC's own init order), but it + * means the ID test can only pass once per console power cycle. */ +extern int g_ncra_reset_done; + +void tests_run_test(test_t *t); +void tests_run_suite(suite_t *s); +void tests_run_all(void); +void tests_reset_status(void); +void tests_tally(int *pass, int *fail, int *skip, int *pending); + +#endif