9c4bc7a61c
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>
510 lines
23 KiB
Python
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)
|