diff --git a/CLAUDE.md b/CLAUDE.md index 0f2bb26..8ee4dc6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1404,10 +1404,50 @@ 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`. +### Command register map — implemented 2026-10-10 + +Enumerated from libogc2's `gcif.c` rather than guessed at random. Swiss's patch +driver touches only `0x02` and `0x03`; everything else here is libogc2. + +| reg | access | who / what | our behaviour | +|---|---|---|---| +| `0x01` | read | revision ID → `priv->revid` | **`0x00`**, read-only | +| `0x02` | write | interrupt mask (`IRMASKALL` `0x00` / `IRMASKNONE` `0xF8`) | gates `INT_N` | +| `0x03` | read, write-1-clear | interrupt status | bit 7 = live MAC level | +| `0x04` | write, **2 bytes** | `priv->devid` = `0xD107` | stored (last byte) | +| `0x05` | write | `priv->acstart` = `0x4E` | stored | +| `0x08` | read, **4 bytes** | challenge | stored/readable | +| `0x09` | write, **4 bytes** | response | stored | +| `0x0B` | read | challenge/response status | stored/readable | +| `0x0F` | read (slow, 200 µs) | in `__bba_reset()`, result **DISCARDED** | stored/readable | +| `> 0x0F` | — | nothing | reads `0x00` | + +**`0x01` = `0x00` is a real-hardware value, not a guess:** the BBA never +received a functional firmware revision that incremented it. ⚠️ Do NOT take +libogc2's `priv->revid = 0xf0` as the device value — that is its pre-init +placeholder, overwritten by whatever this read returns. `revid`, `devid` and +`acstart` feed exactly one thing, `bba_calc_response()`, and that path is +device-initiated (status bit `0x10`), so for us they are functionally inert. + +Everything from `0x04` up is **plain read/write storage — a GUESS.** Both +drivers treat these as write-only and never read them back, so +read-what-you-wrote is the least surprising behaviour and lets our own tests +confirm the write landed. The alternative (ignore them, as before) is strictly +worse: software that writes them was silently dropped. + +⚠️ `0x0F` deserves a note: `bba_cmd_in8_slow(0x0F)` sits in `__bba_reset()` +between two 10 ms delays, reads one byte with CS held for 200 µs, and +**discards the result**. A read whose value is thrown away but whose timing is +deliberate looks like a hardware reset strobe. We treat it as an ordinary +register; if a real-hardware reset ever turns out to depend on it, this is the +place to add the side effect. The NCRA reset follows immediately in that +sequence anyway, which is why ignoring it costs nothing today. + +Test: `bba_register_file` **T14** covers the whole table — revision ID value +and read-only-ness, write/read-back on `0x04`/`0x05`/`0x09`/`0x0B`/`0x0F`, and +`0x20` still reading `0x00` so a stray high register cannot alias onto a real +one. Mutation-tested: making `0x01` plain storage fails it, and dropping the +`0x04+` writes (the old behaviour) fails it. --- diff --git a/exi_bba/bba_register_file.py b/exi_bba/bba_register_file.py index 506a015..ae68894 100644 --- a/exi_bba/bba_register_file.py +++ b/exi_bba/bba_register_file.py @@ -44,6 +44,9 @@ _TWD_HI = 0x35 _TXDATA = 0x48 # Read-only hardcoded values +_CMD_REVID = 0x01 # command-space revision ID (read-only) +_CMD_REVID_VAL = 0x00 # real-hardware value: never revised + _NWAYS_VAL = 0x17 _HIPR_VAL = 0x01 @@ -266,6 +269,11 @@ class BBARegisterFile(Elaboratable): # EXI-side command registers. Swiss uses exactly two: 0x02 is the EXI # interrupt mask and 0x03 the EXI interrupt status (write-1-to-clear). r_cmd_imr = Signal(8, init=0xF8) + # Generic storage for the remaining command registers, so a write + # sticks and a read returns it instead of 0x00. Every command + # register the real drivers touch is in 0x00-0x0F; anything above + # reads 0. See the table in CLAUDE.md for who touches what. + cmd_misc = Array([Signal(8, name=f"cmd_misc_{i}") for i in range(16)]) # 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 @@ -362,9 +370,20 @@ class BBARegisterFile(Elaboratable): cmd_rdval = Signal(8) with m.Switch(cmd_reg): + with m.Case(_CMD_REVID): + # Revision ID. REAL HARDWARE VALUE: 0x00 -- the BBA never + # received a functional firmware revision that incremented + # it. libogc2 pre-seeds priv->revid = 0xF0 and then + # overwrites it with whatever this read returns, so 0xF0 is + # its placeholder, NOT the device's value. + m.d.comb += cmd_rdval.eq(_CMD_REVID_VAL) with m.Case(0x02): m.d.comb += cmd_rdval.eq(r_cmd_imr) 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(): + with m.If(cmd_reg <= 0x0F): + m.d.comb += cmd_rdval.eq(cmd_misc[cmd_reg[0:4]]) + with m.Else(): + m.d.comb += cmd_rdval.eq(0) rd_val = Signal(8) # response for the current non-SPRAM read byte with m.If(cmd_space): @@ -761,8 +780,17 @@ class BBARegisterFile(Elaboratable): with m.If(cmd_wr): m.d.sync += cmd_wr.eq(0) with m.Switch(cmd_wreg): + with m.Case(_CMD_REVID): + pass # revision ID is READ-ONLY with m.Case(0x02): m.d.sync += r_cmd_imr.eq(cmd_wdata) with m.Case(0x03): m.d.sync += r_cmd_ir.eq(r_cmd_ir & ~cmd_wdata) + with m.Default(): + # Everything else is plain storage. GUESSED behaviour -- + # these are write-only in both drivers and never read + # back, so read-what-you-wrote is the least surprising + # choice and lets our own tests verify the writes landed. + with m.If(cmd_wreg <= 0x0F): + m.d.sync += cmd_misc[cmd_wreg[0:4]].eq(cmd_wdata) # ── Interrupt output ───────────────────────────────────────────── m.d.comb += self.rrp.eq(r_rrp_lo) @@ -1233,6 +1261,46 @@ if __name__ == "__main__": ctx.set(reg.tx_irq_i, 0) await ctx.tick("sync").repeat(2) + # ── T14: every command register the real drivers touch ─────────── + # Enumerated from libogc2's gcif.c rather than guessed at random: + # 0x01 read revision ID -> priv->revid + # 0x02 write interrupt mask (IRMASKALL/IRMASKNONE) + # 0x03 read / w1c interrupt status + # 0x04 write 2 bytes priv->devid (0xD107) + # 0x05 write 1 byte priv->acstart (0x4E) + # 0x08 read 4 bytes challenge | device-initiated path, + # 0x09 write 4 bytes response | gated on status bit 0x10 + # 0x0B read challenge/response status + # 0x0F read (slow) in __bba_reset(), result DISCARDED + # Swiss's patch driver touches only 0x02 and 0x03. + rev = (await cmd_read(ctx, _CMD_REVID, 1))[0] + print(f"T14 cmd 0x01 revision ID = 0x{rev:02X} (want 0x00, real HW)") + if rev != _CMD_REVID_VAL: + errors.append(f"T14 revision ID 0x{rev:02X}, want " + f"0x{_CMD_REVID_VAL:02X}") + # Read-only: a write must not change it. + await cmd_write(ctx, _CMD_REVID, [0xA5]) + rev2 = (await cmd_read(ctx, _CMD_REVID, 1))[0] + if rev2 != _CMD_REVID_VAL: + errors.append(f"T14 revision ID writable: became 0x{rev2:02X}") + + # The write-only ones must at least STICK, so software that writes + # them is not silently ignored. libogc2 writes 0x04/0x05 in init + # unconditionally. + for creg, val in ((0x04, 0xD1), (0x05, 0x4E), (0x09, 0x3C), + (0x0B, 0x77), (0x0F, 0x11)): + await cmd_write(ctx, creg, [val]) + rb = (await cmd_read(ctx, creg, 1))[0] + if rb != val: + errors.append(f"T14 cmd 0x{creg:02X}: wrote 0x{val:02X}, " + f"read 0x{rb:02X}") + print(f"T14 cmd 0x04/05/09/0B/0F all read back what was written") + # Out of range still reads 0, so a stray high register cannot alias + # onto a real one. + hi = (await cmd_read(ctx, 0x20, 1))[0] + if hi != 0x00: + errors.append(f"T14 cmd 0x20 reads 0x{hi:02X}, want 0x00") + # ── T13: the COMMAND-level interrupt both drivers gate on ──────── # Both handlers are, identically: # status = bba_cmd_in8(0x03); if (status & 0x80) bba_interrupt();