Bring-up: W5100 bus self-test, eFuse OVLO bug, Pi SPI EXI host
From the physical V2 bring-up session (2026-09-20): - exi_bba/w5100_selftest.py (NEW): standalone FPGA<->W5100 bus diagnostic. Clock-independent data-bus walk (direct-mode GAR0 read-back, all 8 lines) + MR software-reset self-clear (needs the 25 MHz crystal). Reports 4 verdicts on D6/D7 LEDs AND over UART (115200): GOOD / data-line-N / clock-dead / no-response. Boot wait is 85 ms (> the W5100S 60.3 ms init). On this board: GOOD -- bus + all 8 data lines + 25 MHz clock all verified (unlike V1's broken D3). - REVIEW.md / TODO.md: eFuse U13 OVLO is tied to GND, which on the TPS25961 selects the internal FIXED ~6 V overvoltage cutoff -- so it blocks the 12 V rail entirely (bench-confirmed: 12V_EXI=0, GC_ON=0, W5100 unpowered). FAB- BLOCKER for V2.1: fix with an OVLO divider (~1.1M/100k -> trip ~15 V). Bench workaround: lift U13 pin 2 (OVLO) off GND. Also closed the eFuse-placement, GND-via/3V3-zone, and CC-ESD TODO items. - hardware/re-bba-rb/exi_devid_spi.py (NEW): Pi hardware-SPI (~25 MHz) EXI device-ID host, companion to the bit-bang exi_devid_rpi.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -32,3 +32,4 @@ hardware/re-bba-rb/re-bba-rb-eeprom.bin
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
.history/
|
.history/
|
||||||
|
build_w5100test/
|
||||||
|
|||||||
@@ -0,0 +1,451 @@
|
|||||||
|
"""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 <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
|
||||||
|
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)
|
||||||
@@ -308,6 +308,19 @@ datasheet extraction, WebSearch/WebFetch for parts and reference designs.
|
|||||||
EFUSE_EN : R46, R47, U13.5 EN/UVLO
|
EFUSE_EN : R46, R47, U13.5 EN/UVLO
|
||||||
EFUSE_ILIM: R45, U13.3 ILIM
|
EFUSE_ILIM: R45, U13.3 ILIM
|
||||||
GND : U13.2 OVLO, U13.4 GND, R45.2, R47.2, C66.2
|
GND : U13.2 OVLO, U13.4 GND, R45.2, R47.2, C66.2
|
||||||
|
⚠️ **BUG FOUND AT BRING-UP 2026-09-16 — this OVLO=GND is WRONG for a 12 V
|
||||||
|
rail and the eFuse passes NOTHING.** Per the TPS25961 datasheet, tying OVLO to
|
||||||
|
GND selects the *internal fixed* overvoltage threshold **VOVP(R) = 5.98 V typ
|
||||||
|
(5.55–6.5 V)** — NOT "OVLO disabled". A 12 V input is far above that, so the
|
||||||
|
device sits in overvoltage lockout with the FET off (bench-confirmed: 12V_EXI =
|
||||||
|
0 V, GC_ON = 0 V, W5100 unpowered). The earlier review note ("OVLO tied to GND
|
||||||
|
for the internal fixed threshold, abs max 6.5 V fine") caught the pin abs-max
|
||||||
|
but MISSED that the fixed VIN cutoff is ~6 V. **Fix (V2.1):** drive OVLO from a
|
||||||
|
divider off 12V_RAW so the trip sits ~14–16 V (above 12 V, below the 19 V max /
|
||||||
|
the D3 clamp): e.g. **R_top ≈ 1.1 MΩ (12V_RAW→OVLO) + R_bottom ≈ 100 kΩ
|
||||||
|
(OVLO→GND)** → OVLO pin = 1.24 V at ~15 V VIN, and ~1.0 V at 12 V (stays on).
|
||||||
|
BENCH WORKAROUND to keep bring-up moving: inject 12 V straight onto 12V_EXI
|
||||||
|
(C65+ pad or U3.3 VIN), bypassing the mis-set eFuse.
|
||||||
**D3 (SMAJ12A TVS) stays on the INPUT side on purpose** — it clamps
|
**D3 (SMAJ12A TVS) stays on the INPUT side on purpose** — it clamps
|
||||||
GC-side surges before they reach the eFuse's 21 V absolute maximum. **C65
|
GC-side surges before they reach the eFuse's 21 V absolute maximum. **C65
|
||||||
is on the OUTPUT side on purpose** — it is the bulk cap whose inrush we are
|
is on the OUTPUT side on purpose** — it is the bulk cap whose inrush we are
|
||||||
|
|||||||
+19
-12
@@ -241,6 +241,14 @@ schematic changes.
|
|||||||
at (148.49, 61.7225) near U11 pin 48. Verified 2026-07-22: nets
|
at (148.49, 61.7225) near U11 pin 48. Verified 2026-07-22: nets
|
||||||
`/ethernet/ETH_RST` + `/ethernet/ETH_3V3` both present on the footprint,
|
`/ethernet/ETH_RST` + `/ethernet/ETH_3V3` both present on the footprint,
|
||||||
0 unconnected items.
|
0 unconnected items.
|
||||||
|
- [ ] 🔴 **eFuse U13 OVLO MISCONFIGURED — BLOCKS 12 V (found at bring-up
|
||||||
|
2026-09-16, FAB-BLOCKER for V2.1).** OVLO (pin 2) is tied to GND, which on
|
||||||
|
the TPS25961 selects the internal *fixed* overvoltage cutoff ~5.98 V — so a
|
||||||
|
12 V rail is permanently in overvoltage lockout and the eFuse passes
|
||||||
|
nothing (bench-confirmed: 12V_EXI=0, GC_ON=0, W5100 unpowered). **Fix:**
|
||||||
|
OVLO divider off 12V_RAW, R_top ≈ 1.1 M / R_bottom ≈ 100 k → trip ~15 V.
|
||||||
|
See REVIEW.md eFuse entry. Bench workaround: feed 12 V onto 12V_EXI (C65+ /
|
||||||
|
U3 VIN) to bypass it. This must be fixed before any V2.1 fab order.
|
||||||
- [ ] **12 V inrush limiter (U13 TPS25961 eFuse) — SCHEMATIC DONE 2026-07-31**,
|
- [ ] **12 V inrush limiter (U13 TPS25961 eFuse) — SCHEMATIC DONE 2026-07-31**,
|
||||||
full design rationale in REVIEW.md §6. J3.5 → 12V_RAW → U13 → 12V_EXI;
|
full design rationale in REVIEW.md §6. J3.5 → 12V_RAW → U13 → 12V_EXI;
|
||||||
D3 (TVS) stays upstream, C65 downstream. R45 100 k sets I_LIM ≈ 500 mA
|
D3 (TVS) stays upstream, C65 downstream. R45 100 k sets I_LIM ≈ 500 mA
|
||||||
@@ -273,16 +281,14 @@ schematic changes.
|
|||||||
board PCF.
|
board PCF.
|
||||||
- [ ] **Gateware: EXI INT drive style** — prefer open-drain emulation (drive
|
- [ ] **Gateware: EXI INT drive style** — prefer open-drain emulation (drive
|
||||||
low / release) on J3.3 rather than push-pull high.
|
low / release) on J3.3 rather than push-pull high.
|
||||||
- [ ] **New 2026-07-22: 2 GND vias short the `/Power/3V3` zone on In2.Cu** —
|
- [x] **2 GND vias vs `/Power/3V3` zone on In2.Cu — RESOLVED 2026-08-28.**
|
||||||
found during the last DRC pass, 4 error-severity violations (2×
|
Was 4 error-severity DRC violations (2× `clearance` + 2× `hole_clearance`,
|
||||||
`clearance` + 2× `hole_clearance`, both 0 mm actual). Two GND vias at
|
0 mm) from GND stitching vias sitting on the 3V3 pour. Cleared by refilling
|
||||||
(190.78, 58.58) and (190.34, 58.58) — just south of J3's courtyard,
|
the zones (the pour now voids around the vias); `kicad-cli pcb drc
|
||||||
likely stitching vias added during the J3 reroute — sit directly on
|
--severity-error` = 0 on the filled board. The `flash`/production skill
|
||||||
top of the 3V3 pour on In2.Cu. This is a real short risk, not a
|
(kicad-manufacturing-export) now refills zones before export, so this
|
||||||
manufacturability nit: move or delete these two vias (or void the 3V3
|
can't silently regress. (Any leftover `track_dangling` stub is
|
||||||
zone locally) before ordering. Also 1 harmless `track_dangling`: a
|
warning-level debris, delete when convenient.)
|
||||||
0.06 mm stub on `/exi/EXI_MOSI_RAW` near (188.39, 57.52) — routing
|
|
||||||
debris, delete when convenient.
|
|
||||||
|
|
||||||
## Broader items (beyond the wiring review — block fab on the first two)
|
## Broader items (beyond the wiring review — block fab on the first two)
|
||||||
|
|
||||||
@@ -374,8 +380,9 @@ schematic changes.
|
|||||||
exactly what stock iceprog opens by default, which now matches the
|
exactly what stock iceprog opens by default, which now matches the
|
||||||
rewired pins. Optionally program serial/product string later with
|
rewired pins. Optionally program serial/product string later with
|
||||||
FT_PROG (do NOT change VID/PID, or iceprog needs -d vid:pid).
|
FT_PROG (do NOT change VID/PID, or iceprog needs -d vid:pid).
|
||||||
- [ ] CC1/CC2 have no ESD protection (USBLC6 covers D+/D− only) — optional,
|
- [x] CC1/CC2 ESD protection — **WON'T FIX** (Dennis, 2026-08-28). USBLC6
|
||||||
most hobby designs skip; note spare-channel budget if respinning D2 area.
|
covers D+/D− only; CC lines left unprotected by choice, as most hobby
|
||||||
|
designs do. Closed, not a pending item.
|
||||||
- [x] `.history/` (embedded `.git`) — VERIFIED ignored via root
|
- [x] `.history/` (embedded `.git`) — VERIFIED ignored via root
|
||||||
`.gitignore:28`, nothing tracked.
|
`.gitignore:28`, nothing tracked.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""EXI device-ID query over the Pi's HARDWARE SPI (spidev) — a near-full-speed
|
||||||
|
(~25 MHz, close to the GameCube's real ~27 MHz EXI clock) companion to the
|
||||||
|
bit-bang exi_devid_rpi.py.
|
||||||
|
|
||||||
|
Use bit-bang (exi_devid_rpi.py) for a slow logic smoke test; use THIS to prove
|
||||||
|
the FPGA's SPI Mode-3 capture front-end also samples cleanly *at rate* without a
|
||||||
|
GameCube. The Pi 4 SPI SCK is core_clk/even-divisor, so a 27 MHz request lands
|
||||||
|
on ~25 MHz — comfortably inside the design's 54 MHz 2x oversampling (and a hair
|
||||||
|
more margin than the real 27 MHz). Do NOT push past ~27 MHz: the capture domain
|
||||||
|
only closes ~49-58 MHz across seeds, so >~27 MHz input under-samples.
|
||||||
|
|
||||||
|
The FPGA prefetches the reply during a clock-IDLE gap between the header and the
|
||||||
|
data bytes (the GC pauses the clock there), so CS must stay LOW across that gap.
|
||||||
|
spidev toggles CS per call, so we park SPI0's hardware CS on BCM7 (unused) and
|
||||||
|
drive the real CS manually on BCM8. Pi config (then reboot):
|
||||||
|
/boot/firmware/config.txt: dtoverlay=spi0-1cs,cs0_pin=7
|
||||||
|
(That frees BCM8 and puts the unused hw CE0 on BCM7. To go back to the bit-bang
|
||||||
|
script, remove that line + reboot so BCM9/10/11 return to plain GPIO.)
|
||||||
|
|
||||||
|
Wiring (Pi -> FPGA EXI, all 3.3 V, shared GND) — identical to the bit-bang one:
|
||||||
|
SCLK BCM11 (pin 23) -> EXI CLK (FPGA 44)
|
||||||
|
MOSI BCM10 (pin 19) -> EXI MOSI (FPGA 4)
|
||||||
|
MISO BCM9 (pin 21) <- EXI MISO (FPGA 3)
|
||||||
|
CS BCM8 (pin 24) -> EXI CS (FPGA 45) [held low manually across the txn]
|
||||||
|
GND (pin 25) <-> GND
|
||||||
|
Run: sudo python3 exi_devid_spi.py [speed_hz] [gap_us]
|
||||||
|
"""
|
||||||
|
import sys, time
|
||||||
|
import spidev
|
||||||
|
try:
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
except ImportError:
|
||||||
|
raise SystemExit("needs RPi.GPIO -> sudo apt install python3-rpi.gpio")
|
||||||
|
|
||||||
|
SPEED = int(sys.argv[1]) if len(sys.argv) > 1 else 27_000_000 # ~25 MHz actual (core/10)
|
||||||
|
GAP_US = int(sys.argv[2]) if len(sys.argv) > 2 else 50 # header->data prefetch gap
|
||||||
|
CS = 8 # BCM8 manual chip-select (hardware CE0 parked on BCM7 by the overlay)
|
||||||
|
|
||||||
|
spi = spidev.SpiDev()
|
||||||
|
spi.open(0, 0)
|
||||||
|
spi.mode = 0b11 # SPI Mode 3: CPOL=1, CPHA=1 (EXI CLK idles high)
|
||||||
|
spi.max_speed_hz = SPEED
|
||||||
|
|
||||||
|
GPIO.setmode(GPIO.BCM)
|
||||||
|
GPIO.setup(CS, GPIO.OUT, initial=1) # CS idle high
|
||||||
|
|
||||||
|
GPIO.output(CS, 0) # assert CS (active low) for the whole transaction
|
||||||
|
spi.xfer2([0x00, 0x03]) # header: read, addr[12:0]=0, len-1=3 (4 bytes)
|
||||||
|
time.sleep(GAP_US / 1e6) # clock-idle gap -> FPGA prefetches the reply
|
||||||
|
resp = spi.xfer2([0x00] * 6) # clock read bytes; MISO carries the response
|
||||||
|
GPIO.output(CS, 1) # deassert CS
|
||||||
|
spi.close(); GPIO.cleanup()
|
||||||
|
|
||||||
|
print(f"SPI mode 3, {SPEED/1e6:.1f} MHz requested (actual ~= core/even-divisor, ~25 MHz), "
|
||||||
|
f"header->data gap {GAP_US} us")
|
||||||
|
print("read bytes:", " ".join("%02x" % b for b in resp))
|
||||||
|
want = [0x04, 0x02, 0x02, 0x00]
|
||||||
|
if any(resp[i:i + 4] == want for i in range(len(resp) - 3)):
|
||||||
|
print("EXI device-ID 04 02 02 00 -> FOUND. Capture path works at ~full EXI rate!")
|
||||||
|
else:
|
||||||
|
print("device-ID not found. If the slow bit-bang test (exi_devid_rpi.py) PASSES "
|
||||||
|
"but this fails, the front-end is marginal at rate on the flashed seed -> "
|
||||||
|
"reflash the BEST --seeds bitstream, and/or raise the gap (arg2) or lower "
|
||||||
|
"the speed (arg1). Otherwise check wiring/GND and that the FPGA is running.")
|
||||||
Reference in New Issue
Block a user