1a1557ff54
Getting back to Swiss previously meant power-cycling the console. R boots /boot.dol from the SD card directly. It reuses netboot_boot_file(), so there is ONE DOL launcher and one set of safety checks (MEM1 entry point, section overlap against the reserved staging region, libfat unmounted before the jump) rather than a second copy to keep in step. Confirmed rather than one-touch, and main-menu only: R is an easy trigger to catch by accident, test results are not saved anywhere, and losing a full run to a stray press is worse than one extra button press. Inside a suite R would be easy to hit while navigating, so it is ignored there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
238 lines
10 KiB
Markdown
238 lines
10 KiB
Markdown
# 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.
|
||
|
||
## Build status
|
||
|
||
Builds clean (no warnings) with devkitPPC 16.1.0 / GCC 16 in the devcontainer:
|
||
`gc_bba_test.dol`, ~172 KB, entry `0x80003100`.
|
||
|
||
`.gitignore` excludes the build products — run `make` to produce the `.dol`.
|
||
|
||
## Boot Swiss (R button)
|
||
|
||
**R** on the main menu boots `/boot.dol` from the SD card and leaves the test
|
||
app, so you can get back to Swiss without power-cycling the console.
|
||
|
||
It asks for confirmation (A to go, B to cancel). R is an easy trigger to catch
|
||
by accident, test results are not saved anywhere, and losing a full run to a
|
||
stray press is worse than one extra button. It is also main-menu only — inside
|
||
a suite, R would be easy to hit while navigating.
|
||
|
||
It uses the same `netboot_boot_file()` path as the post-update boot, so there
|
||
is one DOL launcher and one set of safety checks rather than two.
|
||
|
||
## Self-update over the BBA (Z button)
|
||
|
||
Press **Z**, then on the host:
|
||
|
||
```bash
|
||
sudo ./scripts/send-dol.py <iface> gc_bba_test/gc_bba_test.dol
|
||
```
|
||
|
||
The flow is deliberately **receive → temp file → verify → rotate → boot Swiss**:
|
||
|
||
1. The new build arrives over the adapter into a staging buffer.
|
||
2. It is written to `<name>.tmp` and **read back and compared**.
|
||
3. Only then is it rotated in: the existing build becomes
|
||
**`<name>.prev.dol`** and the temp file becomes `<name>.dol`
|
||
(default `/gc_bba_test.dol`, override with `--dest`).
|
||
4. The app then boots **`/boot.dol`** — your known-good Swiss.
|
||
|
||
**Ordering is the point.** If the transfer, the write or the verify fails, the
|
||
build you are currently running is still on the card untouched, so you can
|
||
just try again. And once an update succeeds, the previous build is kept as
|
||
`<name>.prev.dol` — so if the *new* one turns out to be broken, boot the
|
||
previous one from Swiss and transfer again from there. One generation back is
|
||
enough to never get stranded needing an SD card reader.
|
||
|
||
The temp file deliberately has **no `.dol` extension**: Swiss lists `.dol`
|
||
files, and a half-written image Swiss offers to boot is exactly what this
|
||
avoids. The backup keeps `.dol` precisely because booting it is the point.
|
||
|
||
**The received image is never launched.** The only thing this ever jumps into
|
||
is Swiss, which was already known good. So a broken test build costs nothing:
|
||
Swiss comes up, and you load whatever you like from there, including the
|
||
previous build. That is what makes the update recoverable without a power
|
||
cycle or digging the SD card out.
|
||
|
||
If the SD write or the verify fails, nothing is launched at all — you stay in
|
||
the menu with a working app.
|
||
|
||
`sudo` is needed for `CAP_NET_RAW`. The GC's MAC is discovered from its first
|
||
ACK, so neither side needs an address configured.
|
||
|
||
### Why it is not IP
|
||
|
||
Frames carry a private ethertype (`0x88B5`, IEEE 802 local experimental 1) and
|
||
a 12-byte header: no ARP, no IP stack, no checksums, and no libogc network
|
||
driver competing for the EXI channel. It reuses exactly the two paths the test
|
||
suite already proves on hardware — `bba_tx_bytes()` for TX and the SPRAM ring
|
||
at RRP for RX — so each update doubles as a few hundred round trips of soak
|
||
testing through that path.
|
||
|
||
### ⚠️ Quiet the link first — the ring has no overflow protection
|
||
|
||
```bash
|
||
sudo ./scripts/gc-testlink.sh up <iface> # static IP, no DHCP, NM off
|
||
```
|
||
|
||
Nothing in the gateware consumes RRP, so `RXFrameAssembler` keeps writing and
|
||
**laps the reader** — the real BBA has RBFI for this and this design does not.
|
||
The poll loop costs ~5 EXI transactions per frame (~200 µs) while a 100 Mbit
|
||
link delivers a 342-byte frame every ~30 µs, and the ring is only 15 pages
|
||
(~3–11 frames). Under continuous traffic pages are overwritten before they are
|
||
read, and the "descriptor" you then parse is really frame data from the frame
|
||
that replaced it.
|
||
|
||
That is where a descriptor of `00 00 00 01` comes from. It is **not** a dropped
|
||
write — it is someone else's bytes.
|
||
|
||
The biggest offender is usually the host's own DHCP client broadcasting on that
|
||
interface (a 342-byte broadcast). `gc-testlink.sh up` sets a static address and
|
||
takes the device out of NetworkManager, which stops it. Disabling IPv6 and mDNS
|
||
on that interface helps further.
|
||
|
||
`netboot_receive()` detects being lapped (writer more than two thirds of the
|
||
ring ahead), resyncs and continues rather than parsing untrustworthy bytes, and
|
||
reports the count on timeout — but detection is not prevention. A busy link
|
||
will still cost retransmissions.
|
||
|
||
### Throughput
|
||
|
||
The transfer is **round-trip bound**, not bandwidth bound: stop-and-wait means
|
||
one block per round trip, so block size sets the speed.
|
||
|
||
- `--block` defaults to **1024** (292 blocks for a ~298 KB image). 256 was the
|
||
conservative bring-up value; drop back to it if large frames misbehave.
|
||
- `--timeout` defaults to **0.1 s**. The round trip is a few ms on a LAN, so
|
||
the original 0.5 s made every lost ACK cost half a second.
|
||
- The GC polls for TI without sleeping. A `usleep(500) × 20` loop cost up to
|
||
10 ms per ACK even when the transmit succeeded immediately — on every block.
|
||
|
||
A missing ACK is not fatal: the host retransmits, and the GC re-ACKs a
|
||
duplicate block rather than ignoring it, so a lost ACK recovers in one
|
||
timeout.
|
||
|
||
**The GC stops with its summary before booting Swiss** — press a button to
|
||
continue. Those counters are the only place the *reason* for a retry shows up:
|
||
|
||
```
|
||
desc@+0=N +1=M bad=N lapped=N txto=N
|
||
```
|
||
|
||
- `txto` — ACKs where TI never set, i.e. the frame never reached the W5100.
|
||
High `txto` with many host-side retries means the TX path is the problem.
|
||
- `txto` near zero with many retries means ACKs were sent but lost on the wire
|
||
or at the host.
|
||
- `desc@+1` — frames whose ring descriptor landed at page offset +1 instead of
|
||
+0. Still unexplained; the reader handles both.
|
||
|
||
Measured 2026-10-09: 299908 bytes in 293 blocks took 15.7 s with roughly a
|
||
third of blocks retried — the retries, not the data, dominate the time.
|
||
|
||
### Stop-and-wait is load-bearing
|
||
|
||
The gateware does not consume RRP, so `RXFrameAssembler` can overwrite ring
|
||
pages the GC has not read yet — there is no overflow protection. One block in
|
||
flight means the ring never holds more than one unread frame. Do **not** turn
|
||
this into a sliding window without adding RBFI-style protection first.
|
||
|
||
### SD interfaces
|
||
|
||
`netboot_sd_write()` auto-detects, trying SD2SP2, then SD Gecko slot B, then
|
||
slot A. Note the SD Gecko in **slot A shares EXI channel 0 with the BBA**
|
||
(channel 0, device 2); libogc arbitrates, but SD2SP2 sits on its own channel
|
||
and avoids the question.
|
||
|
||
### Why the copier lives in assembly at the top of MEM1
|
||
|
||
A DOL's sections load from 0x80003100 upward — exactly where this program's own
|
||
`.text` sits. A copier compiled as an ordinary C function would **overwrite its
|
||
own instructions partway through the copy** and hang. So:
|
||
|
||
- `netboot_reserve()` carves the top of MEM1 and hands it to
|
||
`SYS_SetArena1Hi()`, so neither libogc nor `malloc` will touch it.
|
||
- `netboot_stub.S` is copied into that region and executed *there*. It must
|
||
stay strictly position independent: no globals, no literal pool, no TOC
|
||
references, no calls — otherwise it reads from the address it was *linked*
|
||
at instead of the one it is *running* at.
|
||
- `netboot_launch()` checks every section (and BSS) against the reserved
|
||
region and **refuses** if any would land on it.
|
||
|
||
### Failure behaviour
|
||
|
||
- 30 s with no frames, or 5 s mid-transfer, aborts back to the menu.
|
||
- A duplicate block is re-ACKed rather than ignored, so a lost ACK recovers.
|
||
- `netboot_receive()` re-asserts the NCRA SR bit first: an earlier test may
|
||
have left receive disabled, and it would otherwise wait forever for frames
|
||
the gateware is not storing.
|
||
- A failed SD write or verify launches nothing.
|
||
- The residual risk is confined to booting `/boot.dol`. If that is a healthy
|
||
Swiss, this is the safe part; if it is missing or corrupt, you get a message
|
||
and stay in the menu — the new build is already on the card either way.
|