EXI bring-up: device enumerates on hardware over the J4 header

First silicon proof of the EXI capture path.  A Pi bit-banging EXI into J4
reads the device ID 04 02 02 00 21/21, NWAYS 0x17, HIPR 0x01, writes and reads
back PAR0-3, writes IMR, and sees NCRA's reset bit self-clear.

Fixes two host-side bugs found getting there:
- exi_devid_rpi.py sampled MISO just after the rising edge, racing the slave's
  own update and landing one bit early (04 came back as 08).  Sample in the
  CLK-low phase, as the simulation does.
- pi-exi-wirecheck.py's bridge scan drove HIGH against pull-downs, but the
  FPGA's SB_IO pull-up out-pulls the Pi's internal pull-down, so every pin
  reported as bridged.  The aggressor now drives LOW against pull-ups.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 18:32:18 +00:00
parent 28b7665b33
commit a082e15c4f
4 changed files with 93 additions and 11 deletions
+28
View File
@@ -259,6 +259,34 @@ edge as the EXI pads (x=13–18 vs x=5–9, both y=0 per icestorm's `pinloc_db`
have perturbed the one placement variable the pin-co-location experiment showed have perturbed the one placement variable the pin-co-location experiment showed
is worth ±5 MHz — see "Remaining capture bottleneck" above. is worth ±5 MHz — see "Remaining capture bottleneck" above.
**HARDWARE BRING-UP 2026-09-30: the EXI capture path is PROVEN on silicon.**
A Raspberry Pi 4 bit-banging EXI into J4 (EXI_J4 build, seed 8) enumerated the
device and exercised the register file end-to-end:
| test | result |
|---|---|
| device ID @0x0000, 4 B | `04 02 02 00` — **21/21** |
| NWAYS @0x31 | `0x17` — 21/21 |
| HIPR @0x3A | `0x01` — 21/21 |
| PAR0–3 write `DE AD BE EF` + readback | **match**; restore to `00 09 BF 00` verified |
| IMR @0x08 write `0x86` + readback | `0x86` |
| NCRA @0x00 write `0x01` (reset) | self-cleared to `0x00` on the first poll |
Two bring-up gotchas, both cost real debugging time:
- **The master must sample MISO during the CLK-LOW phase.** The FPGA drives MISO
on the RISING edge (Mode 3), so sampling just after that edge races the
slave's own update and lands one bit position early — `04 02 02 00` comes back
as `08 ...`. Sample after taking CLK low, before raising it (this is exactly
what the simulation's `spi_byte` does).
- **Clock EXACTLY the requested byte count on a register read.** Over-clocking
(asking for 4 bytes then clocking 8–12) leaves stale prefetched bytes queued
and desynchronises following transactions — the symptom is a *deterministic*
every-Nth-read correctness cycle that looks like a signal-integrity fault but
is speed-independent. The GC driver never over-clocks a register read, so this
is a probe artefact, NOT a gateware bug; reads ≥ 0x100 deliberately stream
until CS (the DMA path). `hardware/re-bba-rb/exi_regcheck_rpi.py` clocks
correctly and is the reference host-side probe.
**Measured 2026-09-30**, `EXI_J4=1` + socket-3 UDP, `--seeds 8`: **Measured 2026-09-30**, `EXI_J4=1` + socket-3 UDP, `--seeds 8`:
`clk` 29.2–31.4 MHz (PASS on all 8); `capture_clk` **4 of 8 PASS** (seeds 2, 5, `clk` 29.2–31.4 MHz (PASS on all 8); `capture_clk` **4 of 8 PASS** (seeds 2, 5,
6, 8), best **seed 8 = 63.09 MHz** (+9.1 MHz / 17% margin). Same pass rate as 6, 8), best **seed 8 = 63.09 MHz** (+9.1 MHz / 17% margin). Same pass rate as
+5 -1
View File
@@ -43,8 +43,12 @@ def xfer(b):
for i in range(7, -1, -1): for i in range(7, -1, -1):
GPIO.output(MOSI, (b >> i) & 1) # MOSI stable while CLK is high GPIO.output(MOSI, (b >> i) & 1) # MOSI stable while CLK is high
GPIO.output(CLK, 0) # falling edge -> FPGA samples MOSI GPIO.output(CLK, 0) # falling edge -> FPGA samples MOSI
GPIO.output(CLK, 1) # rising edge -> FPGA drives MISO # Sample MISO HERE, while CLK is low. The FPGA drives MISO on the
# RISING edge, so reading just after that edge races the slave's own
# update and lands one bit position early (04 02 02 00 comes back as
# 08 ...). The low phase is when the bit is settled.
r = (r << 1) | GPIO.input(MISO) r = (r << 1) | GPIO.input(MISO)
GPIO.output(CLK, 1) # rising edge -> FPGA drives next bit
return r return r
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Clock EXACTLY the requested byte count, the way the GC driver does."""
import time, collections
import RPi.GPIO as GPIO
CLK, MOSI, MISO, CS = 11, 10, 9, 8
GPIO.setmode(GPIO.BCM); GPIO.setwarnings(False)
GPIO.setup(CLK, GPIO.OUT, initial=1); GPIO.setup(MOSI, GPIO.OUT, initial=0)
GPIO.setup(CS, GPIO.OUT, initial=1); GPIO.setup(MISO, GPIO.IN)
def xfer(b):
r = 0
for i in range(7, -1, -1):
GPIO.output(MOSI, (b >> i) & 1)
GPIO.output(CLK, 0)
r = (r << 1) | GPIO.input(MISO)
GPIO.output(CLK, 1)
return r
def read(addr, n, gap=0.005, idle=0.010):
GPIO.output(CS, 0); GPIO.output(CLK, 1); time.sleep(0.0005)
xfer((addr >> 6) & 0x7F); xfer(((addr & 0x3F) << 2) | ((n - 1) & 3))
time.sleep(gap)
out = [xfer(0x00) for _ in range(n)] # EXACTLY n bytes, no more
GPIO.output(CS, 1); time.sleep(idle)
return out
def trial(tag, addr, n, want, reps=21, idle=0.010):
hits = collections.Counter(); ok = 0
seq = []
for _ in range(reps):
r = read(addr, n, idle=idle)
hits[" ".join("%02x" % x for x in r)] += 1
good = r == want
ok += good
seq.append("." if good else "x")
print(f" {tag:<34} {ok:>2}/{reps} {''.join(seq)}")
for pat, cnt in hits.most_common(3):
print(f" {cnt:>3}x {pat}")
print("=== exact-length reads, 10ms idle between transactions ===")
trial("device ID @0x0000 (4B)", 0x0000, 4, [0x04, 0x02, 0x02, 0x00])
trial("NWAYS @0x31 (1B)", 0x31, 1, [0x17])
trial("HIPR @0x3A (1B)", 0x3A, 1, [0x01])
print()
print("=== same, but 200ms idle (does state decay?) ===")
trial("device ID @0x0000 (4B)", 0x0000, 4, [0x04, 0x02, 0x02, 0x00], reps=9, idle=0.200)
GPIO.cleanup()
+13 -10
View File
@@ -85,25 +85,28 @@ for pin, name, j4 in DRIVEN_BY_PI:
print() print()
print("=" * 66) print("=" * 66)
print("T3 Bridge scan on the Pi-driven lines (catches solder bridges on J4).") print("T3 Bridge scan on the Pi-driven lines (catches solder bridges).")
print(" Drive one HIGH, hold the others as inputs with pull-down: any") print(" The aggressor drives LOW and victims are pulled UP: a strong")
print(" victim reading HIGH shares copper with the aggressor.") print(" driver beats any pull, so a victim dragged low IS bridged.")
print(" (Driving HIGH against pull-downs does NOT work here -- the FPGA's")
print(" SB_IO pull-up out-pulls the Pi's internal pull-down, which would")
print(" report every pin as bridged. T2 shows this directly: down=1.)")
print("=" * 66) print("=" * 66)
for pin, name, j4 in DRIVEN_BY_PI: for pin, name, j4 in DRIVEN_BY_PI:
victims = [(p, n) for p, n, _ in DRIVEN_BY_PI if p != pin] victims = [(p2, n) for p2, n, _ in DRIVEN_BY_PI if p2 != pin]
for vp, _ in victims: for vp, _ in victims:
GPIO.setup(vp, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) GPIO.setup(vp, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(pin, GPIO.OUT) GPIO.setup(pin, GPIO.OUT)
GPIO.output(pin, 1) GPIO.output(pin, 0) # aggressor drives LOW
time.sleep(0.003) time.sleep(0.003)
bad = [n for vp, n in victims if GPIO.input(vp) == 1] bad = [n for vp, n in victims if GPIO.input(vp) == 0]
GPIO.output(pin, 0) GPIO.output(pin, 1)
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_OFF) GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_OFF)
if bad: if bad:
print(f" drive {name:<5} HIGH -> ** BRIDGED to {', '.join(bad)} **") print(f" drive {name:<5} LOW -> ** BRIDGED to {', '.join(bad)} **")
fails.append(f"{name} is bridged to {', '.join(bad)}") fails.append(f"{name} is bridged to {', '.join(bad)}")
else: else:
print(f" drive {name:<5} HIGH -> no bridge to {', '.join(n for _, n in victims)}") print(f" drive {name:<5} LOW -> no bridge to {', '.join(n for _, n in victims)}")
print() print()
print("=" * 66) print("=" * 66)