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 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 10:59:37 +02:00
parent f2a721cb1f
commit ccf4eb4996
8 changed files with 934 additions and 0 deletions
+29
View File
@@ -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
+71
View File
@@ -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
#---------------------------------------------------------------------------------
+67
View File
@@ -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 <nic>` 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.
+105
View File
@@ -0,0 +1,105 @@
#include "bba.h"
#include <string.h>
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;
}
+74
View File
@@ -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 <gccore.h>
/* 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
+188
View File
@@ -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 <gccore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#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;
}
+358
View File
@@ -0,0 +1,358 @@
#include "tests.h"
#include "bba.h"
#include <stdio.h>
#include <string.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_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;
}
}
+42
View File
@@ -0,0 +1,42 @@
#ifndef TESTS_H
#define TESTS_H
#include <gccore.h>
#include <stddef.h>
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