Merge the exi and sync domains into one

bba_top wired ClockSignal("sync") to ClockSignal("exi"), so the two "domains"
were always the same physical clock net.  Everything between them -- six
AsyncFIFOs and three PulseSynchronizers -- was pure latency and logic for a
crossing that did not exist.

  * the six AsyncFIFOs become plain SyncFIFOs
  * the three PulseSynchronizers become wires
  * BBARegisterFile and ExiCapture move from m.d.exi to m.d.sync
  * the exi ClockDomain is gone; HFOSC drives sync directly

The SPRAM request write port is also combinational now.  A registered w_en
costs a 24 MHz cycle, and the protocol's turnaround byte (~296 ns at 27 MHz)
has no cycles to spare.

Passing: bba_register_file, spram_arbiter, rx_frame_assembler, tx_frame_drain,
w5100_parallel_master, uart_shell, status_panel.  bba_top at EXI_HALF=1
(27 MHz, the hardware rate) passes device ID, PAR read/write, NWAYS, RWP and
the 34-byte TX frame.

STILL OPEN: the SPRAM/DMA read is one byte late.  Removing the CDC was not
enough -- the remaining round trip is the rx crossing (exiclk->sync, a REAL
CDC, ~125 ns) plus FSM decode plus the spram_req FIFO, arbiter, SPRAM access
and spram_rsp FIFO, which still exceeds the turnaround byte.  Next step: now
that it is all one domain, drop the two SPRAM FIFOs entirely and give the
register file a direct request/grant port on the arbiter -- that should take
the round trip to ~2-3 cycles and fit.

exi_capture and exi_j4 testbenches still assert removed behaviour (the CS-fall
flush and the old MISO slot timing) and need rewriting for the clocked engine.
Still WIP -- do not flash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 17:02:58 +02:00
parent da21a080d9
commit 02239466fb
4 changed files with 115 additions and 115 deletions
+9 -9
View File
@@ -120,9 +120,9 @@ class ExiCapture(Elaboratable):
# (~296 ns at 27 MHz) of slack -- unlike the old design, where
# bit-level MISO timing crossed a domain with a 37 ns budget.
rx_fifo = AsyncFIFO(width=8, depth=self._rx_depth,
w_domain="exififo_w", r_domain="exi")
w_domain="exififo_w", r_domain="sync")
tx_fifo = AsyncFIFO(width=8, depth=self._tx_depth,
w_domain="exi", r_domain="exififo_r")
w_domain="sync", r_domain="exififo_r")
m.submodules.rx_fifo = rx_fifo
m.submodules.tx_fifo = tx_fifo
@@ -160,9 +160,9 @@ class ExiCapture(Elaboratable):
# there is no longer any reason to detect it in a fast domain.
cs_n_s = Signal(init=1)
m.submodules.cs_sync = FFSynchronizer(self.spi_cs_n, cs_n_s,
o_domain="exi", init=1)
o_domain="sync", init=1)
cs_n_prev = Signal(init=1)
m.d.exi += cs_n_prev.eq(cs_n_s)
m.d.sync += cs_n_prev.eq(cs_n_s)
m.d.comb += self.cs_active.eq(~cs_n_s)
return m
@@ -204,7 +204,7 @@ if __name__ == "__main__":
if ctx.get(dut.rx_rdy):
into.append(ctx.get(dut.rx_data))
ctx.set(dut.rx_en, 1)
await ctx.tick("exi").repeat(1)
await ctx.tick("sync").repeat(1)
ctx.set(dut.rx_en, 0)
return True
return False
@@ -212,7 +212,7 @@ if __name__ == "__main__":
async def push_tx(ctx, b):
ctx.set(dut.tx_data, b)
ctx.set(dut.tx_en, 1)
await ctx.tick("exi").repeat(1)
await ctx.tick("sync").repeat(1)
ctx.set(dut.tx_en, 0)
async def do_txn(ctx, hdr, responses, n_data, rx_seen):
@@ -226,7 +226,7 @@ if __name__ == "__main__":
await spi_byte(ctx, h)
for _ in range(20): # clock-idle gap
await core_drain_rx(ctx, rx_seen)
await ctx.tick("exi").repeat(1)
await ctx.tick("sync").repeat(1)
for r in responses:
await push_tx(ctx, r)
await ctx.delay(HP * (2))
@@ -235,7 +235,7 @@ if __name__ == "__main__":
await ctx.delay(HP * (HALF))
for _ in range(20): # drain data-phase dummies
await core_drain_rx(ctx, rx_seen)
await ctx.tick("exi").repeat(1)
await ctx.tick("sync").repeat(1)
return miso
async def testbench(ctx):
@@ -262,7 +262,7 @@ if __name__ == "__main__":
errors.append(f"T2 flush failed — stale byte leaked: {[hex(b) for b in misoB]}")
sim = Simulator(dut)
sim.add_clock(Period(MHz=24), domain="exi")
sim.add_clock(Period(MHz=24), domain="sync")
sim.add_testbench(testbench)
with sim.write_vcd("ExiCapture.vcd"):