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
+105 -101
View File
@@ -15,8 +15,7 @@ Addresses 0x0100–0x1FFF : SPRAM ring buffer (sync domain, prefetch FIFOs).
"""
from amaranth import *
from amaranth.lib.cdc import PulseSynchronizer
from amaranth.lib.fifo import AsyncFIFO
from amaranth.lib.fifo import SyncFIFO
__all__ = ["BBARegisterFile"]
@@ -120,12 +119,17 @@ class BBARegisterFile(Elaboratable):
m = Module()
# ── CDC FIFOs ────────────────────────────────────────────────────
spram_req = AsyncFIFO(width=16, depth=4, w_domain="exi", r_domain="sync")
spram_rsp = AsyncFIFO(width=8, depth=4, w_domain="sync", r_domain="exi")
tx_bytes = AsyncFIFO(width=8, depth=16, w_domain="exi", r_domain="sync")
tx_ctrl = AsyncFIFO(width=16, depth=4, w_domain="exi", r_domain="sync")
rx_wptr = AsyncFIFO(width=8, depth=4, w_domain="sync", r_domain="exi")
rx_rptr = AsyncFIFO(width=8, depth=4, w_domain="exi", r_domain="sync")
# Plain SyncFIFOs: `exi` and `sync` were always the SAME clock net
# (bba_top wired ClockSignal("sync") to ClockSignal("exi")), so the
# AsyncFIFOs between them bought nothing and cost ~3 cycles each way.
# On the SPRAM path that latency exceeded the protocol's turnaround
# byte and shifted every DMA read by one byte. One slow domain now.
spram_req = SyncFIFO(width=16, depth=4)
spram_rsp = SyncFIFO(width=8, depth=4)
tx_bytes = SyncFIFO(width=8, depth=16)
tx_ctrl = SyncFIFO(width=16, depth=4)
rx_wptr = SyncFIFO(width=8, depth=4)
rx_rptr = SyncFIFO(width=8, depth=4)
m.submodules.spram_req = spram_req
m.submodules.spram_rsp = spram_rsp
@@ -162,19 +166,11 @@ class BBARegisterFile(Elaboratable):
]
# ── PulseSynchronizers ───────────────────────────────────────────
ncra_rst_ps = PulseSynchronizer(i_domain="exi", o_domain="sync")
rx_irq_ps = PulseSynchronizer(i_domain="sync", o_domain="exi")
tx_irq_ps = PulseSynchronizer(i_domain="sync", o_domain="exi")
m.submodules.ncra_rst_ps = ncra_rst_ps
m.submodules.rx_irq_ps = rx_irq_ps
m.submodules.tx_irq_ps = tx_irq_ps
m.d.comb += [
self.ncra_rst_o .eq(ncra_rst_ps.o),
rx_irq_ps.i .eq(self.rx_irq_i),
tx_irq_ps.i .eq(self.tx_irq_i),
]
# Single slow domain, so these are plain wires rather than
# PulseSynchronizers. A pulse generated here is already in the only
# domain that exists.
ncra_rst_pulse = Signal()
m.d.comb += self.ncra_rst_o.eq(ncra_rst_pulse)
# ── Register file (sparse individual Signals, exi domain) ────────
# Only the registers actually read/written by the GC or sync domain.
@@ -287,15 +283,19 @@ class BBARegisterFile(Elaboratable):
m.d.comb += rd_val.eq(reg_rdval)
# ── Default strobes ──────────────────────────────────────────────
m.d.exi += [
spram_req.w_en .eq(0),
m.d.sync += [
tx_bytes.w_en .eq(0),
tx_ctrl.w_en .eq(0),
rx_rptr.w_en .eq(0),
rx_wptr.r_en .eq(0),
ncra_rst_ps.i .eq(0),
ncra_rst_pulse .eq(0),
]
# The SPRAM request write port is COMBINATIONAL (see RHDR_LO): a
# registered w_en costs a 24 MHz cycle the turnaround byte cannot
# spare. Default it off here and let the states assert it.
m.d.comb += [
spram_req.w_en .eq(0),
spram_req.w_data.eq(0),
self.rx_en .eq(0),
self.tx_en .eq(0),
self.tx_data.eq(0),
@@ -308,7 +308,7 @@ class BBARegisterFile(Elaboratable):
# The SPI bit cadence lives in the capture domain; here we just consume
# received bytes and, for reads, push response bytes into tx_fifo during
# the EXI clock-idle gap before the GC clocks the data phase.
with m.FSM(domain="exi", name="exi_fsm"):
with m.FSM(domain="sync", name="exi_fsm"):
with m.State("HEADER0"):
# Byte 0 selects the space and direction. The REAL protocol
@@ -316,13 +316,13 @@ class BBARegisterFile(Elaboratable):
# clocks as many data bytes as it wants and ends with CS.
with m.If(self.rx_rdy):
m.d.comb += self.rx_en.eq(1)
m.d.exi += [is_write.eq(self.rx_data[6]), prefetched.eq(0),
m.d.sync += [is_write.eq(self.rx_data[6]), prefetched.eq(0),
byte_ctr.eq(0), sp_req.eq(0), outstanding.eq(0)]
with m.If(self.rx_data[7]): # register space
m.d.exi += cmd_space.eq(0)
m.d.sync += cmd_space.eq(0)
m.next = "RHDR_HI"
with m.Else(): # command space
m.d.exi += [cmd_space.eq(1),
m.d.sync += [cmd_space.eq(1),
cmd_reg.eq(self.rx_data[0:6]),
addr.eq(self.rx_data[0:6]),
eff_addr.eq(self.rx_data[0:6])]
@@ -333,14 +333,14 @@ class BBARegisterFile(Elaboratable):
with m.If((self.rx_data == 0x00) & id_phase
& self.tx_rdy):
m.d.comb += [self.tx_data.eq(0x04), self.tx_en.eq(1)]
m.d.exi += byte_ctr.eq(1)
m.d.sync += byte_ctr.eq(1)
m.next = "CHDR_1"
# ── register space: [0x80|w<<6, ah, al, 0x00] ──────────────────
with m.State("RHDR_HI"):
with m.If(self.rx_rdy):
m.d.comb += self.rx_en.eq(1)
m.d.exi += addr[8:16].eq(self.rx_data)
m.d.sync += addr[8:16].eq(self.rx_data)
m.next = "RHDR_LO"
with m.State("RHDR_LO"):
@@ -353,13 +353,18 @@ class BBARegisterFile(Elaboratable):
with m.If(self.rx_rdy):
m.d.comb += self.rx_en.eq(1)
full = Cat(self.rx_data, addr[8:16]) # 16-bit address
m.d.exi += [addr.eq(full), eff_addr.eq(full),
m.d.sync += [addr.eq(full), eff_addr.eq(full),
sp_addr.eq(full)]
with m.If(~is_write & (full >= 0x100) & spram_req.w_rdy):
m.d.exi += [spram_req.w_data.eq(full),
spram_req.w_en.eq(1),
sp_addr.eq(full + 1),
outstanding.eq(1)]
# COMBINATIONAL write: the turnaround byte gives only
# ~296 ns and the SPRAM round trip needs most of it, so
# a registered w_en (one more 24 MHz cycle, ~42 ns) is
# enough to miss the window and shift the whole DMA
# read by a byte. Same-domain SyncFIFO now, so this is
# legal.
m.d.comb += [spram_req.w_data.eq(full),
spram_req.w_en.eq(1)]
m.d.sync += [sp_addr.eq(full + 1), outstanding.eq(1)]
m.next = "RHDR_PAD"
with m.State("RHDR_PAD"):
@@ -382,7 +387,7 @@ class BBARegisterFile(Elaboratable):
# arrived, and repeating it would consume the register
# stream early (symptom: reads shifted by a byte).
m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)]
m.d.exi += [eff_addr.eq(eff_addr + 1), prefetched.eq(1)]
m.d.sync += [eff_addr.eq(eff_addr + 1), prefetched.eq(1)]
# ── command space: [reg, 00, 00, data] in ONE 4-byte transfer ──
with m.State("CHDR_1"):
@@ -405,11 +410,11 @@ class BBARegisterFile(Elaboratable):
# NOT one-shot -- the FIFO is only 2 deep, and stopping
# after one byte leaves a hole in the next slot.
m.d.comb += [self.tx_data.eq(devid), self.tx_en.eq(1)]
m.d.exi += byte_ctr.eq(byte_ctr + 1)
m.d.sync += byte_ctr.eq(byte_ctr + 1)
with m.Elif(~is_write & self.tx_rdy & ~prefetched):
# Ordinary command read: response lands in byte slot 3.
m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)]
m.d.exi += prefetched.eq(1)
m.d.sync += prefetched.eq(1)
with m.State("CHDR_2"):
with m.If(self.rx_rdy):
@@ -420,8 +425,8 @@ class BBARegisterFile(Elaboratable):
with m.If(self.rx_rdy):
m.d.comb += self.rx_en.eq(1)
with m.If(is_write):
m.d.exi += cmd_wr.eq(1) # applied below
m.d.exi += cmd_wdata.eq(self.rx_data)
m.d.sync += cmd_wr.eq(1) # applied below
m.d.sync += cmd_wdata.eq(self.rx_data)
m.next = "TRAILER"
with m.State("TRAILER"):
@@ -437,37 +442,37 @@ class BBARegisterFile(Elaboratable):
m.d.comb += self.rx_en.eq(1)
with m.Switch(rd_sel):
with m.Case(_NCRA):
m.d.exi += r_ncra.eq(self.rx_data)
m.d.sync += r_ncra.eq(self.rx_data)
with m.If(self.rx_data[0]):
m.d.exi += r_ncra[0].eq(0) # RESET self-clears
m.d.exi += ncra_rst_ps.i.eq(1)
m.d.exi += id_phase.eq(0)
m.d.sync += r_ncra[0].eq(0) # RESET self-clears
m.d.sync += ncra_rst_pulse.eq(1)
m.d.sync += id_phase.eq(0)
# Fallback trigger: TWD_HI normally queues the
# frame (see _TWD_HI). Only fire here if it did
# not, so software that skips TWD still transmits
# and we never double-queue one frame.
with m.If(self.rx_data[1:3].any() & ~tx_queued):
with m.If(tx_ctrl.w_rdy):
m.d.exi += tx_ctrl.w_data.eq(tx_frame_len)
m.d.exi += tx_ctrl.w_en.eq(1)
m.d.exi += tx_queued.eq(1)
m.d.sync += tx_ctrl.w_data.eq(tx_frame_len)
m.d.sync += tx_ctrl.w_en.eq(1)
m.d.sync += tx_queued.eq(1)
with m.Case(_IMR):
m.d.exi += r_imr.eq(self.rx_data)
m.d.sync += r_imr.eq(self.rx_data)
with m.Case(_IR):
m.d.exi += r_ir.eq(r_ir & ~self.rx_data) # write-1-clear
m.d.sync += r_ir.eq(r_ir & ~self.rx_data) # write-1-clear
with m.Case(_RRP_LO):
m.d.exi += r_rrp_lo.eq(self.rx_data)
m.d.sync += r_rrp_lo.eq(self.rx_data)
with m.If(rx_rptr.w_rdy):
m.d.exi += rx_rptr.w_data.eq(self.rx_data)
m.d.exi += rx_rptr.w_en.eq(1)
m.d.sync += rx_rptr.w_data.eq(self.rx_data)
m.d.sync += rx_rptr.w_en.eq(1)
with m.Case(_PAR0, _PAR1, _PAR2, _PAR3, _PAR4, _PAR5):
m.d.exi += r_par[eff_addr[0:3]].eq(self.rx_data)
m.d.sync += r_par[eff_addr[0:3]].eq(self.rx_data)
with m.Case(_TWD_LO):
m.d.exi += r_twd_lo.eq(self.rx_data)
m.d.exi += tx_frame_len[0:8].eq(self.rx_data)
m.d.sync += r_twd_lo.eq(self.rx_data)
m.d.sync += tx_frame_len[0:8].eq(self.rx_data)
with m.Case(_TWD_HI):
m.d.exi += r_twd_hi.eq(self.rx_data)
m.d.exi += tx_frame_len[8:16].eq(self.rx_data)
m.d.sync += r_twd_hi.eq(self.rx_data)
m.d.sync += tx_frame_len[8:16].eq(self.rx_data)
# Hand the length to TXFrameDrain NOW, not at the ST
# trigger. The drain blocks on tx_bytes_r_rdy, so
# starting it here lets it consume bytes AS the GC
@@ -476,16 +481,16 @@ class BBARegisterFile(Elaboratable):
# to fit in the 16-deep FIFO, and anything past 16
# bytes was silently dropped (hardware 2026-09-30:
# frames truncated at exactly 16 bytes on the wire).
m.d.exi += twd_armed.eq(1)
m.d.sync += twd_armed.eq(1)
with m.Case(_TXDATA):
with m.If(tx_bytes.w_rdy):
m.d.exi += tx_bytes.w_data.eq(self.rx_data)
m.d.exi += tx_bytes.w_en.eq(1)
m.d.sync += tx_bytes.w_data.eq(self.rx_data)
m.d.sync += tx_bytes.w_en.eq(1)
# All other addresses silently ignored
# No length field in the real protocol: consume data
# bytes until CS rises.
m.d.exi += eff_addr.eq(eff_addr + 1)
m.d.sync += eff_addr.eq(eff_addr + 1)
with m.If(~self.cs_active):
m.next = "HEADER0"
@@ -494,7 +499,7 @@ class BBARegisterFile(Elaboratable):
# path. The real protocol has no length field.
with m.If(self.tx_rdy & self.cs_active):
m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)]
m.d.exi += eff_addr.eq(eff_addr + 1)
m.d.sync += eff_addr.eq(eff_addr + 1)
with m.If(self.rx_rdy):
m.d.comb += self.rx_en.eq(1) # drain dummies
with m.If(~self.cs_active):
@@ -504,7 +509,7 @@ class BBARegisterFile(Elaboratable):
# Stream 04 02 02 00 until CS rises.
with m.If(self.tx_rdy & self.cs_active):
m.d.comb += [self.tx_data.eq(devid), self.tx_en.eq(1)]
m.d.exi += byte_ctr.eq(byte_ctr + 1)
m.d.sync += byte_ctr.eq(byte_ctr + 1)
with m.If(self.rx_rdy):
m.d.comb += self.rx_en.eq(1)
with m.If(~self.cs_active):
@@ -523,14 +528,14 @@ class BBARegisterFile(Elaboratable):
m.d.comb += pop.eq(spram_rsp.r_rdy & self.tx_rdy)
with m.If(issue):
m.d.exi += spram_req.w_data.eq(sp_addr)
m.d.exi += spram_req.w_en.eq(1)
m.d.exi += sp_addr.eq(sp_addr + 1)
m.d.comb += spram_req.w_data.eq(sp_addr)
m.d.comb += spram_req.w_en.eq(1)
m.d.sync += sp_addr.eq(sp_addr + 1)
with m.If(pop):
m.d.comb += self.tx_data.eq(spram_rsp.r_data)
m.d.comb += self.tx_en.eq(1)
m.d.comb += spram_rsp.r_en.eq(1)
m.d.exi += outstanding.eq(outstanding + issue - pop)
m.d.sync += outstanding.eq(outstanding + issue - pop)
with m.If(self.rx_rdy):
m.d.comb += self.rx_en.eq(1) # drain dummy bytes
@@ -544,7 +549,7 @@ class BBARegisterFile(Elaboratable):
# ExiCapture on the next CS assertion.
with m.If(spram_rsp.r_rdy):
m.d.comb += spram_rsp.r_en.eq(1)
m.d.exi += outstanding.eq(outstanding - 1)
m.d.sync += outstanding.eq(outstanding - 1)
with m.If(self.rx_rdy):
m.d.comb += self.rx_en.eq(1)
with m.If((outstanding == 0) & ~self.rx_rdy & ~spram_rsp.r_rdy):
@@ -552,35 +557,35 @@ class BBARegisterFile(Elaboratable):
# ── Command-space write, applied the cycle after CHDR_3 ──────────
with m.If(cmd_wr):
m.d.exi += cmd_wr.eq(0)
m.d.sync += cmd_wr.eq(0)
with m.Switch(cmd_reg):
with m.Case(0x02): m.d.exi += r_cmd_imr.eq(cmd_wdata)
with m.Case(0x03): m.d.exi += r_cmd_ir.eq(r_cmd_ir & ~cmd_wdata)
with m.Case(0x02): m.d.sync += r_cmd_imr.eq(cmd_wdata)
with m.Case(0x03): m.d.sync += r_cmd_ir.eq(r_cmd_ir & ~cmd_wdata)
# ── Interrupt output ─────────────────────────────────────────────
m.d.exi += self.exi_int_n.eq(~(r_ir & r_imr).any())
m.d.sync += self.exi_int_n.eq(~(r_ir & r_imr).any())
# ── Consume RWP updates from sync domain ──────────────────────────
with m.If(rx_wptr.r_rdy):
m.d.exi += rx_wptr.r_en.eq(1)
m.d.exi += r_rwp_lo.eq(rx_wptr.r_data)
m.d.sync += rx_wptr.r_en.eq(1)
m.d.sync += r_rwp_lo.eq(rx_wptr.r_data)
# ── PulseSynchronizer arrivals ────────────────────────────────────
with m.If(rx_irq_ps.o):
m.d.exi += r_ir[1].eq(1) # RI bit
with m.If(self.rx_irq_i):
m.d.sync += r_ir[1].eq(1) # RI bit
# ── Deferred TX queue (armed by a TWD_HI write) ──────────────────
# One cycle after TWD_HI so tx_frame_len holds both bytes. Held until
# tx_ctrl has room so a full control FIFO cannot drop the trigger.
with m.If(twd_armed & tx_ctrl.w_rdy):
m.d.exi += tx_ctrl.w_data.eq(tx_frame_len)
m.d.exi += tx_ctrl.w_en.eq(1)
m.d.exi += twd_armed.eq(0)
m.d.exi += tx_queued.eq(1)
m.d.sync += tx_ctrl.w_data.eq(tx_frame_len)
m.d.sync += tx_ctrl.w_en.eq(1)
m.d.sync += twd_armed.eq(0)
m.d.sync += tx_queued.eq(1)
with m.If(tx_irq_ps.o):
m.d.exi += r_ir[2].eq(1) # TI bit
m.d.exi += r_ncra[1:3].eq(0) # clear ST bits
m.d.exi += tx_queued.eq(0) # ready for the next frame
with m.If(self.tx_irq_i):
m.d.sync += r_ir[2].eq(1) # TI bit
m.d.sync += r_ncra[1:3].eq(0) # clear ST bits
m.d.sync += tx_queued.eq(0) # ready for the next frame
return m
@@ -615,7 +620,7 @@ if __name__ == "__main__":
while True:
en = ctx.get(reg.rx_en)
sample_tx(ctx)
await ctx.tick("exi")
await ctx.tick("sync")
if en:
break
ctx.set(reg.rx_rdy, 0)
@@ -630,7 +635,7 @@ if __name__ == "__main__":
break
if ctx.get(reg.tx_en):
out.append(ctx.get(reg.tx_data))
await ctx.tick("exi")
await ctx.tick("sync")
return out
# Real protocol (docs/exi_protocol_real.md): register space is a 4-byte
@@ -645,7 +650,7 @@ if __name__ == "__main__":
await push_rx(ctx, 0x00) # turnaround / prefetch window
result = await collect_tx(ctx, length)
ctx.set(reg.cs_active, 0)
await ctx.tick("exi").repeat(6)
await ctx.tick("sync").repeat(6)
return result
async def exi_write(ctx, addr, data):
@@ -657,7 +662,7 @@ if __name__ == "__main__":
for b in data:
await push_rx(ctx, b)
ctx.set(reg.cs_active, 0)
await ctx.tick("exi").repeat(6)
await ctx.tick("sync").repeat(6)
async def exi_devid(ctx):
"""EXI_GetID: write 0x0000 (2 bytes), then clock 4 read bytes."""
@@ -667,7 +672,7 @@ if __name__ == "__main__":
await push_rx(ctx, 0x00)
result = await collect_tx(ctx, 4)
ctx.set(reg.cs_active, 0)
await ctx.tick("exi").repeat(6)
await ctx.tick("sync").repeat(6)
return result
# SPRAM contents the streaming-read test reads back (byte i = 0xA0+i).
@@ -701,51 +706,51 @@ if __name__ == "__main__":
async def testbench(ctx):
ctx.set(reg.tx_rdy, 1) # tx_fifo always has room in this model
await ctx.tick("exi").repeat(8)
await ctx.tick("sync").repeat(8)
# T1: Device ID (addr=0, 4-byte read)
result = await exi_devid(ctx)
if result != _DEVICE_ID:
errors.append(f"T1 device ID: expected {_DEVICE_ID}, got {result}")
print(f"T1 device ID: {[f'0x{b:02X}' for b in result]}")
await ctx.tick("exi").repeat(4)
await ctx.tick("sync").repeat(4)
# T2: Write and read back PAR0-PAR3
await exi_write(ctx, _PAR0, [0xDE, 0xAD, 0xBE, 0xEF])
await ctx.tick("exi").repeat(4)
await ctx.tick("sync").repeat(4)
result = await exi_read(ctx, _PAR0, length=4)
if result != [0xDE, 0xAD, 0xBE, 0xEF]:
errors.append(f"T2 PAR readback: {result}")
print(f"T2 PAR0-3: {[f'0x{b:02X}' for b in result]}")
await ctx.tick("exi").repeat(4)
await ctx.tick("sync").repeat(4)
# T3: NWAYS hardcoded 0x17
result = await exi_read(ctx, _NWAYS, length=1)
if result != [0x17]:
errors.append(f"T3 NWAYS: expected 0x17, got {result}")
print(f"T3 NWAYS: 0x{result[0]:02X}")
await ctx.tick("exi").repeat(4)
await ctx.tick("sync").repeat(4)
# T4: HIPR hardcoded 0x01
result = await exi_read(ctx, _HIPR, length=1)
if result != [0x01]:
errors.append(f"T4 HIPR: expected 0x01, got {result}")
print(f"T4 HIPR: 0x{result[0]:02X}")
await ctx.tick("exi").repeat(4)
await ctx.tick("sync").repeat(4)
# T5: IMR write, rx_irq pulse, INT_N asserts, then IR clear
await exi_write(ctx, _IMR, [0x02]) # enable RI (bit 1)
await ctx.tick("exi").repeat(4)
await ctx.tick("sync").repeat(4)
ctx.set(reg.rx_irq_i, 1)
await ctx.tick("sync").repeat(1)
ctx.set(reg.rx_irq_i, 0)
await ctx.tick("exi").repeat(12) # wait for PS propagation
await ctx.tick("sync").repeat(12) # wait for PS propagation
int_n = ctx.get(reg.exi_int_n)
if int_n != 0:
errors.append(f"T5 INT_N after RI: expected 0, got {int_n}")
print(f"T5 INT_N after RI pulse: {int_n} (want 0)")
await exi_write(ctx, _IR, [0x02]) # write-1-to-clear RI
await ctx.tick("exi").repeat(4)
await ctx.tick("sync").repeat(4)
int_n = ctx.get(reg.exi_int_n)
if int_n != 1:
errors.append(f"T5 INT_N after clear: expected 1, got {int_n}")
@@ -767,9 +772,9 @@ if __name__ == "__main__":
got.append(ctx.get(reg.tx_data))
if len(got) >= N:
break
await ctx.tick("exi")
await ctx.tick("sync")
ctx.set(reg.cs_active, 0) # end the transaction
await ctx.tick("exi").repeat(40) # let SPRAM_END drain/clean up
await ctx.tick("sync").repeat(40) # let SPRAM_END drain/clean up
want = [spram_mem[0x100 + i] for i in range(N)]
print(f"T6 DMA read {N}B: {[f'0x{b:02X}' for b in got]}")
if got != want:
@@ -783,7 +788,6 @@ if __name__ == "__main__":
print(f"T7 NWAYS after DMA read: 0x{result[0]:02X}")
sim = Simulator(reg)
sim.add_clock(Period(MHz=24), domain="exi")
sim.add_clock(Period(MHz=24), domain="sync")
sim.add_testbench(testbench)
sim.add_process(spram_model)
+1 -4
View File
@@ -159,16 +159,14 @@ class BBATop(Elaboratable):
# The bulky register-file / SPRAM / W5500 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.
m.domains += ClockDomain("exi")
m.domains += ClockDomain("sync")
m.submodules.hfosc = Instance(
"SB_HFOSC",
p_CLKHF_DIV = "0b01", # 48 ÷ 2 → 24 MHz
i_CLKHFEN = Const(1, 1),
i_CLKHFPU = Const(1, 1),
o_CLKHF = ClockSignal("exi"),
o_CLKHF = ClockSignal("sync"),
)
m.d.comb += ClockSignal("sync").eq(ClockSignal("exi"))
# (simulation: test harness provides capture/exi/sync clocks via add_clock)
# ── Submodules ────────────────────────────────────────────────────
@@ -676,7 +674,6 @@ if __name__ == "__main__":
print(f"T8 TX frame matches byte-for-byte ({len(TX_FRAME)} bytes)")
sim = Simulator(dut)
sim.add_clock(Period(MHz=24), domain="exi")
sim.add_clock(Period(MHz=24), domain="sync")
sim.add_testbench(testbench)
sim.add_process(w5100_model)
+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"):
-1
View File
@@ -183,7 +183,6 @@ if __name__ == "__main__":
unplug(ctx, top.j4)
sim = Simulator(top)
sim.add_clock(Period(MHz=24), domain="exi")
sim.add_clock(Period(MHz=24), domain="sync")
sim.add_testbench(testbench)
with sim.write_vcd("exi_j4_merge.vcd"):