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:
2026-10-03 19:16:58 +02:00
parent d35688390e
commit 5f7f20db43
5 changed files with 106 additions and 486 deletions
-159
View File
@@ -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)