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:
+105
-101
@@ -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
@@ -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)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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"):
|
||||
|
||||
Reference in New Issue
Block a user