Implement the COMMAND-level interrupt: without it no driver sees a frame

The BBA has two interrupt levels and we implemented only one.  Both drivers'
handlers are identical in shape -- libogc2's bba_event_handler and Swiss's
exi_coroutine:

  status = bba_cmd_in8(0x03);            /* COMMAND level */
  bba_cmd_out8(0x02, BBA_CMD_IRMASKALL);
  if (status & 0x80) bba_interrupt();    /* ONLY then the MAC level */

and bba_interrupt() is what reads register-space IR/IMR and calls
bba_start_rx().  Bit 7 of command register 0x03 means "a MAC interrupt is
pending".  r_cmd_ir was write-1-clear with nothing ever setting it, so a
command read of 0x03 returned 0x00, every branch of both handlers fell through,
and NEITHER DRIVER WOULD EVER HAVE PROCESSED A RECEIVED FRAME.

Our own tooling could not have caught it: gc_bba_test and netboot poll RWP/IR
directly and never use the interrupt path, so the whole command-level mechanism
was untested and unused.  Green suite, dead interface.

cmd_ir[7] is now a live level equal to (IR & IMR) != 0, and INT_N is
(cmd_ir & cmd_imr) != 0.  A level rather than a latch because both drivers
clear the MAC IR inside bba_interrupt() and only then write-1-clear command
0x03: a latched bit would be cleared by that write even if a new frame had
arrived in between, while a level stays asserted as long as the condition does.
INT_N honouring the command-level mask is what stops the line re-asserting
while a handler runs (IRMASKALL on entry, IRMASKNONE on exit).  r_cmd_imr
resets to 0xF8 since the real power-on value is unknown and both drivers set it
explicitly first thing.

T13 and T13b cover it, both mutation-tested: reading the latched bits only
leaves 0x00 after an RI, and ignoring the command mask leaves INT_N asserted
through IRMASKALL.

Also records in CLAUDE.md which driver actually talks to us, since Swiss has
several back-ends and it is easy to read the wrong one: cube/patches/bba/ has
one file per chip (bba.c is ours; enc28j60.c, w5500.c, w6100.c, w6300.c are
other adapters), Swiss's menu networking goes through libogc2's gcif.c, and
cube/patches/bba/bba.c is the in-game patch driver.  Detection is by EXI device
ID, so returning 04 02 02 00 is what gets us handed to the BBA path at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 11:11:55 +02:00
parent 88d05988b5
commit 0d100eb84f
2 changed files with 159 additions and 3 deletions
+77
View File
@@ -1307,6 +1307,83 @@ parallel master is the default — see "W5100 vs W5500".)
---
## ⚠️ Which driver actually talks to us — and the TWO interrupt levels
**Swiss has several ethernet back-ends, and `cube/patches/bba/` holds one file
per chip** — `bba.c` (real DOL-015 / MX98730EC), `enc28j60.c`, `w5500.c`,
`w6100.c`, `w6300.c`. Only `bba.c` is our chain; the others are different
adapters (Swiss also ships `eth2gc` and `gcnet` artwork). When reading Swiss,
check which file you are in.
**Swiss uses TWO different drivers, and we meet both:**
| context | driver | command-space form |
|---|---|---|
| Swiss's own menu/network (`if_configex`) | **libogc2** `lwip/arch/gc/netif/gcif.c` | 2-byte header + data |
| patches injected into a GAME | **Swiss** `cube/patches/bba/bba.c` | one 4-byte transfer |
`cube/swiss/source/bba.c` is only the app-side glue: it calls `if_configex()`
and then picks a `GetExiSpeed` by the interface name libogc2 registered
(`'e'` → MX98730EC, `'E'` → ENC28J60, `'W'` → W5500, `'w'` → W6X00).
Detection is by **EXI device ID**:
```c
if ((location & LOC_SERIAL_PORT_1) && EXI_GetID(EXI_CHANNEL_0, EXI_DEVICE_2, &id)) {
switch (id) { case EXI_W5500_ID: case EXI_MX98730EC_ID: case EXI_ENC28J60_ID:
```
so returning `04 02 02 00` is what makes Swiss treat us as a real BBA and hand
us to the libogc2 path.
### ✅ FIXED 2026-10-10 — the COMMAND-level interrupt (showstopper)
**The BBA has two interrupt levels and we implemented only one.** Both drivers'
handlers are identical in shape:
```c
/* libogc2 bba_event_handler, and Swiss's exi_coroutine */
status = bba_cmd_in8(0x03); /* COMMAND level */
bba_cmd_out8(0x02, BBA_CMD_IRMASKALL); /* mask while working */
if (status & 0x80) bba_interrupt(); /* ONLY then the MAC level */
...
bba_cmd_out8(0x03, status); /* write-1-clear */
bba_cmd_out8(0x02, BBA_CMD_IRMASKNONE);
```
and `bba_interrupt()` is the function that reads register-space `IR`/`IMR` and
calls `bba_start_rx()`. **Bit 7 of command register `0x03` means "a MAC
interrupt is pending".** `r_cmd_ir` was write-1-clear and nothing ever set it,
so a command read of `0x03` returned `0x00`, every branch of both handlers fell
through, and **neither driver would ever have processed a received frame.**
⚠️ **Our own tooling could not have caught this.** `gc_bba_test` and `netboot`
poll `RWP`/`IR` directly and never use the interrupt path, so the entire
command-level mechanism was untested and unused — green suite, dead interface.
Now: `cmd_ir[7]` is a **live level** equal to `(IR & IMR) != 0`, and
`INT_N = (cmd_ir & cmd_imr) != 0`.
- A level, not a latch, because both drivers clear the MAC `IR` inside
`bba_interrupt()` and only then write-1-clear command `0x03`. A latched bit
would be cleared by that write even if a new frame had arrived in between;
a level stays asserted exactly as long as the MAC condition does.
- `INT_N` honours the **command-level** mask, which is how both drivers stop
the line re-asserting while their handler runs (`IRMASKALL` = `0x00` on
entry, `IRMASKNONE` = `0xF8` on exit). Ignoring it is an interrupt storm.
- `r_cmd_imr` resets to `0xF8` (unmasked). The real power-on value is unknown;
both drivers set it explicitly first thing, so a permissive default costs
them nothing and keeps `INT_N` usable for software that never writes it.
Tests: `bba_register_file` **T13** (bit 7 clear when idle, set after an RI with
`IMR` enabling it, `INT_N` asserted) and **T13b** (`IRMASKALL` drops `INT_N`
but must NOT clear the pending status). Both mutation-tested.
Other command registers both drivers touch, none of them decoded: `0x01`
(revision ID — libogc2 stores it as `revid`), `0x04`/`0x05` (libogc2 writes its
`devid` 2 bytes + `acstart`), `0x08`/`0x09`/`0x0B` (challenge/response — see
the note saying not to implement it). Reads return `0x00`.
---
## TX path — libogc writes NO TWD (found 2026-10-10)
`__bba_link_tx` is **four transactions**: