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):