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 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 14:51:33 +00:00
parent 59948812f3
commit 9c4bc7a61c
3 changed files with 101 additions and 31 deletions
+1 -1
View File
@@ -457,7 +457,7 @@ if __name__ == "__main__":
# Pre-loads a known MACRAW packet in the RX buffer so we can verify the full # 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. # ethernet→SPRAM→GC path. Same protocol as the W5100ParallelMaster bench.
RX_FRAME = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04] 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_CR = 0x0401
_W_S0_RX_RSR = 0x0426 _W_S0_RX_RSR = 0x0426
_W_S0_RX_RD = 0x0428 _W_S0_RX_RD = 0x0428
+13 -1
View File
@@ -49,6 +49,8 @@ _GAR0 = 0x0001 # Gateway IP, 4 bytes
_SUBR0 = 0x0005 # Subnet mask, 4 bytes _SUBR0 = 0x0005 # Subnet mask, 4 bytes
_SHAR0 = 0x0009 # Source MAC, 6 bytes _SHAR0 = 0x0009 # Source MAC, 6 bytes
_SIPR0 = 0x000F # Source IP, 4 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 _IR = 0x0015 # Interrupt register
_IMR = 0x0016 # Interrupt mask _IMR = 0x0016 # Interrupt mask
_RMSR = 0x001A # RX memory size (2 bits/socket) _RMSR = 0x001A # RX memory size (2 bits/socket)
@@ -565,7 +567,16 @@ class W5100ParallelMaster(Elaboratable):
m.d.sync += wait_ctr.eq(wait_ctr + 1) m.d.sync += wait_ctr.eq(wait_ctr + 1)
# MR = indirect + auto-increment (direct A=00). # 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). # SHAR = source MAC (6-byte auto-increment burst).
with m.State("SHAR"): with m.State("SHAR"):
@@ -911,6 +922,7 @@ if __name__ == "__main__":
EXPECTED = [ EXPECTED = [
("MR", _MR_RST), ("MR", _MR_RST),
("MR", _MR_IND | _MR_AI), ("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 + 0, MAC[0]), (_SHAR0 + 1, MAC[1]), (_SHAR0 + 2, MAC[2]),
(_SHAR0 + 3, MAC[3]), (_SHAR0 + 4, MAC[4]), (_SHAR0 + 5, MAC[5]), (_SHAR0 + 3, MAC[3]), (_SHAR0 + 4, MAC[4]), (_SHAR0 + 5, MAC[5]),
(_RMSR + 0, 0x55), (_RMSR + 1, 0x55), (_RMSR + 0, 0x55), (_RMSR + 1, 0x55),
+87 -29
View File
@@ -91,6 +91,7 @@ class W5100SelfTest(Elaboratable):
self.done = Signal() # high once the test has a verdict self.done = Signal() # high once the test has a verdict
self.mr_ok = Signal() # MR reset self-cleared (chip alive) self.mr_ok = Signal() # MR reset self-cleared (chip alive)
self.bad_bits = Signal(8) # data lines that failed read-back 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): def elaborate(self, platform):
m = Module() m = Module()
@@ -213,15 +214,58 @@ class W5100SelfTest(Elaboratable):
with m.If(bus_done): with m.If(bus_done):
with m.If(~bus_rd[7]): with m.If(~bus_rd[7]):
m.d.sync += mr_ok.eq(1) m.d.sync += mr_ok.eq(1)
m.next = "REPORT" m.next = "IND_MODE"
with m.Else(): with m.Else():
m.d.sync += ctr.eq(ctr + 1) m.d.sync += ctr.eq(ctr + 1)
with m.If(ctr == self._poll - 1): with m.If(ctr == self._poll - 1):
m.d.sync += mr_ok.eq(0) # never cleared m.d.sync += mr_ok.eq(0) # never cleared
m.next = "REPORT" m.next = "IND_MODE"
with m.Else(): with m.Else():
m.next = "MR_POLL_ISSUE" 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"): with m.State("REPORT"):
m.d.comb += self.done.eq(1) m.d.comb += self.done.eq(1)
@@ -273,7 +317,7 @@ class W5100SelfTest(Elaboratable):
# ── UART status report (115200 8N1) ────────────────────────────────── # ── UART status report (115200 8N1) ──────────────────────────────────
# Continuously transmits "W5100 MROK=X BAD=YY <verdict>\r\n" once `done`. # Continuously transmits "W5100 MROK=X BAD=YY <verdict>\r\n" once `done`.
DIV = round(24_000_000 / 115_200) # 208 cycles / bit 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) MSGLEN = len(tmpl)
rom = Array([Const(b, 8) for b in 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(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(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(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): 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) shift = Signal(10, init=0x3FF)
nbits = Signal(range(11)) nbits = Signal(range(11))
@@ -395,32 +440,45 @@ if __name__ == "__main__":
sim.add_clock(Period(MHz=24), domain="sync") sim.add_clock(Period(MHz=24), domain="sync")
async def w5100_model(ctx): async def w5100_model(ctx):
mem = {} # direct-mode regs by A[1:0] mem = {} # direct A (0-3) or ('i',addr)
for _ in range(8000): ar = 0; ind = False # IDM pointer, MR.IND state
for _ in range(14000):
await ctx.tick("sync") await ctx.tick("sync")
cs = ctx.get(dut.cs_n); rd = ctx.get(dut.rd_n); wr = ctx.get(dut.wr_n) 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) a = ctx.get(dut.bus_addr); dko = ctx.get(dut.bus_data_o)
if cs == 0 and wr == 0: # write strobe (idempotent) if cs == 0 and wr == 0: # write (idempotent)
mem[a] = dko 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 cs == 0 and rd == 0: # drive read data
if bus_dead: if bus_dead:
ctx.set(dut.bus_data_i, 0xFF) # nothing drives -> float hi val = 0xFF
elif a == _A_MR: elif a == _A_MR:
mr = mem.get(_A_MR, 0) # reset self-clears only if mr = mem.get(0, 0)
ctx.set(dut.bus_data_i, # the clock is alive val = mr if clock_dead else (mr & 0x7F) # reset self-clear
mr if clock_dead else (mr & 0x7F)) elif ind and a == _A_DR:
else: # GAR0.. scratch R/W val = mem.get(('i', ar), 0)
elif not ind:
val = mem.get(a, 0) val = mem.get(a, 0)
if break_bit is not None: # stuck-low data line else:
val &= ~(1 << break_bit) & 0xFF val = 0
ctx.set(dut.bus_data_i, val) 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): async def checker(ctx):
for _ in range(4000): for _ in range(9000):
await ctx.tick("sync") await ctx.tick("sync")
if ctx.get(dut.done): if ctx.get(dut.done):
return (ctx.get(dut.mr_ok), ctx.get(dut.bad_bits)) return (ctx.get(dut.mr_ok), ctx.get(dut.bad_bits), ctx.get(dut.ind_ok))
return (None, None) return (None, None, None)
result = {} result = {}
async def tb(ctx): async def tb(ctx):
@@ -431,21 +489,21 @@ if __name__ == "__main__":
return result["v"] return result["v"]
ok = True ok = True
mr, bad = run() mr, bad, ind = run()
print(f"[healthy] mr_ok={mr} bad={bad:#04x} -> GREEN solid") print(f"[healthy] mr_ok={mr} bad={bad:#04x} ind_ok={ind} -> GREEN solid")
ok &= (mr == 1 and bad == 0) ok &= (mr == 1 and bad == 0 and ind == 1)
mr, bad = run(break_bit=3) mr, bad, ind = run(break_bit=3)
print(f"[D3 broken] mr_ok={mr} bad={bad:#04x} -> green dark, RED 4 blinks") print(f"[D3 broken] mr_ok={mr} bad={bad:#04x} ind_ok={ind} -> green dark, RED 4 blinks")
ok &= ((bad & 0x08) and bad != 0xFF) ok &= ((bad & 0x08) and bad != 0xFF)
mr, bad = run(clock_dead=True) mr, bad, ind = run(clock_dead=True)
print(f"[clock/Y2 dead] mr_ok={mr} bad={bad:#04x} -> GREEN+RED slow together") print(f"[clock/Y2 dead] mr_ok={mr} bad={bad:#04x} ind_ok={ind} -> GREEN+RED slow together")
ok &= (mr == 0 and bad == 0) ok &= (mr == 0 and bad == 0)
mr, bad = run(bus_dead=True) mr, bad, ind = run(bus_dead=True)
print(f"[unpowered/dead] mr_ok={mr} bad={bad:#04x} -> green dark, RED fast") print(f"[unpowered/dead] mr_ok={mr} bad={bad:#04x} ind_ok={ind} -> green dark, RED fast")
ok &= (bad == 0xFF) ok &= (bad == 0xFF and ind == 0)
print("PASS" if ok else "FAIL") print("PASS" if ok else "FAIL")
import sys; sys.exit(0 if ok else 1) import sys; sys.exit(0 if ok else 1)