Clock the EXI bit engine from the EXI pin: timing closes on all 8 seeds
Replaces the PLL-fed 54 MHz `capture` domain, which oversampled the GC's
27 MHz EXI clock 2x and so put BIT-level timing across a CDC boundary: the
engine's view of the clock lagged the pin by 2 FFSynchronizer stages plus a
register (~55 ns) against a 37 ns bit time, so MISO came out ~2 bits late and
the GC read 0x17 as 05 C0. No amount of oversampling fixes that -- 54 MHz
gives only 2 ticks per bit and the part cannot close the 81 MHz that 3x needs.
The engine is now clocked by the pin itself (pin 44 = global buffer input net
6, no board change), making MISO clock-to-Q. ExiCapture creates four domains:
exiclk (posedge, samples MOSI), exiclk_n (negedge, drives MISO), both
async-reset by CS, plus reset-less exififo_w/exififo_r for the byte FIFO sides
-- reset-less because resetting one side of an AsyncFIFO desynchronises its
gray pointers, and posedge because AsyncFIFO has a posedge-only reset CDC
internally and cannot host a negedge side.
Also removes the SPRAM response FIFO. The DMA read was one byte late because
the round trip did not fit the protocol's turnaround byte; the arbiter already
exposed valid/ready, so the FIFO bought a cycle of latency and nothing else.
The response is handled outside the FSM (the reply lands in RHDR_PAD, not
SPRAM_STREAM) with a one-entry hold for when tx_fifo is momentarily full.
The PLL was left instantiated but unused after the rewrite; removing it frees
the hard macro and pin 35 (which stays unconnected only because the board is
already built that way). This is what lifted the pass rate:
domain target before after
clk (sync) 24 MHz 28.1-30.2 26.4-32.2 8/8
capture_clk 54 MHz 2 of 8 PASS (gone)
EXI pin domain 27 MHz -- 45.6-64.4 8/8
Best seed 3: clk 32.18 MHz, EXI 55.86 MHz. The binding constraint has FLIPPED
-- `clk` (1.10-1.34x) is now tighter than the EXI domain (1.69-2.39x) -- so
synth.py ranks seeds by worst RELATIVE margin, not by the EXI number.
synth.py could not have scored this design at all: it required the vanished
capture_clk, and its regex '(\w+)' cannot match nextpnr's exi_clk_$glb_clk
(no $ in \w). add_clock_constraint was also landing on the wrong net -- under
EXI_J4 the clock is the AND of two pads, not either pad -- so nextpnr silently
fell back to a 12 MHz default and under-optimised the whole EXI domain.
Testbenches:
- spi_mode3_slave had lost its testbench entirely in the rewrite. Restored
with a strict Mode 3 master (changes MOSI on the falling edge, samples MISO
in the clock-low phase) and MUTATION-TESTED: flipping either clock edge in
the RTL makes it fail. Both of those bugs previously reached silicon
against a lax master that parked MOSI well before the edge.
- exi_j4 T2/T3 returned [0,4,2,2]. Real bug: the engine latches the FIFO head
at each byte boundary, so a 2-byte command header leaves only ONE drain
slot. DEVID streamed zeros until CS and left two stale bytes -- one too
many -- so the NEXT enumeration came back a slot late. DEVID now pushes
exactly four bytes and stops. Register-space reads may still over-push:
their 4-byte header gives three drain slots.
- Dead ports removed from SPIMode3Slave (hdr_bytes, frame_start, cs_active,
byte_idx) and the mode2 parameter is gone -- there is one correct edge
convention and it is now enforced by tests.
All 14 module suites pass, plus bba_top at EXI_HALF=1/2/8.
CLAUDE.md: rewrote the clocking, CDC inventory and module tables; marked the
2026-09-30 capture_clk measurements historical; and corrected the line in
"Critical Protocol Notes" that still stated the INVERTED edge convention
("MOSI sampled on falling, MISO driven on rising" is Mode 2) -- the original
implementation followed that doc error.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+32
-43
@@ -2,11 +2,11 @@
|
||||
|
||||
Clock domains
|
||||
-------------
|
||||
capture : 54 MHz, from 12 MHz oscillator via SB_PLL40_CORE (REFERENCECLK fed
|
||||
from a global-buffer input pin; DIVR=0 DIVF=71 DIVQ=4). CORE (not
|
||||
PAD) so the reference clock is not tied to the dedicated PLL pad — it
|
||||
enters on a clock-capable GBIN pin, leaving board routing free.
|
||||
exi/sync : 24 MHz, from the iCE40UP5K internal SB_HFOSC (÷2, CLKHF_DIV=0b01)
|
||||
sync : 24 MHz, from the iCE40UP5K internal SB_HFOSC (÷2, CLKHF_DIV=0b01).
|
||||
Everything except the EXI bit engine. No PLL, no crystal.
|
||||
exiclk : the GC's EXI clock pin itself (~27 MHz), posedge — created inside
|
||||
exiclk_n : the same pin, negedge ................ `ExiCapture`.
|
||||
exififo_* the same pin, posedge, reset-less — the byte-FIFO sides.
|
||||
|
||||
Submodule instantiation and signal wiring
|
||||
-----------------------------------------
|
||||
@@ -125,40 +125,30 @@ class BBATop(Elaboratable):
|
||||
m = Module()
|
||||
|
||||
# ── Clock domain generation ───────────────────────────────────────
|
||||
# Three domains, two physical sources (1 PLL + 1 internal HFOSC):
|
||||
# capture @ 54 MHz (PLL) — SPI bit engine only; oversamples the
|
||||
# 27 MHz EXI clock 2× (robust Mode-3).
|
||||
# exi @ 24 MHz (HFOSC) — register file / transaction FSM.
|
||||
# sync @ 24 MHz (HFOSC) — SPRAM, RX/TX engines, ethernet master.
|
||||
# exi and sync share the HFOSC net (frequency- and phase-matched); the
|
||||
# AsyncFIFOs between them are still valid CDC and keep the module
|
||||
# boundaries clean. Only the tiny capture front-end needs the fast
|
||||
# clock — which is why 27 MHz-EXI / OG performance is reachable on the
|
||||
# iCE40UP5K even though the register-file logic tops out ~44 MHz.
|
||||
# ONE generated domain, one physical source:
|
||||
# sync @ 24 MHz (internal SB_HFOSC ÷2) — register file, SPRAM,
|
||||
# RX/TX engines, ethernet master. Everything that is not the EXI
|
||||
# bit engine lives here.
|
||||
#
|
||||
# The EXI front-end is NOT clocked from here: `ExiCapture` builds its
|
||||
# own exiclk/exiclk_n domains directly from the GC's EXI clock pin.
|
||||
# That is what removed the old 54 MHz PLL-fed `capture` domain, which
|
||||
# oversampled the EXI clock 2× and put BIT-level timing across a CDC
|
||||
# boundary (~55 ns of synchroniser latency against a 37 ns bit) — the
|
||||
# cause of the `0x17` -> `05 C0` MISO shift on hardware. With the
|
||||
# engine clocked by the pin, MISO is clock-to-Q and the only crossings
|
||||
# left are byte-level FIFOs with a full byte-time of slack.
|
||||
#
|
||||
# Consequence for timing closure: `capture_clk` is GONE as a
|
||||
# constraint. It was the binding path for this whole design and the
|
||||
# reason builds only closed on a minority of seeds.
|
||||
if platform is not None:
|
||||
# capture @ 54 MHz: icepll -i 12 -o 54 → DIVR=0 DIVF=71 DIVQ=4.
|
||||
# 54 MHz = 2× the 27 MHz EXI clock — the minimum oversampling that
|
||||
# cleanly implements SPI Mode 3. The isolated SPI bit engine closes
|
||||
# ~91 MHz on this device; the byte-FIFO read path brings the
|
||||
# integrated capture domain to ~62 MHz, so 54 closes with margin.
|
||||
m.domains += ClockDomain("capture")
|
||||
m.submodules.pll = Instance(
|
||||
"SB_PLL40_CORE",
|
||||
p_FEEDBACK_PATH = "SIMPLE",
|
||||
p_DIVR = 0,
|
||||
p_DIVF = 71,
|
||||
p_DIVQ = 4,
|
||||
p_FILTER_RANGE = 1,
|
||||
i_REFERENCECLK = platform.request("clk12").i,
|
||||
i_RESETB = Const(1, 1),
|
||||
i_BYPASS = Const(0, 1),
|
||||
o_PLLOUTGLOBAL = ClockSignal("capture"),
|
||||
)
|
||||
|
||||
# exi & sync @ 24 MHz: one SB_HFOSC (÷2) drives both slow domains.
|
||||
# The bulky register-file / SPRAM / W5500 logic is routing-bound at
|
||||
# sync @ 24 MHz from the internal oscillator. No PLL and no
|
||||
# external crystal are needed any more, so the 12 MHz input (pin
|
||||
# 35) and the PLL hard macro are both free.
|
||||
# The register-file / SPRAM / ethernet logic is routing-bound at
|
||||
# ~33–44 MHz on the UP5K; 24 MHz closes with large margin. The byte
|
||||
# rate (27 MHz EXI ÷ 8 ≈ 3.4 MHz) leaves ~7 slow cycles per byte.
|
||||
# rate (27 MHz EXI ÷ 8 ≈ 3.4 MHz) leaves ~7 sync cycles per byte.
|
||||
m.domains += ClockDomain("sync")
|
||||
m.submodules.hfosc = Instance(
|
||||
"SB_HFOSC",
|
||||
@@ -167,7 +157,7 @@ class BBATop(Elaboratable):
|
||||
i_CLKHFPU = Const(1, 1),
|
||||
o_CLKHF = ClockSignal("sync"),
|
||||
)
|
||||
# (simulation: test harness provides capture/exi/sync clocks via add_clock)
|
||||
# (simulation: the test harness drives the EXI pins and `sync` directly)
|
||||
|
||||
# ── Submodules ────────────────────────────────────────────────────
|
||||
cap = ExiCapture() # SPI bit engine (capture) + byte FIFOs
|
||||
@@ -191,7 +181,7 @@ class BBATop(Elaboratable):
|
||||
# ── External pin connections ──────────────────────────────────────
|
||||
m.d.comb += [
|
||||
# EXI inputs (to capture-domain front-end)
|
||||
cap.spi_clk .eq(self.exi_clk),
|
||||
cap.spi_clk .eq(self.exi_clk), # drives the exiclk domains
|
||||
cap.spi_mosi.eq(self.exi_mosi),
|
||||
cap.spi_cs_n.eq(self.exi_cs_n),
|
||||
# EXI outputs
|
||||
@@ -244,9 +234,8 @@ class BBATop(Elaboratable):
|
||||
]
|
||||
# SPRAM response: arb result → reg sync→exi FIFO write side
|
||||
m.d.comb += [
|
||||
reg.spram_rsp_w_data.eq(arb.exi_rsp_data),
|
||||
reg.spram_rsp_w_en .eq(arb.exi_rsp_valid),
|
||||
# arb does not need w_rdy feedback (spram_rsp FIFO is deeper than latency)
|
||||
reg.spram_rsp_data .eq(arb.exi_rsp_data),
|
||||
reg.spram_rsp_valid.eq(arb.exi_rsp_valid),
|
||||
]
|
||||
|
||||
# ── BBARegisterFile ↔ TXFrameDrain (sync domain FIFO sides) ──────
|
||||
@@ -421,7 +410,7 @@ if __name__ == "__main__":
|
||||
# `capture` domain to count ticks against. Drive the SPI clock with real
|
||||
# delays: one half-period = HALF * (1/54 MHz), preserving the old meaning
|
||||
# of HALF so the rates in the table above still hold.
|
||||
HP = Period(MHz=54) * HALF
|
||||
HP = Period(MHz=54) # ONE capture-equivalent tick; delays are HP * n
|
||||
# Gap between transactions (CS high). cs_active reaches the exi domain
|
||||
# through a 2-stage FFSynchronizer at 24 MHz, so CS must stay high for
|
||||
# ~3 exi cycles or the register file never sees the transaction end and
|
||||
|
||||
Reference in New Issue
Block a user