From 2893e7f6fbd30e2a16ab3b74fd4177b9a179fa46 Mon Sep 17 00:00:00 2001 From: Dennis Brentjes Date: Sat, 10 Oct 2026 14:51:17 +0200 Subject: [PATCH] Filter RX to own-MAC + broadcast: unfiltered MACRAW hung Swiss Compat TODO item 1. S0_MR now sets the MACRAW MF bit, so the W5100 delivers frames addressed to SHAR or broadcast only -- what the real BBA does, since libogc2 configures NCRB = AB|CA|2_PACKETS_PER_INT (PR=0, PM=0, AB=1). NCRB.PR (bit 0) clears MF if the GC asks for promiscuous, sampled when socket 0 is OPENed, so a later PR change applies on the next NCRA reset. This was not a performance issue, it was a HANG. Swiss sat on "Initialising Network" forever. libogc2's bba_interrupt() loops while (status) { clear IR; start_rx; status = IR & IMR; } which only exits when IR & IMR reaches zero. Unfiltered, every frame on the segment set RI, re-asserting faster than the loop could clear it, so it never exited. Unplugging the ethernet cable let Swiss run all the way through and then fail DHCP for want of a server -- a one-minute experiment that identified it after a lot of theorising about descriptors and interrupts. Filtering against SHAR turned out to be safe for this path after all. The earlier worry was that MF filters on SHAR, which we write only at init, so a driver WRITING PAR0-5 would leave it stale and MF would drop everything. libogc2 READS the MAC out of PAR0-5 instead, so SHAR -- written from reg.par in that same init -- cannot go stale underneath it. Broadcast still passes MF and a DHCP OFFER unicast to our MAC passes too. Left open and recorded: refreshing SHAR on a PAR0-5 write plus a socket re-open, for a driver that does write it (the official SDK driver is unread). The W5500 back-end is unchanged, being the unused alternate path; it would need the same Sn_MR treatment if revived. T1 expects S0_MR = MACRAW|MF in the init sequence, T1b checks promisc high re-inits with MF clear. Mutation-tested: never setting MF fails both. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 14 ++++---- docs/bba_compat_todo.md | 46 ++++++++++++++++++++++-- exi_bba/bba_top.py | 6 ++++ exi_bba/w5100_parallel_master.py | 61 ++++++++++++++++++++++++++++++-- 4 files changed, 117 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7d0c018..03e9eb9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1359,12 +1359,14 @@ Known differences between this implementation and what real GC software expects, with the evidence for each, are tracked in **`docs/bba_compat_todo.md`**. Summary of what is still OPEN: -1. **No hardware RX filtering** — ⚠️ STILL OPEN. We take every frame on the - segment; the real BBA delivers own-MAC + broadcast. Causes the ring - pressure already seen on hardware. Needs a decision, not just code: the - W5100's MF bit filters against `SHAR`, which we write only at init, so a - driver that writes PAR0–5 would leave it stale and MF would drop - everything. +1. ✅ **Hardware RX filtering** — `S0_MR` sets the MACRAW **MF** bit, so the + chip delivers own-MAC + broadcast only; `NCRB.PR` clears it for + promiscuous. Running unfiltered **hung Swiss** on "Initialising Network": + libogc2's `bba_interrupt()` loops until `IR & IMR` is zero, and every + frame on the segment re-set `RI` faster than it could clear them, so it + livelocked. Unplugging the cable let Swiss through, which identified it. + Still to do: refresh `SHAR` on a PAR0–5 write (libogc2 reads the MAC + rather than writing it, so this path is safe today). 2. ✅ **Ring geometry** — `BP`/`RHBP` are honoured, with garbage rejected and `geom_honoured` exposed. A ring must still be larger than `_MAX_FRAME_PAGES` (6) or the worst-case reservation drops every frame. diff --git a/docs/bba_compat_todo.md b/docs/bba_compat_todo.md index c158098..df04f91 100644 --- a/docs/bba_compat_todo.md +++ b/docs/bba_compat_todo.md @@ -25,9 +25,51 @@ The register map referenced throughout is the one in libogc2's `gcif.c` --- -## 1. No hardware RX filtering — **the consequential one** +## 1. No hardware RX filtering — **it hung Swiss** -**Status: OPEN. Needs a decision, not just code.** +**Status: ✅ DONE 2026-10-10** — `S0_MR` now sets the MACRAW **MF** bit, so the +W5100 delivers own-MAC (`SHAR`) + broadcast only, matching what the real BBA +does. `NCRB.PR` (bit 0) clears MF if the GC asks for promiscuous, sampled when +socket 0 is OPENed (i.e. at init, so a later `PR` change takes effect on the +next NCRA reset). + +**How it presented, which is worth knowing because it does not look like a +filtering problem.** Swiss hung forever on "Initialising Network". libogc2's +`bba_interrupt()` is + +```c +ir = bba_in8(BBA_IR); imr = bba_in8(BBA_IMR); status = ir & imr; +while (status) { bba_out8(BBA_IR, status); ... start_rx ...; status = ir & imr; } +``` + +— it only exits when `IR & IMR` reaches zero. Unfiltered, every frame on the +segment set `RI`, which re-asserted faster than the loop could clear it, so it +**never exited**. Not slow: livelocked. Unplugging the ethernet cable let Swiss +get all the way through (then fail DHCP, there being no server), which is what +identified it — a one-minute experiment after a lot of theorising. + +**Why filtering against `SHAR` was safe after all.** The earlier worry was that +MF filters on `SHAR`, which we write only during init, so a driver that WRITES +PAR0–5 would leave it stale and MF would drop everything. libogc2 **reads** the +MAC out of PAR0–5 rather than writing it, so for this path `SHAR` (written from +`reg.par` in the same init) cannot go stale. Broadcast still passes MF, and a +DHCP OFFER unicast to our MAC passes too. + +⚠️ **Still to do for a driver that writes PAR0–5 after init:** refresh `SHAR` +on a PAR write and re-open the socket, otherwise MF filters on the old MAC. +Nothing we have seen does this, but the official SDK driver is unread. + +⚠️ **The W5500 back-end was NOT changed** — it is the alternate path and +unused. If it is ever revived it needs the same treatment (`Sn_MR` MF bit). + +Tests: `w5100_parallel_master` **T1** (the init sequence expects +`S0_MR = MACRAW|MF`) and **T1b** (`promisc` high re-inits with MF clear). +Mutation-tested: never setting MF fails both, with the message naming the +Swiss hang. + +### Original analysis, kept for the reasoning + +**Was: OPEN. Needs a decision, not just code.** The real BBA filters received frames. libogc2 configures `NCRB = AB|CA|2_PACKETS_PER_INT` = `0x52`, i.e. `PR=0` (not promiscuous), diff --git a/exi_bba/bba_top.py b/exi_bba/bba_top.py index dd6b5a4..26ad8c2 100644 --- a/exi_bba/bba_top.py +++ b/exi_bba/bba_top.py @@ -313,6 +313,12 @@ class BBATop(Elaboratable): # ── RX enabled gate (NCRA SR / start-receive bit) ───────────────── # The RX ring-buffer path is active only after the GC sets NCRA[3]. m.d.comb += asm.rx_enabled.eq(reg.ncra_sr) + # NCRB.PR (bit 0) -> MACRAW MAC filter. Low means the ethernet chip + # delivers own-MAC + broadcast only, matching the real BBA; the GC can + # ask for promiscuous by setting PR. See the S0_MODE comment in + # w5100_parallel_master.py -- running unfiltered hung Swiss. + if self._eth == "w5100": + m.d.comb += eth.promisc.eq(reg.ncrb[0]) # ── Optional bring-up peripherals (sync domain) ─────────────────── # Build init_req as an OR of all reinit sources (NCRA pulse plus any diff --git a/exi_bba/w5100_parallel_master.py b/exi_bba/w5100_parallel_master.py index 224e3b9..d29bc66 100644 --- a/exi_bba/w5100_parallel_master.py +++ b/exi_bba/w5100_parallel_master.py @@ -103,6 +103,7 @@ _MR_RST = 0x80 _MR_AI = 0x02 # address auto-increment (indirect mode) _MR_IND = 0x01 # indirect bus interface mode _S0_MR_MACRAW = 0x04 +_S0_MR_MF = 0x40 # MACRAW MAC filter: own-MAC (SHAR) + broadcast only _S1_MR_UDP = 0x02 # socket UDP mode _CR_OPEN = 0x01 _CR_SEND = 0x20 @@ -229,6 +230,11 @@ class W5100ParallelMaster(Elaboratable): self.dbg_rd_data = Signal(8) self.dbg_rd_done = Signal() self.par = Signal(48) # MAC address (PAR0..5 packed) + # NCRB.PR from the GC. Low (the normal case) means MACRAW filters to + # own-MAC + broadcast, which is what the real BBA does -- see the + # comment on the S0_MODE write. Sampled when the socket is OPENed, + # i.e. at init; a later change takes effect on the next NCRA reset. + self.promisc = Signal() # TX stream self.tx_data = Signal(8) @@ -667,7 +673,28 @@ class W5100ParallelMaster(Elaboratable): # RMSR/TMSR = 0x55 (2 KB per socket — default; socket 0 used). write_reg("MEMSZ", _RMSR, [0x55, 0x55], "S0_MODE") # RMSR then TMSR # Socket 0: MACRAW mode, OPEN, enable interrupt. - write_reg("S0_MODE", _S0_MR, [_S0_MR_MACRAW], "S0_OPEN") + # + # ⚠️ MF (MAC filter) IS SET unless the GC asked for promiscuous. + # Without it MACRAW delivers EVERY frame on the segment, which is + # not what the real BBA does: libogc2 configures + # NCRB = AB|CA|2_PACKETS_PER_INT (0x52) -- PR=0, PM=0, AB=1 -- + # i.e. own-MAC + broadcast only. + # + # Unfiltered, this HUNG SWISS. libogc2's bba_interrupt() loops + # while (status) { clear IR; start_rx; status = IR & IMR; } + # which only exits when IR & IMR reaches zero. On a live segment + # RI re-asserted faster than the loop could clear it, so it never + # exited and Swiss sat on "Initialising Network" forever. With + # the cable unplugged it got through, which is what identified it. + # + # Filtering against SHAR is safe here because libogc2 READS the + # MAC out of PAR0-5 rather than writing it, so SHAR (written from + # reg.par during this same init) cannot go stale underneath it. + # A driver that WRITES PAR0-5 after init would need SHAR + # refreshed -- see docs/bba_compat_todo.md item 1. + write_reg("S0_MODE", _S0_MR, + [Mux(self.promisc, _S0_MR_MACRAW, + _S0_MR_MACRAW | _S0_MR_MF)], "S0_OPEN") write_reg("S0_OPEN", _S0_CR, [_CR_OPEN], "S0_IMR") # After S0 IMR, either finish (plain BBA) or configure the IP stack # and open socket 1 in UDP mode (bring-up test build). @@ -1011,7 +1038,7 @@ if __name__ == "__main__": (_SHAR0 + 0, MAC[0]), (_SHAR0 + 1, MAC[1]), (_SHAR0 + 2, MAC[2]), (_SHAR0 + 3, MAC[3]), (_SHAR0 + 4, MAC[4]), (_SHAR0 + 5, MAC[5]), (_RMSR + 0, 0x55), (_RMSR + 1, 0x55), - (_S0_MR, _S0_MR_MACRAW), + (_S0_MR, _S0_MR_MACRAW | _S0_MR_MF), # MF set: own-MAC + broadcast (_S0_CR, _CR_OPEN), (_IMR, 0x01), ] @@ -1100,6 +1127,36 @@ if __name__ == "__main__": print("T1 init sequence matches expected (MR, SHAR, mem sizes, " "S0 MACRAW/OPEN, IMR)") + # T1b: NCRB.PR -> promiscuous, i.e. MF CLEARED. + # + # The default (MF set) filters to own-MAC + broadcast, matching the + # real BBA. Running unfiltered hung Swiss: libogc2's bba_interrupt() + # loops until IR & IMR reaches zero, and on a live segment RI + # re-asserted faster than the loop could clear it. But the GC can ask + # for promiscuous via NCRB bit 0, and then it must actually get it. + s0_default = [v for a, v in writes if a == _S0_MR] + writes.clear() + ctx.set(dut.promisc, 1) + ctx.set(dut.init_req, 1) + await ctx.tick("sync").repeat(1) + ctx.set(dut.init_req, 0) + for _ in range(4000): + await ctx.tick("sync").repeat(1) + if ctx.get(dut.init_done): + break + s0_promisc = [v for a, v in writes if a == _S0_MR] + print(f"T1b S0_MR: default={[hex(v) for v in s0_default]} " + f"promisc={[hex(v) for v in s0_promisc]}") + if s0_default != [_S0_MR_MACRAW | _S0_MR_MF]: + errors.append(f"T1b default S0_MR {s0_default}, want MACRAW|MF " + f"- unfiltered MACRAW hangs Swiss") + if s0_promisc != [_S0_MR_MACRAW]: + errors.append(f"T1b promiscuous S0_MR {s0_promisc}, want MACRAW " + f"with MF clear") + ctx.set(dut.promisc, 0) + writes.clear() + writes.extend(EXPECTED) # restore for any later comparison + # ── helper: stream one TX frame through the external tx interface ───── async def feed_frame(ctx, frame): for i, b in enumerate(frame):