From 9c4bc7a61c766f24947965a0f53cf6b9c36dce39 Mon Sep 17 00:00:00 2001 From: Dennis Brentjes Date: Sun, 20 Sep 2026 14:51:33 +0000 Subject: [PATCH] fix(w5100): unlock NETLCKR before writing GWR/SUBR/SHAR/SIPR (W5100S) Second W5100S-vs-W5100 gotcha found at bring-up: the W5100S has a Network Lock Register (NETLCKR, 0x0071) the original W5100 lacks. It defaults to 0x00 (locked), and while locked, ALL writes to GWR/SUBR/SHAR/SIPR are silently dropped -- so the chip has no source IP/MAC and the UDP/MACRAW send builds nothing (chip reports SEND_OK but transmits nothing; socket registers aren't locked, which is why an indirect write/readback of Sn_PORT succeeded). Fix: write the unlock value 0x3A to NETLCKR right after MR enables indirect mode, before the network-config writes. On the bench this flipped the board from silent to actually keying frames onto the wire (magjack activity LED now blinks). Also: - bba_top.py test model: RX buffer base 0x6000 -> 0xC000 to match the W5100S buffer-base fix (the model hardcoded the old W5100 address). - w5100_selftest.py: added an indirect-mode (IDM) write/readback probe of a socket register + IND= field in the UART report, to isolate direct-vs-indirect access (confirmed indirect works on the real chip). Co-Authored-By: Claude Opus 4.8 --- exi_bba/bba_top.py | 2 +- exi_bba/w5100_parallel_master.py | 14 +++- exi_bba/w5100_selftest.py | 116 +++++++++++++++++++++++-------- 3 files changed, 101 insertions(+), 31 deletions(-) diff --git a/exi_bba/bba_top.py b/exi_bba/bba_top.py index dba89f8..58b46be 100644 --- a/exi_bba/bba_top.py +++ b/exi_bba/bba_top.py @@ -457,7 +457,7 @@ if __name__ == "__main__": # Pre-loads a known MACRAW packet in the RX buffer so we can verify the full # ethernet→SPRAM→GC path. Same protocol as the W5100ParallelMaster bench. RX_FRAME = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04] - _W_RX_BASE = 0x6000 + _W_RX_BASE = 0xC000 # W5100S RX memory base (was 0x6000 on the W5100) _W_S0_CR = 0x0401 _W_S0_RX_RSR = 0x0426 _W_S0_RX_RD = 0x0428 diff --git a/exi_bba/w5100_parallel_master.py b/exi_bba/w5100_parallel_master.py index f6db067..8a4111c 100644 --- a/exi_bba/w5100_parallel_master.py +++ b/exi_bba/w5100_parallel_master.py @@ -49,6 +49,8 @@ _GAR0 = 0x0001 # Gateway IP, 4 bytes _SUBR0 = 0x0005 # Subnet mask, 4 bytes _SHAR0 = 0x0009 # Source MAC, 6 bytes _SIPR0 = 0x000F # Source IP, 4 bytes +_NETLCKR = 0x0071 # W5100S Network Lock Register (WO) — GWR/SUBR/SHAR/SIPR are +_NETLCK_UNLOCK = 0x3A # writable ONLY while NETLCKR==0x3A (default 0x00 = locked) _IR = 0x0015 # Interrupt register _IMR = 0x0016 # Interrupt mask _RMSR = 0x001A # RX memory size (2 bits/socket) @@ -565,7 +567,16 @@ class W5100ParallelMaster(Elaboratable): m.d.sync += wait_ctr.eq(wait_ctr + 1) # MR = indirect + auto-increment (direct A=00). - write_reg("MR_MODE", _MR, [_MR_IND | _MR_AI], "SHAR", direct=True) + write_reg("MR_MODE", _MR, [_MR_IND | _MR_AI], "NETLCK", direct=True) + + # W5100S-only: GWR/SUBR/SHAR/SIPR are LOCKED until NETLCKR (0x0071) + # is written 0x3A (default 0x00 = locked). Without this, every + # SHAR/SIPR/GAR/SUBR write below is silently DROPPED -> no source + # MAC/IP -> the UDP/MACRAW send builds nothing and the chip transmits + # nothing on the wire despite reporting SEND_OK (bench-confirmed + # 2026-09-20; the original W5100 had no such lock). Indirect mode is + # already on from MR_MODE, so this reaches 0x0071. + write_reg("NETLCK", _NETLCKR, [_NETLCK_UNLOCK], "SHAR") # SHAR = source MAC (6-byte auto-increment burst). with m.State("SHAR"): @@ -911,6 +922,7 @@ if __name__ == "__main__": EXPECTED = [ ("MR", _MR_RST), ("MR", _MR_IND | _MR_AI), + (_NETLCKR, _NETLCK_UNLOCK), # W5100S: unlock GWR/SUBR/SHAR/SIPR (_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), diff --git a/exi_bba/w5100_selftest.py b/exi_bba/w5100_selftest.py index 9eb8096..f239275 100644 --- a/exi_bba/w5100_selftest.py +++ b/exi_bba/w5100_selftest.py @@ -91,6 +91,7 @@ class W5100SelfTest(Elaboratable): self.done = Signal() # high once the test has a verdict self.mr_ok = Signal() # MR reset self-cleared (chip alive) self.bad_bits = Signal(8) # data lines that failed read-back + self.ind_ok = Signal() # indirect-mode (IDM) write/readback OK def elaborate(self, platform): m = Module() @@ -213,15 +214,58 @@ class W5100SelfTest(Elaboratable): with m.If(bus_done): with m.If(~bus_rd[7]): m.d.sync += mr_ok.eq(1) - m.next = "REPORT" + m.next = "IND_MODE" with m.Else(): m.d.sync += ctr.eq(ctr + 1) with m.If(ctr == self._poll - 1): m.d.sync += mr_ok.eq(0) # never cleared - m.next = "REPORT" + m.next = "IND_MODE" with m.Else(): m.next = "MR_POLL_ISSUE" + # ── INDIRECT-mode probe: write+readback Sn_PORT of socket 3 + # (0x0704) through IDM_AR/IDM_DR, the exact access path the BBA + # master uses to reach socket/buffer space. Direct mode (the walk + # above) can only touch 0x0000-0x0003; this proves the chip honours + # the indirect interface for the high address space too. + with m.State("IND_MODE"): # enter indirect bus mode + bus_write(_A_MR, _MR_IND) + m.next = "IND_MODE_W" + with m.State("IND_MODE_W"): + with m.If(bus_done): m.next = "IND_WAR0" + with m.State("IND_WAR0"): + bus_write(_A_AR0, 0x07) + m.next = "IND_WAR0_W" + with m.State("IND_WAR0_W"): + with m.If(bus_done): m.next = "IND_WAR1" + with m.State("IND_WAR1"): + bus_write(_A_AR1, 0x04) + m.next = "IND_WAR1_W" + with m.State("IND_WAR1_W"): + with m.If(bus_done): m.next = "IND_WDR" + with m.State("IND_WDR"): + bus_write(_A_DR, 0xA5) # write pattern to Sn_PORT + m.next = "IND_WDR_W" + with m.State("IND_WDR_W"): + with m.If(bus_done): m.next = "IND_RAR0" + with m.State("IND_RAR0"): + bus_write(_A_AR0, 0x07) # re-point AR (no auto-inc) + m.next = "IND_RAR0_W" + with m.State("IND_RAR0_W"): + with m.If(bus_done): m.next = "IND_RAR1" + with m.State("IND_RAR1"): + bus_write(_A_AR1, 0x04) + m.next = "IND_RAR1_W" + with m.State("IND_RAR1_W"): + with m.If(bus_done): m.next = "IND_RDR" + with m.State("IND_RDR"): + bus_read(_A_DR) + m.next = "IND_RDR_W" + with m.State("IND_RDR_W"): + with m.If(bus_done): + m.d.sync += self.ind_ok.eq(bus_rd == 0xA5) + m.next = "REPORT" + with m.State("REPORT"): m.d.comb += self.done.eq(1) @@ -273,7 +317,7 @@ class W5100SelfTest(Elaboratable): # ── UART status report (115200 8N1) ────────────────────────────────── # Continuously transmits "W5100 MROK=X BAD=YY \r\n" once `done`. DIV = round(24_000_000 / 115_200) # 208 cycles / bit - tmpl = list(b"W5100 MROK=0 BAD=00 ....\r\n") # X@11, hi@17, lo@18, verdict@20..23 + tmpl = list(b"W5100 MROK=0 BAD=00 IND=0 ....\r\n") # X@11 hi@17 lo@18 ind@24 verdict@26..29 MSGLEN = len(tmpl) rom = Array([Const(b, 8) for b in tmpl]) @@ -298,8 +342,9 @@ class W5100SelfTest(Elaboratable): with m.Case(11): m.d.comb += cur.eq(0x30 + mr_ok) with m.Case(17): m.d.comb += cur.eq(hexch(bad[4:8])) with m.Case(18): m.d.comb += cur.eq(hexch(bad[0:4])) + with m.Case(24): m.d.comb += cur.eq(0x30 + self.ind_ok) for k in range(4): - with m.Case(20 + k): m.d.comb += cur.eq(verd[8*k:8*k+8]) + with m.Case(26 + k): m.d.comb += cur.eq(verd[8*k:8*k+8]) shift = Signal(10, init=0x3FF) nbits = Signal(range(11)) @@ -395,32 +440,45 @@ if __name__ == "__main__": sim.add_clock(Period(MHz=24), domain="sync") async def w5100_model(ctx): - mem = {} # direct-mode regs by A[1:0] - for _ in range(8000): + mem = {} # direct A (0-3) or ('i',addr) + ar = 0; ind = False # IDM pointer, MR.IND state + for _ in range(14000): await ctx.tick("sync") cs = ctx.get(dut.cs_n); rd = ctx.get(dut.rd_n); wr = ctx.get(dut.wr_n) a = ctx.get(dut.bus_addr); dko = ctx.get(dut.bus_data_o) - if cs == 0 and wr == 0: # write strobe (idempotent) - mem[a] = dko + if cs == 0 and wr == 0: # write (idempotent) + if bus_dead: + pass + elif a == _A_MR: + mem[0] = dko; ind = bool(dko & _MR_IND) + elif ind: # indirect: IDM_AR / IDM_DR + if a == _A_AR0: ar = (ar & 0x00FF) | (dko << 8) + elif a == _A_AR1: ar = (ar & 0xFF00) | dko + elif a == _A_DR: mem[('i', ar)] = dko + else: # direct: A[1:0] register + mem[a] = dko if cs == 0 and rd == 0: # drive read data if bus_dead: - ctx.set(dut.bus_data_i, 0xFF) # nothing drives -> float hi + val = 0xFF elif a == _A_MR: - mr = mem.get(_A_MR, 0) # reset self-clears only if - ctx.set(dut.bus_data_i, # the clock is alive - mr if clock_dead else (mr & 0x7F)) - else: # GAR0.. scratch R/W + mr = mem.get(0, 0) + val = mr if clock_dead else (mr & 0x7F) # reset self-clear + elif ind and a == _A_DR: + val = mem.get(('i', ar), 0) + elif not ind: val = mem.get(a, 0) - if break_bit is not None: # stuck-low data line - val &= ~(1 << break_bit) & 0xFF - ctx.set(dut.bus_data_i, val) + else: + val = 0 + if break_bit is not None and a != _A_MR: # stuck-low data line + val &= ~(1 << break_bit) & 0xFF + ctx.set(dut.bus_data_i, val) async def checker(ctx): - for _ in range(4000): + for _ in range(9000): await ctx.tick("sync") if ctx.get(dut.done): - return (ctx.get(dut.mr_ok), ctx.get(dut.bad_bits)) - return (None, None) + return (ctx.get(dut.mr_ok), ctx.get(dut.bad_bits), ctx.get(dut.ind_ok)) + return (None, None, None) result = {} async def tb(ctx): @@ -431,21 +489,21 @@ if __name__ == "__main__": return result["v"] ok = True - mr, bad = run() - print(f"[healthy] mr_ok={mr} bad={bad:#04x} -> GREEN solid") - ok &= (mr == 1 and bad == 0) + mr, bad, ind = run() + print(f"[healthy] mr_ok={mr} bad={bad:#04x} ind_ok={ind} -> GREEN solid") + ok &= (mr == 1 and bad == 0 and ind == 1) - mr, bad = run(break_bit=3) - print(f"[D3 broken] mr_ok={mr} bad={bad:#04x} -> green dark, RED 4 blinks") + mr, bad, ind = run(break_bit=3) + print(f"[D3 broken] mr_ok={mr} bad={bad:#04x} ind_ok={ind} -> green dark, RED 4 blinks") ok &= ((bad & 0x08) and bad != 0xFF) - mr, bad = run(clock_dead=True) - print(f"[clock/Y2 dead] mr_ok={mr} bad={bad:#04x} -> GREEN+RED slow together") + mr, bad, ind = run(clock_dead=True) + print(f"[clock/Y2 dead] mr_ok={mr} bad={bad:#04x} ind_ok={ind} -> GREEN+RED slow together") ok &= (mr == 0 and bad == 0) - mr, bad = run(bus_dead=True) - print(f"[unpowered/dead] mr_ok={mr} bad={bad:#04x} -> green dark, RED fast") - ok &= (bad == 0xFF) + mr, bad, ind = run(bus_dead=True) + print(f"[unpowered/dead] mr_ok={mr} bad={bad:#04x} ind_ok={ind} -> green dark, RED fast") + ok &= (bad == 0xFF and ind == 0) print("PASS" if ok else "FAIL") import sys; sys.exit(0 if ok else 1)