Implement the command registers libogc2 actually touches
Previously every command register except 0x02/0x03 read 0x00 and dropped writes. Real software will hit them, so they are now implemented, enumerated from libogc2's gcif.c rather than guessed at random (Swiss's patch driver touches only 0x02 and 0x03): 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 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 0x01 returns 0x00 and is read-only. That is a real-hardware value, not a guess: the BBA never received a functional firmware revision that incremented it. libogc2's priv->revid = 0xf0 is its PRE-INIT PLACEHOLDER, overwritten by whatever this read returns, so it must not be mistaken for the device value. revid/devid/acstart feed only bba_calc_response(), whose path is device-initiated, so they are functionally inert for us. 0x04 and up are plain read/write storage, which IS a guess: both drivers treat them as write-only and never read them back, so read-what-you-wrote is the least surprising behaviour and lets our tests confirm the write landed. Ignoring them (the old behaviour) is strictly worse -- the write vanished. Registers above 0x0F still read 0x00 so a stray high register cannot alias onto a real one. Noted for later: bba_cmd_in8_slow(0x0F) sits in __bba_reset() between two 10 ms delays, holds CS for 200 us and discards the result. A read whose value is thrown away but whose timing is deliberate looks like a reset strobe; we treat it as an ordinary register, and the NCRA reset follows immediately in that sequence, so ignoring the side effect costs nothing today. T14 covers the whole table and is mutation-tested: making 0x01 plain storage fails it, and dropping the 0x04+ writes fails it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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`
|
`IMR` enabling it, `INT_N` asserted) and **T13b** (`IRMASKALL` drops `INT_N`
|
||||||
but must NOT clear the pending status). Both mutation-tested.
|
but must NOT clear the pending status). Both mutation-tested.
|
||||||
|
|
||||||
Other command registers both drivers touch, none of them decoded: `0x01`
|
### Command register map — implemented 2026-10-10
|
||||||
(revision ID — libogc2 stores it as `revid`), `0x04`/`0x05` (libogc2 writes its
|
|
||||||
`devid` 2 bytes + `acstart`), `0x08`/`0x09`/`0x0B` (challenge/response — see
|
Enumerated from libogc2's `gcif.c` rather than guessed at random. Swiss's patch
|
||||||
the note saying not to implement it). Reads return `0x00`.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ _TWD_HI = 0x35
|
|||||||
_TXDATA = 0x48
|
_TXDATA = 0x48
|
||||||
|
|
||||||
# Read-only hardcoded values
|
# 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
|
_NWAYS_VAL = 0x17
|
||||||
_HIPR_VAL = 0x01
|
_HIPR_VAL = 0x01
|
||||||
|
|
||||||
@@ -266,6 +269,11 @@ class BBARegisterFile(Elaboratable):
|
|||||||
# 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, init=0xF8)
|
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
|
# Reset UNMASKED: the real device's power-on value is unknown, and
|
||||||
# both drivers set this explicitly (IRMASKALL first thing in init), so
|
# both drivers set this explicitly (IRMASKALL first thing in init), so
|
||||||
# the permissive default costs them nothing while keeping INT_N usable
|
# the permissive default costs them nothing while keeping INT_N usable
|
||||||
@@ -362,9 +370,20 @@ class BBARegisterFile(Elaboratable):
|
|||||||
|
|
||||||
cmd_rdval = Signal(8)
|
cmd_rdval = Signal(8)
|
||||||
with m.Switch(cmd_reg):
|
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(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.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
|
rd_val = Signal(8) # response for the current non-SPRAM read byte
|
||||||
with m.If(cmd_space):
|
with m.If(cmd_space):
|
||||||
@@ -761,8 +780,17 @@ class BBARegisterFile(Elaboratable):
|
|||||||
with m.If(cmd_wr):
|
with m.If(cmd_wr):
|
||||||
m.d.sync += cmd_wr.eq(0)
|
m.d.sync += cmd_wr.eq(0)
|
||||||
with m.Switch(cmd_wreg):
|
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(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.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 ─────────────────────────────────────────────
|
# ── Interrupt output ─────────────────────────────────────────────
|
||||||
m.d.comb += self.rrp.eq(r_rrp_lo)
|
m.d.comb += self.rrp.eq(r_rrp_lo)
|
||||||
@@ -1233,6 +1261,46 @@ 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)
|
||||||
|
|
||||||
|
# ── 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 ────────
|
# ── T13: the COMMAND-level interrupt both drivers gate on ────────
|
||||||
# Both handlers are, identically:
|
# Both handlers are, identically:
|
||||||
# status = bba_cmd_in8(0x03); if (status & 0x80) bba_interrupt();
|
# status = bba_cmd_in8(0x03); if (status & 0x80) bba_interrupt();
|
||||||
|
|||||||
Reference in New Issue
Block a user