1327f82719
The alignment bug is FIXED -- hardware now reports "frame 1 (342 B) etype=0800 at 12: FF FF FF FF FF FF 00 2B", i.e. broadcast dst at 0, host MAC at 6, ethertype at 12. Perfectly aligned, correctly received, correctly skipped as not ours. So 00 00 00 01 was never a dropped write. It is frame data being read as a descriptor, because the ring HAS NO OVERFLOW PROTECTION: nothing in the gateware consumes RRP, so RXFrameAssembler keeps writing and laps the reader (the real BBA has RBFI for exactly this). The poll loop costs ~5 EXI transactions per frame (~200 us) while a 100 Mbit link delivers a 342-byte frame every ~30 us, and the ring is 15 pages -- roughly 3 to 11 frames. Under continuous traffic pages are reused before they are read. That is also why it "worked once out of three tries": it is a race against the host's background traffic. ring_next() now returns -4 when the writer is more than two thirds of the ring ahead, and the caller resyncs and continues instead of parsing bytes that belong to a different frame. The timeout reports seen / bad-descriptor / lapped counts so the three cases stay distinguishable. Detection is not prevention, so the app and the README both now say to quiet the link first with gc-testlink.sh, which sets a static address and takes the interface out of NetworkManager -- stopping the DHCP client whose 342-byte broadcasts are the main offender. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
192 lines
8.4 KiB
Markdown
192 lines
8.4 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`.
|
||
|
||
## 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.
|
||
|
||
### 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.
|