diff --git a/.gitignore b/.gitignore index cdc94db..b8ab273 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ hardware/re-bba-rb/re-bba-rb-eeprom.bin .DS_Store *.swp .history/ +build_w5100test/ diff --git a/exi_bba/w5100_selftest.py b/exi_bba/w5100_selftest.py new file mode 100644 index 0000000..9eb8096 --- /dev/null +++ b/exi_bba/w5100_selftest.py @@ -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 \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) diff --git a/hardware/re-bba-rb/REVIEW.md b/hardware/re-bba-rb/REVIEW.md index 925cdd7..02c9662 100644 --- a/hardware/re-bba-rb/REVIEW.md +++ b/hardware/re-bba-rb/REVIEW.md @@ -308,6 +308,19 @@ datasheet extraction, WebSearch/WebFetch for parts and reference designs. EFUSE_EN : R46, R47, U13.5 EN/UVLO EFUSE_ILIM: R45, U13.3 ILIM 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 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 diff --git a/hardware/re-bba-rb/TODO.md b/hardware/re-bba-rb/TODO.md index b92f671..82c9c59 100644 --- a/hardware/re-bba-rb/TODO.md +++ b/hardware/re-bba-rb/TODO.md @@ -241,6 +241,14 @@ schematic changes. 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, 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**, 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 @@ -273,16 +281,14 @@ schematic changes. board PCF. - [ ] **Gateware: EXI INT drive style** — prefer open-drain emulation (drive 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** — - found during the last DRC pass, 4 error-severity violations (2× - `clearance` + 2× `hole_clearance`, both 0 mm actual). Two GND vias at - (190.78, 58.58) and (190.34, 58.58) — just south of J3's courtyard, - likely stitching vias added during the J3 reroute — sit directly on - top of the 3V3 pour on In2.Cu. This is a real short risk, not a - manufacturability nit: move or delete these two vias (or void the 3V3 - zone locally) before ordering. Also 1 harmless `track_dangling`: a - 0.06 mm stub on `/exi/EXI_MOSI_RAW` near (188.39, 57.52) — routing - debris, delete when convenient. +- [x] **2 GND vias vs `/Power/3V3` zone on In2.Cu — RESOLVED 2026-08-28.** + Was 4 error-severity DRC violations (2× `clearance` + 2× `hole_clearance`, + 0 mm) from GND stitching vias sitting on the 3V3 pour. Cleared by refilling + the zones (the pour now voids around the vias); `kicad-cli pcb drc + --severity-error` = 0 on the filled board. The `flash`/production skill + (kicad-manufacturing-export) now refills zones before export, so this + can't silently regress. (Any leftover `track_dangling` stub is + warning-level debris, delete when convenient.) ## 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 rewired pins. Optionally program serial/product string later with 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, - most hobby designs skip; note spare-channel budget if respinning D2 area. +- [x] CC1/CC2 ESD protection — **WON'T FIX** (Dennis, 2026-08-28). USBLC6 + 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 `.gitignore:28`, nothing tracked. diff --git a/hardware/re-bba-rb/exi_devid_spi.py b/hardware/re-bba-rb/exi_devid_spi.py new file mode 100644 index 0000000..13a95d7 --- /dev/null +++ b/hardware/re-bba-rb/exi_devid_spi.py @@ -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.")