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
+87 -29
View File
@@ -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 <verdict>\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)