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 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 14:51:17 +02:00
parent 90a583dd46
commit 2893e7f6fb
4 changed files with 117 additions and 10 deletions
+59 -2
View File
@@ -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):