Files
rebbarb/exi_bba/w5100_selftest.py
T
Roflin 9c4bc7a61c 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>
2026-09-20 14:51:33 +00:00

510 lines
23 KiB
Python

"""Standalone W5100 <-> FPGA bus connection self-test (bring-up diagnostic).
Flashed on its own (NOT the BBA design), this exercises ONLY the parallel bus
between the iCE40 and the WIZnet W5100 and reports the result on the two
discrete LEDs — no GameCube, no PC, no UART needed, just look at the board:
The test runs in two stages so it can tell a bad bus apart from a dead clock:
1. a data-bus walk (write/read-back patterns to the scratch register) — this
is a purely ASYNCHRONOUS host-register access, so it works even if the
25 MHz crystal is dead; it validates D0..D7 + the CS/RD/WR/address path.
2. an MR software-reset self-clear poll — this DOES need the 25 MHz clock.
Four distinct LED verdicts (GREEN=D6, RED=D7):
GREEN solid, red dark = ALL GOOD (bus + all 8 data lines +
the W5100 clock all verified).
green dark, RED blinks N times = data line D(N-1) is stuck/broken
(e.g. 4 blinks = D3). Reports the
lowest bad line; fix & re-run.
green dark, RED fast continuous = the W5100 does not respond on the bus
at all (read-back never tracked the
write) -> dead/unpowered chip, or the
CS/RD/WR/address path is broken.
GREEN+RED blink together (~2 Hz) = the bus is FINE (data walk passed) but
the MR reset never self-cleared -> the
25 MHz clock isn't running: suspect the
Y2 crystal / its load caps / solder.
Why this and not the BBA build: the BBA W5100 init is write-only with a fixed
settle wait, so it "completes" even on a dead bus. A real connection test must
READ BACK; the MR self-clear poll is the canonical W5100 liveness read.
Reuses the exact indirect-bus (IDM) access engine timing from
`w5100_parallel_master.py`. Single `sync` domain (24 MHz HFOSC) — no PLL/
capture domain, so it place-and-routes with huge timing margin on every seed.
Run: python -m exi_bba.w5100_selftest # self-test in simulation
Build/flash via synth.py (W5100_SELFTEST=1), see that file.
"""
from amaranth import *
# W5100 register addresses (indirect-mode common block).
_MR = 0x0000 # Mode Register (reachable directly at A=00)
_MR_RST = 0x80 # MR reset bit (self-clears when reset completes)
_MR_IND = 0x01 # MR indirect-bus-mode enable
_SCRATCH = 0x0001 # GAR0 (gateway addr byte 0): plain R/W, harmless scratch
# A[1:0] indirect-mode selects.
_A_MR = 0b00 # Mode Register (direct)
_A_AR0 = 0b01 # IDM address high
_A_AR1 = 0b10 # IDM address low
_A_DR = 0b11 # IDM data (auto-inc only if MR.AI=1; here it is NOT)
# Walking-1s then 0xAA/0x55 — catches stuck-high, stuck-low, and (partly)
# shorted-adjacent data lines on read-back of the scratch register.
_PATTERNS = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0xAA, 0x55]
class W5100SelfTest(Elaboratable):
"""Drives the W5100 indirect bus and reports on led_g / led_r.
Timing params default to real-hardware values (24 MHz sync); the testbench
passes tiny ones so the FSM runs in a few hundred ticks.
"""
def __init__(self, *, strobe_cycles=3, rst_cycles=48_000, boot_cycles=2_040_000,
poll_timeout=48_000, blink_cycles=3_000_000):
self._strobe = strobe_cycles
self._rst = rst_cycles # RST_N low pulse (~2 ms @24MHz)
self._boot = boot_cycles # post-reset boot (~85 ms > 60.3 ms W5100S spec)
self._poll = poll_timeout # MR self-clear wait (~2 ms)
self._blink = blink_cycles # LED blink tick (~0.125 s)
# Physical W5100 bus (wired to the platform's `w5100` resource).
self.bus_addr = Signal(2)
self.bus_data_o = Signal(8)
self.bus_data_oe = Signal()
self.bus_data_i = Signal(8)
self.cs_n = Signal(init=1)
self.rd_n = Signal(init=1)
self.wr_n = Signal(init=1)
self.rst_n = Signal(init=1)
# Status LEDs.
self.led_g = Signal()
self.led_r = Signal()
# UART status report (115200 8N1), so a host can read the verdict too.
self.uart_tx = Signal(init=1)
# Observability (simulation checks these instead of the LED blink).
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()
STROBE = self._strobe
# ── one indirect-bus read/write cycle (same engine as the master) ────
bus_go = Signal()
bus_rw = Signal() # 1 = write, 0 = read
bus_a = Signal(2)
bus_wd = Signal(8)
bus_rd = Signal(8)
bus_done = Signal()
bus_ctr = Signal(range(STROBE + 2))
rw_r = Signal()
a_o = Signal(2)
d_o = Signal(8)
d_oe = Signal()
cs_r = Signal(init=1)
rd_r = Signal(init=1)
wr_r = Signal(init=1)
rstn = Signal(init=1)
m.d.comb += [
self.bus_addr.eq(a_o), self.bus_data_o.eq(d_o), self.bus_data_oe.eq(d_oe),
self.cs_n.eq(cs_r), self.rd_n.eq(rd_r), self.wr_n.eq(wr_r), self.rst_n.eq(rstn),
]
m.d.sync += bus_done.eq(0)
with m.FSM(domain="sync", name="bus_fsm"):
with m.State("IDLE"):
m.d.sync += [cs_r.eq(1), rd_r.eq(1), wr_r.eq(1), d_oe.eq(0)]
with m.If(bus_go):
m.d.sync += [a_o.eq(bus_a), rw_r.eq(bus_rw), cs_r.eq(0), bus_ctr.eq(0)]
with m.If(bus_rw):
m.d.sync += [d_o.eq(bus_wd), d_oe.eq(1), wr_r.eq(0)]
with m.Else():
m.d.sync += rd_r.eq(0)
m.next = "STROBE"
with m.State("STROBE"):
m.d.sync += bus_ctr.eq(bus_ctr + 1)
with m.If(bus_ctr == STROBE - 1):
with m.If(~rw_r):
m.d.sync += bus_rd.eq(self.bus_data_i)
m.d.sync += [rd_r.eq(1), wr_r.eq(1)]
m.next = "FINISH"
with m.State("FINISH"):
m.d.sync += [cs_r.eq(1), d_oe.eq(0), bus_done.eq(1)]
m.next = "IDLE"
# ── test sequencer ───────────────────────────────────────────────────
m.d.comb += [bus_go.eq(0), bus_rw.eq(0), bus_a.eq(0), bus_wd.eq(0)]
def bus_write(a, data):
m.d.comb += [bus_go.eq(1), bus_rw.eq(1), bus_a.eq(a), bus_wd.eq(data)]
def bus_read(a):
m.d.comb += [bus_go.eq(1), bus_rw.eq(0), bus_a.eq(a)]
ctr = Signal(range(max(self._rst, self._boot, self._poll) + 2))
pat_idx = Signal(range(len(_PATTERNS)))
patterns = Array([Const(p, 8) for p in _PATTERNS])
mr_ok = self.mr_ok
bad = self.bad_bits
# The scratch register (GAR0 = 0x0001) is reachable in DIRECT mode at
# A[1:0]=01, because the board ties A[14:2]=0 and wires only A[1:0]. So
# the whole data-bus walk runs WITHOUT setting indirect mode and WITHOUT
# the 25 MHz clock — the host register interface is asynchronous. That
# is exactly what lets us tell a broken bus/data-line apart from a dead
# crystal: the walk works in both, only the MR self-clear needs the clock.
_A_SCRATCH = _SCRATCH & 0b11 # 0x0001 -> A=01 (GAR0, direct)
with m.FSM(domain="sync", name="test_fsm"):
# Hardware-reset the W5100: RST_N low, then release and let it boot.
with m.State("RST_LOW"):
m.d.sync += [rstn.eq(0), ctr.eq(ctr + 1)]
with m.If(ctr == self._rst - 1):
m.d.sync += ctr.eq(0)
m.next = "BOOT"
with m.State("BOOT"):
m.d.sync += [rstn.eq(1), ctr.eq(ctr + 1)]
with m.If(ctr == self._boot - 1):
m.d.sync += [ctr.eq(0), pat_idx.eq(0)]
m.next = "WR_ISSUE"
# ── clock-INDEPENDENT data-bus walk (direct access to GAR0) ──────
# For each pattern: write it to the scratch reg, read it back, OR any
# mismatched bits into `bad`. Runs even with a dead crystal.
with m.State("WR_ISSUE"):
bus_write(_A_SCRATCH, patterns[pat_idx])
m.next = "WR_WAIT"
with m.State("WR_WAIT"):
with m.If(bus_done):
m.next = "RD_ISSUE"
with m.State("RD_ISSUE"):
bus_read(_A_SCRATCH)
m.next = "RD_WAIT"
with m.State("RD_WAIT"):
with m.If(bus_done):
m.d.sync += bad.eq(bad | (bus_rd ^ patterns[pat_idx]))
with m.If(pat_idx == len(_PATTERNS) - 1):
m.d.sync += ctr.eq(0)
m.next = "MR_RST_ISSUE"
with m.Else():
m.d.sync += pat_idx.eq(pat_idx + 1)
m.next = "WR_ISSUE"
# ── clock-DEPENDENT liveness: MR software-reset self-clear ────────
# Software reset: write MR=0x80, then poll MR until the reset bit
# self-clears. That only happens if the 25 MHz clock is running.
with m.State("MR_RST_ISSUE"):
bus_write(_A_MR, _MR_RST)
m.next = "MR_RST_WAIT"
with m.State("MR_RST_WAIT"):
with m.If(bus_done):
m.d.sync += ctr.eq(0)
m.next = "MR_POLL_ISSUE"
with m.State("MR_POLL_ISSUE"):
bus_read(_A_MR)
m.next = "MR_POLL_WAIT"
with m.State("MR_POLL_WAIT"):
with m.If(bus_done):
with m.If(~bus_rd[7]):
m.d.sync += mr_ok.eq(1)
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 = "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)
# ── LED reporting (free-running, only meaningful once done) ───────────
blink_ctr = Signal(range(self._blink + 1))
tick = Signal()
m.d.comb += tick.eq(blink_ctr == self._blink - 1)
m.d.sync += blink_ctr.eq(Mux(tick, 0, blink_ctr + 1))
# lowest failing data-line index (0..7); N blinks = index+1.
low = Signal(range(8))
for i in reversed(range(8)): # last write wins -> lowest set bit
with m.If(bad[i]):
m.d.comb += low.eq(i)
# N-blink pattern: phase 0..(2N-1) toggles the LED (N pulses), then a
# PAUSE of dark phases, repeating. Phase advances one step per tick.
PAUSE = 4
nblink = Signal(range(9))
m.d.comb += nblink.eq(low + 1)
span = Signal(range(2 * 8 + PAUSE + 1))
m.d.comb += span.eq((nblink << 1) + PAUSE)
phase = Signal(range(2 * 8 + PAUSE + 1))
with m.If(self.done & tick):
m.d.sync += phase.eq(Mux(phase >= span - 1, 0, phase + 1))
red_code = Signal() # blinks index+1 times, then pauses
m.d.comb += red_code.eq((phase < (nblink << 1)) & ~phase[0])
red_fast = Signal() # fast continuous blink (~8 Hz)
m.d.comb += red_fast.eq(blink_ctr < (self._blink >> 1))
# slow square (~2 Hz) for the "clock dead" code — green AND red together
slow_ctr = Signal(2)
with m.If(self.done & tick):
m.d.sync += slow_ctr.eq(slow_ctr + 1)
sq_slow = Signal()
m.d.comb += sq_slow.eq(slow_ctr[1])
with m.If(self.done):
with m.If(bad == 0xFF): # reads never tracked writes: nothing
m.d.comb += [self.led_g.eq(0), self.led_r.eq(red_fast)] # on the bus
with m.Elif(bad != 0): # bus works, one/some data lines bad
m.d.comb += [self.led_g.eq(0), self.led_r.eq(red_code)] # -> D(N-1)
with m.Elif(~mr_ok): # bus OK but MR reset never self-cleared
m.d.comb += [self.led_g.eq(sq_slow), self.led_r.eq(sq_slow)] # clock
with m.Else(): # everything good
m.d.comb += [self.led_g.eq(1), self.led_r.eq(0)]
# ── 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 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])
def hexch(nib):
return Mux(nib < 10, 0x30 + nib, 0x37 + nib) # 0-9 / A-F
# four-char verdict word at positions 20..23
verd = Signal(32) # 4 bytes, [20],[21],[22],[23] low->high
with m.If(self.done & (bad == 0xFF)):
m.d.comb += verd.eq(int.from_bytes(b"NORE", "little"))
with m.Elif(self.done & (bad != 0)):
m.d.comb += verd.eq(int.from_bytes(b"DBUS", "little"))
with m.Elif(self.done & ~mr_ok):
m.d.comb += verd.eq(int.from_bytes(b"CLK!", "little"))
with m.Else():
m.d.comb += verd.eq(int.from_bytes(b"GOOD", "little"))
uidx = Signal(range(MSGLEN + 1))
cur = Signal(8)
m.d.comb += cur.eq(rom[uidx])
with m.Switch(uidx):
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(26 + k): m.d.comb += cur.eq(verd[8*k:8*k+8])
shift = Signal(10, init=0x3FF)
nbits = Signal(range(11))
baud = Signal(range(DIV))
gap = Signal(range(DIV * 30 + 1))
m.d.comb += self.uart_tx.eq(shift[0])
with m.FSM(domain="sync", name="uart_fsm"):
with m.State("IDLE"):
with m.If(self.done):
m.d.sync += uidx.eq(0)
m.next = "LOAD"
with m.State("LOAD"):
with m.If(uidx == MSGLEN):
m.d.sync += gap.eq(DIV * 30)
m.next = "GAP"
with m.Else():
m.d.sync += [shift.eq(Cat(C(0, 1), cur, C(1, 1))),
nbits.eq(10), baud.eq(DIV - 1)]
m.next = "SHIFT"
with m.State("SHIFT"):
with m.If(baud == 0):
m.d.sync += baud.eq(DIV - 1)
with m.If(nbits == 1):
m.d.sync += uidx.eq(uidx + 1)
m.next = "LOAD"
with m.Else():
m.d.sync += [nbits.eq(nbits - 1),
shift.eq(Cat(shift[1:], C(1, 1)))]
with m.Else():
m.d.sync += baud.eq(baud - 1)
with m.State("GAP"):
m.d.sync += gap.eq(gap - 1)
with m.If(gap == 0):
m.d.sync += uidx.eq(0)
m.next = "LOAD"
return m
class W5100SelfTestTop(Elaboratable):
"""Platform build wrapper: 24 MHz HFOSC `sync` clock + W5100 bus + LED pins.
No PLL / capture domain, so it P&Rs with huge timing margin on any seed.
"""
def elaborate(self, platform):
m = Module()
m.domains += ClockDomain("sync")
m.submodules.hfosc = Instance(
"SB_HFOSC",
p_CLKHF_DIV="0b01", # 48 / 2 -> 24 MHz
i_CLKHFEN=Const(1, 1),
i_CLKHFPU=Const(1, 1),
o_CLKHF=ClockSignal("sync"),
)
m.submodules.dut = dut = W5100SelfTest()
w5100 = platform.request("w5100", 0)
ledr = platform.request("ledr", 0)
ledg = platform.request("ledg", 0)
uart = platform.request("uart", 0)
m.d.comb += [
w5100.addr.o.eq(dut.bus_addr),
w5100.data.o.eq(dut.bus_data_o),
w5100.data.oe.eq(dut.bus_data_oe),
dut.bus_data_i.eq(w5100.data.i),
w5100.cs_n.o.eq(dut.cs_n),
w5100.rd_n.o.eq(dut.rd_n),
w5100.wr_n.o.eq(dut.wr_n),
w5100.rst_n.o.eq(dut.rst_n),
ledr.o.eq(dut.led_r), # LEDs are ACTIVE-HIGH on this board
ledg.o.eq(dut.led_g),
uart.tx.o.eq(dut.uart_tx), # 115200 8N1 status report (FT2232H ch B)
]
return m
# ── simulation self-test: a small W5100 indirect-bus model ───────────────────
if __name__ == "__main__":
import sys
if "--build" in sys.argv or "--flash" in sys.argv:
from exi_bba.synth import IceBreakerPlatform
flash = "--flash" in sys.argv
IceBreakerPlatform().build(W5100SelfTestTop(), do_program=flash,
name="w5100_selftest", build_dir="build_w5100test")
print("[built] build_w5100test/w5100_selftest.bin" + (" + flashed" if flash else ""))
raise SystemExit(0)
from amaranth.sim import Simulator, Period
def run(clock_dead=False, bus_dead=False, break_bit=None):
dut = W5100SelfTest(strobe_cycles=3, rst_cycles=3, boot_cycles=3,
poll_timeout=8, blink_cycles=4)
sim = Simulator(dut)
sim.add_clock(Period(MHz=24), domain="sync")
async def w5100_model(ctx):
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 (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:
val = 0xFF
elif a == _A_MR:
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)
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(9000):
await ctx.tick("sync")
if ctx.get(dut.done):
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):
result["v"] = await checker(ctx)
sim.add_testbench(w5100_model, background=True)
sim.add_testbench(tb)
sim.run()
return result["v"]
ok = True
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, 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, 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, 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)