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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user