Cut the MISO output path: drop EXI_J4 and the CS mux; rank seeds by it
Remaining after the bit-phase fix was a SPEED-DEPENDENT failure -- the GC read
correctly at 1/2/4/8/16 MHz and failed only at index 5 (27 MHz), returning
0B 80 = 0x17 late by exactly ONE bit. Opposite signature to the phase slip it
was hiding under, and this one really is propagation delay.
MISO is source-synchronous: driven on the falling edge, sampled by the GC on
the next rising edge, so the whole path must fit a HALF period -- 18.5 ns at
27 MHz -- minus global-buffer insertion, pad, connector and the GC's setup.
The failing build measured ~10.5 ns of that internally. Critical path:
clk-to-q 1.39 tx_shift (3,12)
routing 3.60 tx_shift[7] (3,12) -> (5,5) to the CS mux
logic 1.21 Mux(cs_n, 1, ...) LUT
routing 4.79 miso (5,5) -> (17,0) to the J4 MISO pad
10.99 ns (2.60 logic, 8.39 routing)
Two removals, both of things that were pure cost here:
1. EXI_J4 is GONE entirely (exi_j4.py, the resource, the AND merge, the docs
section, scripts/pi-exi-wirecheck.py) -- recoverable from git history. The
AND put a LUT in the CLOCK path, denying the EXI clock pad its dedicated
global-buffer route, and MISO fanned out to the J4 pad at die (17,0) which
was the far end of the critical output path: 4.79 ns of routing to a header
with nothing plugged into it. 10.2-11.5 ns -> 9.3-9.6 ns.
2. The Mux(cs_n, 1, ...) LUT is gone from the MISO data path. It only parked
MISO high when deselected, which the flop's own reset value does for free:
tx_shift now has init=0xFF and CS is that domain's async reset, so MISO is
tx_shift[7] with nothing in between. Removes 1.21 ns of logic plus the
3.60 ns route feeding it and lets the placer sit the flop by the pad.
synth.py now PARSES AND RANKS BY the MISO clock-to-pad delay. Ranking by fmax
was how a board that failed at 27 MHz passed every seed at >=1.29x margin --
fmax was never the binding constraint for this design.
--seeds 8: clk 28.7-32.5 MHz, EXI 49.6-62.1 MHz, MISO 8.38-10.22 ns, 8/8 PASS.
Best seed 8 (clk 30.35, EXI 49.56, MISO 8.38 ns), flashed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,159 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify the Pi <-> re-bba-rb J4 wiring BEFORE attempting a real EXI transfer.
|
||||
|
||||
Requires the FPGA to be running an EXI_J4=1 bitstream, which mirrors EXI onto
|
||||
the J4 header and enables SB_IO pull-ups on the three EXI *inputs*.
|
||||
|
||||
J4.1 GND <-> Pi pin 25 GND
|
||||
J4.2 DBG0 <-- Pi pin 23 BCM11 EXI CLK (FPGA input)
|
||||
J4.3 DBG1 <-- Pi pin 19 BCM10 EXI MOSI (FPGA input)
|
||||
J4.4 DBG2 --> Pi pin 21 BCM9 EXI MISO (FPGA OUTPUT - never drive)
|
||||
J4.5 DBG3 <-- Pi pin 24 BCM8 EXI CS_N (FPGA input)
|
||||
J4.6 DBG4 --> Pi pin 22 BCM25 EXI INT_N (FPGA OUTPUT - never drive)
|
||||
|
||||
Run: sudo python3 exi_wirecheck.py
|
||||
"""
|
||||
import sys, time
|
||||
import RPi.GPIO as GPIO
|
||||
|
||||
CLK, MOSI, MISO, CS_N, INT_N = 11, 10, 9, 8, 25
|
||||
HDR = {11: "hdr23", 10: "hdr19", 9: "hdr21", 8: "hdr24", 25: "hdr22"}
|
||||
DRIVEN_BY_PI = [(CLK, "CLK", "J4.2"), (MOSI, "MOSI", "J4.3"), (CS_N, "CS_N", "J4.5")]
|
||||
DRIVEN_BY_FPGA= [(MISO, "MISO", "J4.4"), (INT_N, "INT_N", "J4.6")]
|
||||
|
||||
fails, warns = [], []
|
||||
|
||||
def rd(pin, pull):
|
||||
GPIO.setup(pin, GPIO.IN, pull_up_down=pull)
|
||||
time.sleep(0.002)
|
||||
return GPIO.input(pin)
|
||||
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
for p, _, _ in DRIVEN_BY_PI + DRIVEN_BY_FPGA:
|
||||
GPIO.setup(p, GPIO.IN, pull_up_down=GPIO.PUD_OFF)
|
||||
|
||||
print("=" * 66)
|
||||
print("T0 CONTROL: pins known to be unwired MUST follow the internal pull.")
|
||||
print(" Without this, a broken pull mechanism would silently turn every")
|
||||
print(" T1 verdict into a false \'CONNECTED\'.")
|
||||
print("=" * 66)
|
||||
for pin, hdr in ((26, "hdr37"), (20, "hdr38"), (16, "hdr36"), (21, "hdr40")):
|
||||
u, d = rd(pin, GPIO.PUD_UP), rd(pin, GPIO.PUD_DOWN)
|
||||
ok = (u, d) == (1, 0)
|
||||
print(f" BCM{pin:<2} {hdr}: pull-up={u} pull-down={d} "
|
||||
f"{'pull works' if ok else '** PULL NOT WORKING - T1/T2 INVALID **'}")
|
||||
if not ok:
|
||||
fails.append(f"control pin BCM{pin} did not follow the pull; results below are meaningless")
|
||||
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_OFF)
|
||||
|
||||
print()
|
||||
print("=" * 66)
|
||||
print("T1 FPGA-driven pins: value must NOT follow the Pi's internal pull.")
|
||||
print(" An active driver (~ohms) beats the Pi's ~50k pull; a dead wire")
|
||||
print(" follows it. This test is decisive.")
|
||||
print("=" * 66)
|
||||
for pin, name, j4 in DRIVEN_BY_FPGA:
|
||||
up, dn = rd(pin, GPIO.PUD_UP), rd(pin, GPIO.PUD_DOWN)
|
||||
if up == dn:
|
||||
print(f" {name:<5} {j4} BCM{pin:<2} {HDR[pin]}: pull-up={up} pull-down={dn}"
|
||||
f" -> CONNECTED, FPGA driving {up}")
|
||||
else:
|
||||
print(f" {name:<5} {j4} BCM{pin:<2} {HDR[pin]}: pull-up={up} pull-down={dn}"
|
||||
f" -> ** NOT CONNECTED ** (follows the Pi's pull)")
|
||||
fails.append(f"{name} ({j4} -> BCM{pin}/{HDR[pin]}) is open")
|
||||
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_OFF)
|
||||
|
||||
print()
|
||||
print("=" * 66)
|
||||
print("T2 FPGA-input pins: these carry SB_IO PULLUP=1 in an EXI_J4 build, so")
|
||||
print(" a correct connection reads HIGH regardless of the Pi's pull.")
|
||||
print(" Follows-the-pull = open. Hard low = shorted. Decisive either way.")
|
||||
print("=" * 66)
|
||||
for pin, name, j4 in DRIVEN_BY_PI:
|
||||
u, d = rd(pin, GPIO.PUD_UP), rd(pin, GPIO.PUD_DOWN)
|
||||
if (u, d) == (1, 1):
|
||||
print(f" {name:<5} {j4} BCM{pin:<2} {HDR[pin]}: up=1 down=1 -> held HIGH: FPGA pull-up seen, CONNECTED")
|
||||
elif (u, d) == (1, 0):
|
||||
print(f" {name:<5} {j4} BCM{pin:<2} {HDR[pin]}: up=1 down=0 -> follows the Pi pull: ** OPEN **")
|
||||
fails.append(f"{name} ({j4} -> BCM{pin}/{HDR[pin]}) is open - no connection")
|
||||
else:
|
||||
print(f" {name:<5} {j4} BCM{pin:<2} {HDR[pin]}: up={u} down={d} -> hard LOW: ** SHORTED TO GND **")
|
||||
fails.append(f"{name} ({j4} -> BCM{pin}/{HDR[pin]}) is shorted low - an FPGA input with "
|
||||
f"PULLUP=1 cannot read a hard 0")
|
||||
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_OFF)
|
||||
|
||||
print()
|
||||
print("=" * 66)
|
||||
print("T3 Bridge scan on the Pi-driven lines (catches solder bridges).")
|
||||
print(" The aggressor drives LOW and victims are pulled UP: a strong")
|
||||
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)
|
||||
for pin, name, j4 in DRIVEN_BY_PI:
|
||||
victims = [(p2, n) for p2, n, _ in DRIVEN_BY_PI if p2 != pin]
|
||||
for vp, _ in victims:
|
||||
GPIO.setup(vp, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
GPIO.setup(pin, GPIO.OUT)
|
||||
GPIO.output(pin, 0) # aggressor drives LOW
|
||||
time.sleep(0.003)
|
||||
bad = [n for vp, n in victims if GPIO.input(vp) == 0]
|
||||
GPIO.output(pin, 1)
|
||||
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_OFF)
|
||||
if bad:
|
||||
print(f" drive {name:<5} LOW -> ** BRIDGED to {', '.join(bad)} **")
|
||||
fails.append(f"{name} is bridged to {', '.join(bad)}")
|
||||
else:
|
||||
print(f" drive {name:<5} LOW -> no bridge to {', '.join(n for _, n in victims)}")
|
||||
|
||||
print()
|
||||
print("=" * 66)
|
||||
print("T4 Liveness: assert CS_N, clock slowly, see if MISO ever moves.")
|
||||
print(" Proves CLK + CS_N reach the FPGA and MISO comes back, without")
|
||||
print(" depending on a correct transaction.")
|
||||
print("=" * 66)
|
||||
for p in (CLK, CS_N, MOSI):
|
||||
GPIO.setup(p, GPIO.OUT)
|
||||
GPIO.output(CLK, 1) # Mode 3: CLK idles HIGH
|
||||
GPIO.output(MOSI, 0)
|
||||
GPIO.output(CS_N, 1)
|
||||
GPIO.setup(MISO, GPIO.IN, pull_up_down=GPIO.PUD_OFF)
|
||||
time.sleep(0.01)
|
||||
idle = GPIO.input(MISO)
|
||||
GPIO.output(CS_N, 0) # assert
|
||||
time.sleep(0.005)
|
||||
seen = {GPIO.input(MISO)}
|
||||
for _ in range(32):
|
||||
GPIO.output(CLK, 0); time.sleep(0.001)
|
||||
seen.add(GPIO.input(MISO))
|
||||
GPIO.output(CLK, 1); time.sleep(0.001)
|
||||
seen.add(GPIO.input(MISO))
|
||||
GPIO.output(CS_N, 1)
|
||||
GPIO.output(CLK, 1)
|
||||
print(f" MISO idle (CS_N high) = {idle}; levels seen while clocking = {sorted(seen)}")
|
||||
if len(seen) > 1:
|
||||
print(" -> MISO CHANGES while clocking: the FPGA is responding. Chain alive.")
|
||||
else:
|
||||
print(" -> MISO never changed. Either the FPGA isn't seeing CLK/CS_N, or")
|
||||
print(" it has nothing to send yet. Not conclusive on its own; the")
|
||||
print(" device-ID read is the real test.")
|
||||
warns.append("MISO static while clocking")
|
||||
|
||||
GPIO.cleanup()
|
||||
print()
|
||||
print("=" * 66)
|
||||
if fails:
|
||||
print("WIRING FAULTS:")
|
||||
for f in fails:
|
||||
print(" -", f)
|
||||
if warns:
|
||||
print("ADVISORY:")
|
||||
for w in warns:
|
||||
print(" -", w)
|
||||
if not fails:
|
||||
print("No hard wiring faults found." if not warns else
|
||||
"No hard wiring faults; see advisories above.")
|
||||
print("Next: sudo python3 ~/exi_devid_rpi.py (expect 04 02 02 00)")
|
||||
sys.exit(1 if fails else 0)
|
||||
Reference in New Issue
Block a user