From 4a455bbe88bfaafbcf271a3a31ae87cef7096645 Mon Sep 17 00:00:00 2001 From: Dennis Brentjes Date: Sat, 10 Oct 2026 14:02:28 +0200 Subject: [PATCH] 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 --- docs/bba_compat_todo.md | 28 ++++++- exi_bba/bba_register_file.py | 138 +++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 3 deletions(-) diff --git a/docs/bba_compat_todo.md b/docs/bba_compat_todo.md index a0a4636..22a5542 100644 --- a/docs/bba_compat_todo.md +++ b/docs/bba_compat_todo.md @@ -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 | |---|---|---| diff --git a/exi_bba/bba_register_file.py b/exi_bba/bba_register_file.py index ae68894..7e36b56 100644 --- a/exi_bba/bba_register_file.py +++ b/exi_bba/bba_register_file.py @@ -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