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:
@@ -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)
|
## TX path — libogc writes NO TWD (found 2026-10-10)
|
||||||
|
|
||||||
`__bba_link_tx` is **four transactions**:
|
`__bba_link_tx` is **four transactions**:
|
||||||
|
|||||||
@@ -265,7 +265,11 @@ class BBARegisterFile(Elaboratable):
|
|||||||
cmd_wdata = Signal(8)
|
cmd_wdata = Signal(8)
|
||||||
# EXI-side command registers. Swiss uses exactly two: 0x02 is the EXI
|
# EXI-side command registers. Swiss uses exactly two: 0x02 is the EXI
|
||||||
# interrupt mask and 0x03 the EXI interrupt status (write-1-to-clear).
|
# interrupt mask and 0x03 the EXI interrupt status (write-1-to-clear).
|
||||||
r_cmd_imr = Signal(8)
|
r_cmd_imr = Signal(8, init=0xF8)
|
||||||
|
# Reset UNMASKED: the real device's power-on value is unknown, and
|
||||||
|
# both drivers set this explicitly (IRMASKALL first thing in init), so
|
||||||
|
# the permissive default costs them nothing while keeping INT_N usable
|
||||||
|
# for software that never writes it.
|
||||||
r_cmd_ir = Signal(8)
|
r_cmd_ir = Signal(8)
|
||||||
tx_frame_len = Signal(16)
|
tx_frame_len = Signal(16)
|
||||||
# TX trigger bookkeeping. `twd_armed` fires one cycle AFTER TWD_HI is
|
# TX trigger bookkeeping. `twd_armed` fires one cycle AFTER TWD_HI is
|
||||||
@@ -329,10 +333,37 @@ class BBARegisterFile(Elaboratable):
|
|||||||
# The device-ID constant lives in SPIMode3Slave now (it must be
|
# The device-ID constant lives in SPIMode3Slave now (it must be
|
||||||
# answered with zero CDC latency), so there is no copy here.
|
# answered with zero CDC latency), so there is no copy here.
|
||||||
|
|
||||||
|
# ── Command-level interrupt status (reg 0x03) ────────────────────
|
||||||
|
# ⚠️ THE BBA HAS TWO INTERRUPT LEVELS and both drivers depend on it.
|
||||||
|
# Their handlers are, identically:
|
||||||
|
#
|
||||||
|
# status = bba_cmd_in8(0x03); /* COMMAND level */
|
||||||
|
# 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". This file implemented only the MAC level and
|
||||||
|
# never set that bit, so a command read of 0x03 returned 0x00, every
|
||||||
|
# branch of both handlers fell through, and NEITHER DRIVER WOULD EVER
|
||||||
|
# PROCESS A RECEIVED FRAME. Our own tooling never noticed because
|
||||||
|
# gc_bba_test and netboot poll RWP/IR directly and do not use the
|
||||||
|
# interrupt path at all.
|
||||||
|
#
|
||||||
|
# Bit 7 is a LIVE LEVEL, not a latched bit, deliberately: 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, losing the event; a level stays
|
||||||
|
# asserted exactly as long as the MAC condition does.
|
||||||
|
mac_int = Signal()
|
||||||
|
m.d.comb += mac_int.eq((r_ir & r_imr).any())
|
||||||
|
|
||||||
|
cmd_ir_val = Signal(8)
|
||||||
|
m.d.comb += cmd_ir_val.eq(Cat(r_cmd_ir[0:7], mac_int))
|
||||||
|
|
||||||
cmd_rdval = Signal(8)
|
cmd_rdval = Signal(8)
|
||||||
with m.Switch(cmd_reg):
|
with m.Switch(cmd_reg):
|
||||||
with m.Case(0x02): m.d.comb += cmd_rdval.eq(r_cmd_imr)
|
with m.Case(0x02): m.d.comb += cmd_rdval.eq(r_cmd_imr)
|
||||||
with m.Case(0x03): m.d.comb += cmd_rdval.eq(r_cmd_ir)
|
with m.Case(0x03): m.d.comb += cmd_rdval.eq(cmd_ir_val)
|
||||||
with m.Default(): m.d.comb += cmd_rdval.eq(0)
|
with m.Default(): m.d.comb += cmd_rdval.eq(0)
|
||||||
|
|
||||||
rd_val = Signal(8) # response for the current non-SPRAM read byte
|
rd_val = Signal(8) # response for the current non-SPRAM read byte
|
||||||
@@ -735,7 +766,12 @@ class BBARegisterFile(Elaboratable):
|
|||||||
|
|
||||||
# ── Interrupt output ─────────────────────────────────────────────
|
# ── Interrupt output ─────────────────────────────────────────────
|
||||||
m.d.comb += self.rrp.eq(r_rrp_lo)
|
m.d.comb += self.rrp.eq(r_rrp_lo)
|
||||||
m.d.sync += self.exi_int_n.eq(~(r_ir & r_imr).any())
|
# INT_N comes from the COMMAND level, gated by the command-level mask
|
||||||
|
# (reg 0x02). Both drivers write IRMASKALL (0x00) on entry to their
|
||||||
|
# handler and IRMASKNONE (0xF8) on exit, which is how they stop the
|
||||||
|
# line re-asserting while they work -- so honouring that mask is what
|
||||||
|
# prevents an interrupt storm during handling.
|
||||||
|
m.d.sync += self.exi_int_n.eq(~(cmd_ir_val & r_cmd_imr).any())
|
||||||
|
|
||||||
# ── Consume RWP updates from sync domain ──────────────────────────
|
# ── Consume RWP updates from sync domain ──────────────────────────
|
||||||
with m.If(rx_wptr.r_rdy):
|
with m.If(rx_wptr.r_rdy):
|
||||||
@@ -1197,6 +1233,49 @@ if __name__ == "__main__":
|
|||||||
ctx.set(reg.tx_irq_i, 0)
|
ctx.set(reg.tx_irq_i, 0)
|
||||||
await ctx.tick("sync").repeat(2)
|
await ctx.tick("sync").repeat(2)
|
||||||
|
|
||||||
|
# ── T13: the COMMAND-level interrupt both drivers gate on ────────
|
||||||
|
# Both handlers are, identically:
|
||||||
|
# status = bba_cmd_in8(0x03); if (status & 0x80) bba_interrupt();
|
||||||
|
# so with bit 7 clear NEITHER driver ever reaches the MAC-level
|
||||||
|
# handler that reads IR/IMR and starts RX. Our own tooling cannot
|
||||||
|
# catch this: gc_bba_test and netboot poll RWP/IR and never use the
|
||||||
|
# interrupt path.
|
||||||
|
await exi_write(ctx, _IR, [0xFF]) # clear MAC IR
|
||||||
|
await exi_write(ctx, _IMR, [0x86]) # enable RBFI|TI|RI
|
||||||
|
await cmd_write(ctx, 0x02, [0xF8]) # IRMASKNONE
|
||||||
|
before = (await cmd_read(ctx, 0x03, 1))[0]
|
||||||
|
int_idle = ctx.get(reg.exi_int_n)
|
||||||
|
ctx.set(reg.rx_irq_i, 1) # a frame arrives
|
||||||
|
await ctx.tick("sync")
|
||||||
|
ctx.set(reg.rx_irq_i, 0)
|
||||||
|
await ctx.tick("sync").repeat(3)
|
||||||
|
after = (await cmd_read(ctx, 0x03, 1))[0]
|
||||||
|
int_pend = ctx.get(reg.exi_int_n)
|
||||||
|
print(f"T13 cmd 0x03: idle=0x{before:02X} after RI=0x{after:02X}; "
|
||||||
|
f"INT_N {int_idle} -> {int_pend}")
|
||||||
|
if before & 0x80:
|
||||||
|
errors.append(f"T13 cmd 0x03 bit7 set while idle: 0x{before:02X}")
|
||||||
|
if not (after & 0x80):
|
||||||
|
errors.append(f"T13 cmd 0x03 bit7 NOT set after RI: 0x{after:02X} "
|
||||||
|
f"- both drivers would skip bba_interrupt()")
|
||||||
|
if int_pend:
|
||||||
|
errors.append("T13 INT_N not asserted with an unmasked MAC event")
|
||||||
|
|
||||||
|
# Masking at the COMMAND level must drop INT_N -- this is how both
|
||||||
|
# drivers stop the line re-asserting while their handler runs.
|
||||||
|
await cmd_write(ctx, 0x02, [0x00]) # IRMASKALL
|
||||||
|
await ctx.tick("sync").repeat(3)
|
||||||
|
masked = ctx.get(reg.exi_int_n)
|
||||||
|
still = (await cmd_read(ctx, 0x03, 1))[0]
|
||||||
|
print(f"T13b IRMASKALL: INT_N={masked} (want 1), "
|
||||||
|
f"status still 0x{still:02X}")
|
||||||
|
if not masked:
|
||||||
|
errors.append("T13b INT_N still asserted after IRMASKALL")
|
||||||
|
if not (still & 0x80):
|
||||||
|
errors.append("T13b masking must not clear the pending status")
|
||||||
|
await cmd_write(ctx, 0x02, [0xF8])
|
||||||
|
await exi_write(ctx, _IR, [0xFF]) # tidy up
|
||||||
|
|
||||||
# ── T12: Swiss's command-space form, which DIFFERS from libogc's ──
|
# ── T12: Swiss's command-space form, which DIFFERS from libogc's ──
|
||||||
# Swiss sends ONE 4-byte transfer with the value as byte 3 and reads
|
# Swiss sends ONE 4-byte transfer with the value as byte 3 and reads
|
||||||
# the response from byte 3; libogc sends three bytes with the value as
|
# the response from byte 3; libogc sends three bytes with the value as
|
||||||
|
|||||||
Reference in New Issue
Block a user