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:
2026-10-10 14:02:28 +02:00
parent e10849f73d
commit 4a455bbe88
2 changed files with 163 additions and 3 deletions
+138
View File
@@ -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