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:
@@ -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
|
||||
#---------------------------------------------------------------------------------
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user