Decode NCRB and the registers with non-zero hardware defaults
Compat TODO items 3 and 4. Every register libogc2 writes now stores writes and reads back, with the documented hardware reset defaults: NCRB (0x01), BP (0x0A/0B), TLBP (0x0C/0D), RXINTT (0x14/15), RHBP (0x1A/1B), NWAYC (0x30, default 0x84), GCA (0x32), MISC (0x3D, default 0x3C), MISC2 (0x50). BP and RHBP reset to OUR layout (page 1, page 15) so a driver reading them back without writing sees something consistent with where the ring actually is. Previously all of these read 0x00 and dropped writes, so a driver doing read-modify-write or sanity-checking a default saw zero -- and libogc2 does read NWAYC before writing it. Deliberately still reading 0x00: LTPS, LRPS, TWP, TRP, TXFIFOCNT. Those are STATUS registers, not configuration, so storing what was written to them would be a lie rather than a default. Drivers read LRPS/LTPS only on error bits and 0x00 reads as "no error", which is truthful for a device that does not detect them. NCRB, BP and RHBP are also exported as BBARegisterFile.ncrb/.bp/.rhbp for the RX-filtering work (item 1) and the ring-geometry check (item 2). Nothing consumes them yet -- the decode lands first so the behaviour that uses it is a separate, testable change. T15 checks the defaults, replays libogc2's twelve init writes in its order and reads every one back, and asserts the NCRB export is 0x52 with PR=0, PM=0, AB=1 -- i.e. that the real BBA delivers own-MAC + broadcast only, which is the fact item 1 has to act on. Mutation-tested: dropping NWAYC's 0x84 default fails it, as does dropping NCRB writes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+25
-3
@@ -73,7 +73,9 @@ console) rather than corrupt silently.
|
||||
|
||||
## 3. `NCRB` (0x01) is not decoded at all
|
||||
|
||||
**Status: OPEN.** Bits, from libogc2:
|
||||
**Status: ✅ DECODED 2026-10-10** — stored, readable, and exported as
|
||||
`BBARegisterFile.ncrb`. Nothing consumes the bits yet; that is item 1.
|
||||
Bits, from libogc2:
|
||||
|
||||
| bit | name | meaning |
|
||||
|---|---|---|
|
||||
@@ -87,10 +89,30 @@ console) rather than corrupt silently.
|
||||
We interrupt per frame where libogc2 asks for every 2 packets, which is benign
|
||||
(more interrupts than requested). `PR`/`PM`/`AB` feed item 1.
|
||||
|
||||
`bba_register_file` **T15** asserts the export is `0x52` with `PR=0`, `PM=0`,
|
||||
`AB=1` — i.e. the real BBA would deliver own-MAC + broadcast only — which is
|
||||
the fact item 1 has to act on.
|
||||
|
||||
## 4. Registers that read `0x00` but have non-zero hardware defaults
|
||||
|
||||
**Status: OPEN.** A driver doing read-modify-write, or sanity-checking a
|
||||
default, sees zero. libogc2 *does* read `NWAYC` (0x30) before writing it.
|
||||
**Status: ✅ DONE 2026-10-10** for every register libogc2 writes. They now
|
||||
store writes and read back, with the documented hardware defaults at reset.
|
||||
`bba_register_file` **T15** checks the defaults, replays libogc2's twelve
|
||||
init writes and reads every one back; mutation-tested (dropping `NWAYC`'s
|
||||
`0x84` default fails it, as does dropping `NCRB` writes).
|
||||
|
||||
Still reading `0x00`, deliberately: `LTPS` (0x04), `LRPS` (0x05), `TWP`
|
||||
(0x0E/0F), `TRP` (0x12/13) and `TXFIFOCNT` (0x3E/3F). These are **status**
|
||||
registers, not configuration — a driver reads them to learn what the device
|
||||
did, so storing what was written to them would be a lie rather than a default.
|
||||
Drivers read `LRPS`/`LTPS` only on error bits, and `0x00` reads as "no error",
|
||||
which is the truthful answer for a device that does not detect those errors.
|
||||
`TWP`/`TRP`/`TXFIFOCNT` would need the TX engine to actually report its page
|
||||
pointers and FIFO depth; worth doing if a driver is ever seen to poll them.
|
||||
|
||||
The original problem for reference — a driver doing read-modify-write, or
|
||||
sanity-checking a default, saw zero. libogc2 *does* read `NWAYC` (0x30) before
|
||||
writing it.
|
||||
|
||||
| reg | name | hardware default |
|
||||
|---|---|---|
|
||||
|
||||
@@ -44,6 +44,36 @@ _TWD_HI = 0x35
|
||||
_TXDATA = 0x48
|
||||
|
||||
# Read-only hardcoded values
|
||||
# ── Registers libogc2 writes that were previously ignored ────────────
|
||||
# Addresses and hardware defaults from the official map (the BBA_* defines in
|
||||
# libogc2's lwip/arch/gc/netif/gcif.c). See docs/bba_compat_todo.md items 3
|
||||
# and 4: these used to read 0x00 and drop writes, so a driver doing
|
||||
# read-modify-write or sanity-checking a default saw zero. libogc2 DOES read
|
||||
# NWAYC (0x30) before writing it.
|
||||
_NCRB = 0x01 # Network Control Register B: filtering + RX int counter
|
||||
_BP_LO = 0x0A # Boundary page pointer (TX/RX split)
|
||||
_BP_HI = 0x0B
|
||||
_TLBP_LO = 0x0C # TX low boundary page
|
||||
_TLBP_HI = 0x0D
|
||||
_RXINTT_LO = 0x14 # RX interrupt timer (coalescing)
|
||||
_RXINTT_HI = 0x15
|
||||
_RHBP_LO = 0x1A # RX high boundary page
|
||||
_RHBP_HI = 0x1B
|
||||
_NWAYC = 0x30 # NWAY configuration
|
||||
_GCA = 0x32 # GMAC configuration A
|
||||
_MISC = 0x3D # MISC control 1
|
||||
_MISC2 = 0x50 # MISC control 2
|
||||
|
||||
_NWAYC_VAL = 0x84 # hardware reset default
|
||||
_MISC_VAL = 0x3C # hardware reset default
|
||||
_BP_VAL = 0x01 # our TX/RX split: TX = page 0, RX starts at page 1
|
||||
_RHBP_VAL = 0x0F # our ring ends at page 15
|
||||
|
||||
# NCRB bit positions (libogc2 writes AB|CA|2_PACKETS_PER_INT = 0x52)
|
||||
_NCRB_PR = 0 # promiscuous
|
||||
_NCRB_PM = 2 # pass multicast
|
||||
_NCRB_AB = 4 # accept broadcast
|
||||
|
||||
_CMD_REVID = 0x01 # command-space revision ID (read-only)
|
||||
_CMD_REVID_VAL = 0x00 # real-hardware value: never revised
|
||||
|
||||
@@ -102,6 +132,12 @@ class BBARegisterFile(Elaboratable):
|
||||
|
||||
# NCRA[3] = SR (start receive) bit — gates the RX ring-buffer path.
|
||||
self.ncra_sr = Signal()
|
||||
# Exported for the RX-filtering work (compat TODO item 1) and the ring
|
||||
# geometry check (item 2). Nothing consumes them yet; they are here so
|
||||
# the register decode and the behaviour that uses it land separately.
|
||||
self.ncrb = Signal(8) # filtering / RX-interrupt-counter bits
|
||||
self.bp = Signal(12) # boundary page: where the RX ring starts
|
||||
self.rhbp = Signal(12) # RX high boundary page: where it ends
|
||||
|
||||
# ── CDC FIFO sync-domain sides (wired by BBATop) ──────────────────
|
||||
# SPRAM request exi→sync: sync reads these
|
||||
@@ -269,6 +305,23 @@ 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)
|
||||
# Registers that exist on real hardware and were previously ignored.
|
||||
# Reset values are the documented hardware defaults where known, and
|
||||
# OUR layout where the register describes it (BP/RHBP), so a driver
|
||||
# that reads them back without writing sees something consistent.
|
||||
r_ncrb = Signal(8)
|
||||
r_bp_lo = Signal(8, init=_BP_VAL)
|
||||
r_bp_hi = Signal(8)
|
||||
r_tlbp_lo = Signal(8)
|
||||
r_tlbp_hi = Signal(8)
|
||||
r_rxintt_lo = Signal(8)
|
||||
r_rxintt_hi = Signal(8)
|
||||
r_rhbp_lo = Signal(8, init=_RHBP_VAL)
|
||||
r_rhbp_hi = Signal(8)
|
||||
r_nwayc = Signal(8, init=_NWAYC_VAL)
|
||||
r_gca = Signal(8)
|
||||
r_misc = Signal(8, init=_MISC_VAL)
|
||||
r_misc2 = Signal(8)
|
||||
# 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
|
||||
@@ -334,6 +387,19 @@ class BBARegisterFile(Elaboratable):
|
||||
m.d.comb += reg_rdval.eq(r_par[eff_addr[0:3]])
|
||||
with m.Case(_TWD_LO): m.d.comb += reg_rdval.eq(r_twd_lo)
|
||||
with m.Case(_TWD_HI): m.d.comb += reg_rdval.eq(r_twd_hi)
|
||||
with m.Case(_NCRB): m.d.comb += reg_rdval.eq(r_ncrb)
|
||||
with m.Case(_BP_LO): m.d.comb += reg_rdval.eq(r_bp_lo)
|
||||
with m.Case(_BP_HI): m.d.comb += reg_rdval.eq(r_bp_hi)
|
||||
with m.Case(_TLBP_LO): m.d.comb += reg_rdval.eq(r_tlbp_lo)
|
||||
with m.Case(_TLBP_HI): m.d.comb += reg_rdval.eq(r_tlbp_hi)
|
||||
with m.Case(_RXINTT_LO): m.d.comb += reg_rdval.eq(r_rxintt_lo)
|
||||
with m.Case(_RXINTT_HI): m.d.comb += reg_rdval.eq(r_rxintt_hi)
|
||||
with m.Case(_RHBP_LO): m.d.comb += reg_rdval.eq(r_rhbp_lo)
|
||||
with m.Case(_RHBP_HI): m.d.comb += reg_rdval.eq(r_rhbp_hi)
|
||||
with m.Case(_NWAYC): m.d.comb += reg_rdval.eq(r_nwayc)
|
||||
with m.Case(_GCA): m.d.comb += reg_rdval.eq(r_gca)
|
||||
with m.Case(_MISC): m.d.comb += reg_rdval.eq(r_misc)
|
||||
with m.Case(_MISC2): m.d.comb += reg_rdval.eq(r_misc2)
|
||||
with m.Case(_NWAYS): m.d.comb += reg_rdval.eq(_NWAYS_VAL)
|
||||
with m.Case(_HIPR): m.d.comb += reg_rdval.eq(_HIPR_VAL)
|
||||
with m.Default(): m.d.comb += reg_rdval.eq(0)
|
||||
@@ -658,6 +724,20 @@ class BBARegisterFile(Elaboratable):
|
||||
m.d.sync += tx_bytes.w_data.eq(self.rx_data)
|
||||
m.d.sync += tx_bytes.w_en.eq(1)
|
||||
m.d.sync += txdata_cnt.eq(txdata_cnt + 1)
|
||||
with m.Case(_NCRB):
|
||||
m.d.sync += r_ncrb.eq(self.rx_data)
|
||||
with m.Case(_BP_LO): m.d.sync += r_bp_lo.eq(self.rx_data)
|
||||
with m.Case(_BP_HI): m.d.sync += r_bp_hi.eq(self.rx_data)
|
||||
with m.Case(_TLBP_LO): m.d.sync += r_tlbp_lo.eq(self.rx_data)
|
||||
with m.Case(_TLBP_HI): m.d.sync += r_tlbp_hi.eq(self.rx_data)
|
||||
with m.Case(_RXINTT_LO): m.d.sync += r_rxintt_lo.eq(self.rx_data)
|
||||
with m.Case(_RXINTT_HI): m.d.sync += r_rxintt_hi.eq(self.rx_data)
|
||||
with m.Case(_RHBP_LO): m.d.sync += r_rhbp_lo.eq(self.rx_data)
|
||||
with m.Case(_RHBP_HI): m.d.sync += r_rhbp_hi.eq(self.rx_data)
|
||||
with m.Case(_NWAYC): m.d.sync += r_nwayc.eq(self.rx_data)
|
||||
with m.Case(_GCA): m.d.sync += r_gca.eq(self.rx_data)
|
||||
with m.Case(_MISC): m.d.sync += r_misc.eq(self.rx_data)
|
||||
with m.Case(_MISC2): m.d.sync += r_misc2.eq(self.rx_data)
|
||||
# All other addresses silently ignored
|
||||
|
||||
# No length field in the real protocol: consume data
|
||||
@@ -792,6 +872,16 @@ class BBARegisterFile(Elaboratable):
|
||||
with m.If(cmd_wreg <= 0x0F):
|
||||
m.d.sync += cmd_misc[cmd_wreg[0:4]].eq(cmd_wdata)
|
||||
|
||||
# ── Exported register values ─────────────────────────────────────
|
||||
# For the RX-filtering work (compat TODO item 1) and the ring geometry
|
||||
# check (item 2). Nothing consumes them yet: the decode lands first so
|
||||
# the behaviour that uses it is a separate, testable change.
|
||||
m.d.comb += [
|
||||
self.ncrb.eq(r_ncrb),
|
||||
self.bp .eq(Cat(r_bp_lo, r_bp_hi[0:4])),
|
||||
self.rhbp.eq(Cat(r_rhbp_lo, r_rhbp_hi[0:4])),
|
||||
]
|
||||
|
||||
# ── Interrupt output ─────────────────────────────────────────────
|
||||
m.d.comb += self.rrp.eq(r_rrp_lo)
|
||||
# INT_N comes from the COMMAND level, gated by the command-level mask
|
||||
@@ -1261,6 +1351,54 @@ if __name__ == "__main__":
|
||||
ctx.set(reg.tx_irq_i, 0)
|
||||
await ctx.tick("sync").repeat(2)
|
||||
|
||||
# ── T15: registers libogc2 writes that used to be ignored ────────
|
||||
# compat TODO items 3 and 4. These previously read 0x00 and dropped
|
||||
# writes, so a driver doing read-modify-write, or sanity-checking a
|
||||
# documented default, saw zero. libogc2 DOES read NWAYC (0x30) before
|
||||
# writing it.
|
||||
defaults = ((_NWAYC, _NWAYC_VAL), (_MISC, _MISC_VAL),
|
||||
(_BP_LO, _BP_VAL), (_RHBP_LO, _RHBP_VAL))
|
||||
for addr, want in defaults:
|
||||
got = (await exi_read(ctx, addr, 1))[0]
|
||||
if got != want:
|
||||
errors.append(f"T15 reg 0x{addr:02X} default 0x{got:02X}, "
|
||||
f"want 0x{want:02X}")
|
||||
print(f"T15 defaults: NWAYC/MISC/BP/RHBP = "
|
||||
f"{[hex((await exi_read(ctx, a, 1))[0]) for a, _ in defaults]}")
|
||||
|
||||
# The exact values libogc2's __bba_init writes, in its order.
|
||||
init_writes = ((_NCRB, 0x52), (_RXINTT_LO, 0x00), (_RXINTT_HI, 0x06),
|
||||
(_MISC2, 0x80), (_TLBP_LO, 0x00), (_TLBP_HI, 0x00),
|
||||
(_BP_LO, 0x01), (_BP_HI, 0x00),
|
||||
(_RHBP_LO, 0x0F), (_RHBP_HI, 0x00),
|
||||
(_GCA, 0x08), (_NWAYC, 0x0C))
|
||||
for addr, val in init_writes:
|
||||
await exi_write(ctx, addr, [val])
|
||||
for addr, val in init_writes:
|
||||
got = (await exi_read(ctx, addr, 1))[0]
|
||||
if got != val:
|
||||
errors.append(f"T15 reg 0x{addr:02X}: wrote 0x{val:02X}, "
|
||||
f"read 0x{got:02X}")
|
||||
print(f"T15 libogc2's {len(init_writes)} init writes all read back")
|
||||
|
||||
# NCRB is what the RX filter will be steered from, so the bits must
|
||||
# survive: 0x52 = AB | CA | 2_PACKETS_PER_INT, with PR and PM CLEAR
|
||||
# (so the real BBA delivers own-MAC + broadcast only).
|
||||
ncrb = ctx.get(reg.ncrb)
|
||||
pr = (ncrb >> _NCRB_PR) & 1
|
||||
pm = (ncrb >> _NCRB_PM) & 1
|
||||
ab = (ncrb >> _NCRB_AB) & 1
|
||||
print(f"T15 ncrb export = 0x{ncrb:02X} (PR={pr} PM={pm} AB={ab})")
|
||||
if ncrb != 0x52 or pr or pm or not ab:
|
||||
errors.append(f"T15 ncrb export 0x{ncrb:02X}, want 0x52 "
|
||||
f"(PR=0 PM=0 AB=1)")
|
||||
if ctx.get(reg.rhbp) != 0x00F or ctx.get(reg.bp) != 0x001:
|
||||
errors.append(f"T15 ring geometry exports: bp="
|
||||
f"0x{ctx.get(reg.bp):03X} rhbp="
|
||||
f"0x{ctx.get(reg.rhbp):03X}, want 0x001/0x00F")
|
||||
print(f"T15 geometry exports: bp=0x{ctx.get(reg.bp):03X} "
|
||||
f"rhbp=0x{ctx.get(reg.rhbp):03X}")
|
||||
|
||||
# ── 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
|
||||
|
||||
Reference in New Issue
Block a user