"""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 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 = "REPORT" 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" with m.Else(): m.next = "MR_POLL_ISSUE" 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 \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 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])) for k in range(4): with m.Case(20 + 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-mode regs by A[1:0] for _ in range(8000): 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 rd == 0: # drive read data if bus_dead: ctx.set(dut.bus_data_i, 0xFF) # nothing drives -> float hi 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 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) async def checker(ctx): for _ in range(4000): await ctx.tick("sync") if ctx.get(dut.done): return (ctx.get(dut.mr_ok), ctx.get(dut.bad_bits)) return (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 = run() print(f"[healthy] mr_ok={mr} bad={bad:#04x} -> GREEN solid") ok &= (mr == 1 and bad == 0) mr, bad = run(break_bit=3) print(f"[D3 broken] mr_ok={mr} bad={bad:#04x} -> 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") 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) print("PASS" if ok else "FAIL") import sys; sys.exit(0 if ok else 1)