Added full design created with Claude
This commit is contained in:
@@ -0,0 +1,617 @@
|
||||
"""BBA register file — EXI domain.
|
||||
|
||||
Decodes EXI transactions (2-byte header + N data bytes), reads/writes the BBA
|
||||
register space, and owns all AsyncFIFO / PulseSynchronizer CDC primitives.
|
||||
|
||||
Transaction header format
|
||||
--------------------------
|
||||
Byte 0 [7] write_flag
|
||||
[6:0] addr[12:6]
|
||||
Byte 1 [7:2] addr[5:0]
|
||||
[1:0] xfer_len−1 (0=1B, 1=2B, 2=3B, 3=4B)
|
||||
|
||||
Addresses 0x0000–0x00FF : register file (sparse individual Signals, exi domain).
|
||||
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
|
||||
|
||||
__all__ = ["BBARegisterFile"]
|
||||
|
||||
# Register addresses
|
||||
_NCRA = 0x00
|
||||
_IMR = 0x08
|
||||
_IR = 0x09
|
||||
_RWP_LO = 0x16
|
||||
_RWP_HI = 0x17
|
||||
_RRP_LO = 0x18
|
||||
_RRP_HI = 0x19
|
||||
_PAR0 = 0x20
|
||||
_PAR1 = 0x21
|
||||
_PAR2 = 0x22
|
||||
_PAR3 = 0x23
|
||||
_PAR4 = 0x24
|
||||
_PAR5 = 0x25
|
||||
_NWAYS = 0x31
|
||||
_HIPR = 0x3A
|
||||
_TWD_LO = 0x34
|
||||
_TWD_HI = 0x35
|
||||
_TXDATA = 0x48
|
||||
|
||||
# Read-only hardcoded values
|
||||
_NWAYS_VAL = 0x17
|
||||
_HIPR_VAL = 0x01
|
||||
|
||||
# Device ID returned on first 4-byte read of addr 0x0000
|
||||
_DEVICE_ID = [0x04, 0x02, 0x02, 0x00]
|
||||
|
||||
|
||||
class BBARegisterFile(Elaboratable):
|
||||
"""EXI transaction decoder and BBA register file with CDC bridges.
|
||||
|
||||
Sync-domain FIFO/pulse ports are wired by BBATop to the sync-domain modules.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# ── EXI byte-stream interface (exi domain, from/to ExiCapture) ────
|
||||
# RX: received bytes (header + write data + read dummies) — FWFT read
|
||||
# side of ExiCapture's rx_fifo.
|
||||
self.rx_data = Signal(8)
|
||||
self.rx_rdy = Signal()
|
||||
self.rx_en = Signal()
|
||||
# TX: response bytes pushed proactively into ExiCapture's tx_fifo.
|
||||
self.tx_data = Signal(8)
|
||||
self.tx_en = Signal()
|
||||
self.tx_rdy = Signal()
|
||||
|
||||
# High while an EXI transaction is in progress (from ExiCapture).
|
||||
# SPRAM reads stream until this deasserts → supports variable-length
|
||||
# (DMA) bulk reads, not just ≤4-byte immediate transfers.
|
||||
self.cs_active = Signal()
|
||||
|
||||
# ── Interrupt (exi domain) ────────────────────────────────────────
|
||||
self.exi_int_n = Signal(init=1)
|
||||
|
||||
# ── PAR output (for forwarding to W5500 as source MAC) ───────────
|
||||
self.par = Signal(48) # PAR0-5 packed: PAR0 in low byte par[0:8]
|
||||
|
||||
# NCRA[3] = SR (start receive) bit — gates the RX ring-buffer path.
|
||||
self.ncra_sr = Signal()
|
||||
|
||||
# ── CDC FIFO sync-domain sides (wired by BBATop) ──────────────────
|
||||
# SPRAM request exi→sync: sync reads these
|
||||
self.spram_req_r_data = Signal(16)
|
||||
self.spram_req_r_en = Signal()
|
||||
self.spram_req_r_rdy = Signal()
|
||||
|
||||
# SPRAM response sync→exi: sync writes these
|
||||
self.spram_rsp_w_data = Signal(8)
|
||||
self.spram_rsp_w_en = Signal()
|
||||
self.spram_rsp_w_rdy = Signal()
|
||||
|
||||
# TX bytes exi→sync: sync reads these
|
||||
self.tx_bytes_r_data = Signal(8)
|
||||
self.tx_bytes_r_en = Signal()
|
||||
self.tx_bytes_r_rdy = Signal()
|
||||
|
||||
# TX ctrl (frame length) exi→sync: sync reads these
|
||||
self.tx_ctrl_r_data = Signal(16)
|
||||
self.tx_ctrl_r_en = Signal()
|
||||
self.tx_ctrl_r_rdy = Signal()
|
||||
|
||||
# RX write-pointer update sync→exi: sync writes these
|
||||
self.rx_wptr_w_data = Signal(8)
|
||||
self.rx_wptr_w_en = Signal()
|
||||
self.rx_wptr_w_rdy = Signal()
|
||||
|
||||
# RX read-pointer update exi→sync: sync reads these
|
||||
self.rx_rptr_r_data = Signal(8)
|
||||
self.rx_rptr_r_en = Signal()
|
||||
self.rx_rptr_r_rdy = Signal()
|
||||
|
||||
# PulseSynchronizer ports (exi↔sync)
|
||||
self.ncra_rst_o = Signal() # exi→sync
|
||||
self.rx_irq_i = Signal() # sync→exi
|
||||
self.tx_irq_i = Signal() # sync→exi
|
||||
|
||||
def elaborate(self, platform):
|
||||
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")
|
||||
|
||||
m.submodules.spram_req = spram_req
|
||||
m.submodules.spram_rsp = spram_rsp
|
||||
m.submodules.tx_bytes = tx_bytes
|
||||
m.submodules.tx_ctrl = tx_ctrl
|
||||
m.submodules.rx_wptr = rx_wptr
|
||||
m.submodules.rx_rptr = rx_rptr
|
||||
|
||||
# Expose sync-domain FIFO sides
|
||||
m.d.comb += [
|
||||
self.spram_req_r_data .eq(spram_req.r_data),
|
||||
spram_req.r_en .eq(self.spram_req_r_en),
|
||||
self.spram_req_r_rdy .eq(spram_req.r_rdy),
|
||||
|
||||
spram_rsp.w_data .eq(self.spram_rsp_w_data),
|
||||
spram_rsp.w_en .eq(self.spram_rsp_w_en),
|
||||
self.spram_rsp_w_rdy .eq(spram_rsp.w_rdy),
|
||||
|
||||
self.tx_bytes_r_data .eq(tx_bytes.r_data),
|
||||
tx_bytes.r_en .eq(self.tx_bytes_r_en),
|
||||
self.tx_bytes_r_rdy .eq(tx_bytes.r_rdy),
|
||||
|
||||
self.tx_ctrl_r_data .eq(tx_ctrl.r_data),
|
||||
tx_ctrl.r_en .eq(self.tx_ctrl_r_en),
|
||||
self.tx_ctrl_r_rdy .eq(tx_ctrl.r_rdy),
|
||||
|
||||
rx_wptr.w_data .eq(self.rx_wptr_w_data),
|
||||
rx_wptr.w_en .eq(self.rx_wptr_w_en),
|
||||
self.rx_wptr_w_rdy .eq(rx_wptr.w_rdy),
|
||||
|
||||
self.rx_rptr_r_data .eq(rx_rptr.r_data),
|
||||
rx_rptr.r_en .eq(self.rx_rptr_r_en),
|
||||
self.rx_rptr_r_rdy .eq(rx_rptr.r_rdy),
|
||||
]
|
||||
|
||||
# ── 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),
|
||||
]
|
||||
|
||||
# ── Register file (sparse individual Signals, exi domain) ────────
|
||||
# Only the registers actually read/written by the GC or sync domain.
|
||||
# Writes to unknown addresses are silently ignored; reads return 0.
|
||||
r_ncra = Signal(8)
|
||||
r_imr = Signal(8)
|
||||
r_ir = Signal(8)
|
||||
r_rwp_lo = Signal(8)
|
||||
r_rrp_lo = Signal(8)
|
||||
# PAR0–5 reset to a valid Nintendo OUI MAC (00:09:BF:00:00:01) so the
|
||||
# device has a sane source MAC even before the GC driver programs its
|
||||
# own. PAR0 is the first MAC octet.
|
||||
_par_reset = [0x00, 0x09, 0xBF, 0x00, 0x00, 0x01]
|
||||
r_par = Array([Signal(8, name=f"par{i}", init=_par_reset[i])
|
||||
for i in range(6)])
|
||||
r_twd_lo = Signal(8)
|
||||
r_twd_hi = Signal(8)
|
||||
|
||||
# PAR packed output: PAR0 in the LOW byte (par[0:8]). The W5500 master
|
||||
# reads mac_shadow[i] = par[i*8:(i+1)*8], so this puts PAR0 first in the
|
||||
# SHAR write — i.e. PAR0 is the first MAC octet on the wire.
|
||||
m.d.comb += self.par.eq(Cat(
|
||||
r_par[0], r_par[1], r_par[2], r_par[3], r_par[4], r_par[5],
|
||||
))
|
||||
m.d.comb += self.ncra_sr.eq(r_ncra[3]) # start-receive bit
|
||||
|
||||
# ── Transaction state ────────────────────────────────────────────
|
||||
hdr0 = Signal(8)
|
||||
addr = Signal(13)
|
||||
is_write = Signal()
|
||||
xfer_len = Signal(2) # 0=1B … 3=4B
|
||||
byte_ctr = Signal(2)
|
||||
tx_frame_len = Signal(16)
|
||||
|
||||
# True until first NCRA reset write: return device ID on addr=0 reads
|
||||
id_phase = Signal(init=1)
|
||||
|
||||
# Per-byte SPRAM read handshake (register-read path): sp_req marks a
|
||||
# request in flight; drain_ctr counts the read-phase dummy bytes.
|
||||
sp_req = Signal()
|
||||
drain_ctr = Signal(2)
|
||||
|
||||
# SPRAM streaming-read state (DMA / variable-length reads):
|
||||
# sp_addr — next SPRAM byte address to request (auto-increments)
|
||||
# outstanding — SPRAM requests issued but whose responses are not yet
|
||||
# popped (bounds prefetch and is drained at end)
|
||||
sp_addr = Signal(13)
|
||||
outstanding = Signal(4)
|
||||
SP_LIMIT = 4 # max prefetch depth in flight
|
||||
|
||||
# Effective address of the current data byte — a REGISTERED running
|
||||
# pointer (set to the base in HEADER1, incremented per byte). Keeping
|
||||
# it registered keeps the 13-bit adder off the combinational path that
|
||||
# feeds the read-response mux → tx_fifo write data.
|
||||
eff_addr = Signal(13)
|
||||
rd_sel = eff_addr[0:8]
|
||||
|
||||
# ── Combinational read-response value (non-SPRAM) ────────────────
|
||||
reg_rdval = Signal(8)
|
||||
with m.Switch(rd_sel):
|
||||
with m.Case(_NCRA): m.d.comb += reg_rdval.eq(r_ncra)
|
||||
with m.Case(_IMR): m.d.comb += reg_rdval.eq(r_imr)
|
||||
with m.Case(_IR): m.d.comb += reg_rdval.eq(r_ir)
|
||||
with m.Case(_RWP_LO): m.d.comb += reg_rdval.eq(r_rwp_lo)
|
||||
with m.Case(_RRP_LO): m.d.comb += reg_rdval.eq(r_rrp_lo)
|
||||
with m.Case(_PAR0, _PAR1, _PAR2, _PAR3, _PAR4, _PAR5):
|
||||
m.d.comb += reg_rdval.eq(r_par[eff_addr[0:3]])
|
||||
with m.Case(_TWD_LO): m.d.comb += reg_rdval.eq(r_twd_lo)
|
||||
with m.Case(_TWD_HI): m.d.comb += reg_rdval.eq(r_twd_hi)
|
||||
with m.Case(_NWAYS): m.d.comb += reg_rdval.eq(_NWAYS_VAL)
|
||||
with m.Case(_HIPR): m.d.comb += reg_rdval.eq(_HIPR_VAL)
|
||||
with m.Default(): m.d.comb += reg_rdval.eq(0)
|
||||
|
||||
# Device-ID bytes (addr 0 read while id_phase): 0x04 0x02 0x02 0x00
|
||||
devid = Signal(8)
|
||||
with m.Switch(byte_ctr):
|
||||
with m.Case(0): m.d.comb += devid.eq(0x04)
|
||||
with m.Case(1): m.d.comb += devid.eq(0x02)
|
||||
with m.Case(2): m.d.comb += devid.eq(0x02)
|
||||
with m.Case(3): m.d.comb += devid.eq(0x00)
|
||||
|
||||
rd_val = Signal(8) # response for the current non-SPRAM read byte
|
||||
with m.If((addr == 0) & id_phase):
|
||||
m.d.comb += rd_val.eq(devid)
|
||||
with m.Else():
|
||||
m.d.comb += rd_val.eq(reg_rdval)
|
||||
|
||||
# ── Default strobes ──────────────────────────────────────────────
|
||||
m.d.exi += [
|
||||
spram_req.w_en .eq(0),
|
||||
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),
|
||||
]
|
||||
m.d.comb += [
|
||||
self.rx_en .eq(0),
|
||||
self.tx_en .eq(0),
|
||||
self.tx_data.eq(0),
|
||||
# Combinational so the FIFO advances in the SAME cycle as the pop —
|
||||
# a registered r_en would let `pop` re-fire on the same byte.
|
||||
spram_rsp.r_en.eq(0),
|
||||
]
|
||||
|
||||
# ── Transaction FSM (proactive push/pull over byte FIFOs) ────────
|
||||
# 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.State("HEADER0"):
|
||||
with m.If(self.rx_rdy):
|
||||
m.d.comb += self.rx_en.eq(1)
|
||||
m.d.exi += hdr0.eq(self.rx_data)
|
||||
m.next = "HEADER1"
|
||||
|
||||
with m.State("HEADER1"):
|
||||
with m.If(self.rx_rdy):
|
||||
m.d.comb += self.rx_en.eq(1)
|
||||
new_addr = Cat(self.rx_data[2:8], hdr0[0:7]) # 13-bit addr
|
||||
new_len = self.rx_data[0:2]
|
||||
new_write = hdr0[7]
|
||||
|
||||
m.d.exi += addr.eq(new_addr)
|
||||
m.d.exi += eff_addr.eq(new_addr) # running pointer init
|
||||
m.d.exi += xfer_len.eq(new_len)
|
||||
m.d.exi += is_write.eq(new_write)
|
||||
m.d.exi += byte_ctr.eq(0)
|
||||
m.d.exi += sp_req.eq(0)
|
||||
m.d.exi += drain_ctr.eq(0)
|
||||
|
||||
with m.If(new_write):
|
||||
m.next = "WRITE"
|
||||
with m.Elif(new_addr >= 0x100):
|
||||
# SPRAM region: stream until CS deasserts (DMA-capable).
|
||||
m.d.exi += sp_addr.eq(new_addr)
|
||||
m.d.exi += outstanding.eq(0)
|
||||
m.next = "SPRAM_STREAM"
|
||||
with m.Else():
|
||||
m.next = "REG_READ"
|
||||
|
||||
with m.State("WRITE"):
|
||||
# Consume xfer_len+1 data bytes, writing the register file.
|
||||
with m.If(self.rx_rdy):
|
||||
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)
|
||||
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)
|
||||
with m.If(self.rx_data[1:3].any()):
|
||||
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)
|
||||
with m.Case(_IMR):
|
||||
m.d.exi += r_imr.eq(self.rx_data)
|
||||
with m.Case(_IR):
|
||||
m.d.exi += 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)
|
||||
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)
|
||||
with m.Case(_PAR0, _PAR1, _PAR2, _PAR3, _PAR4, _PAR5):
|
||||
m.d.exi += 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)
|
||||
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)
|
||||
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)
|
||||
# All other addresses silently ignored
|
||||
|
||||
with m.If(byte_ctr == xfer_len):
|
||||
m.next = "HEADER0"
|
||||
with m.Else():
|
||||
m.d.exi += byte_ctr.eq(byte_ctr + 1)
|
||||
m.d.exi += eff_addr.eq(eff_addr + 1)
|
||||
|
||||
with m.State("REG_READ"):
|
||||
# Register / device-ID read (addr < 0x100): value available
|
||||
# immediately, bounded by the header's xfer_len (≤4 bytes).
|
||||
with m.If(self.tx_rdy):
|
||||
m.d.comb += self.tx_data.eq(rd_val)
|
||||
m.d.comb += self.tx_en.eq(1)
|
||||
with m.If(byte_ctr == xfer_len):
|
||||
m.next = "READ_DRAIN"
|
||||
with m.Else():
|
||||
m.d.exi += byte_ctr.eq(byte_ctr + 1)
|
||||
m.d.exi += eff_addr.eq(eff_addr + 1)
|
||||
|
||||
with m.State("READ_DRAIN"):
|
||||
# Discard the xfer_len+1 dummy bytes the GC clocks while reading.
|
||||
with m.If(self.rx_rdy):
|
||||
m.d.comb += self.rx_en.eq(1)
|
||||
with m.If(drain_ctr == xfer_len):
|
||||
m.next = "HEADER0"
|
||||
with m.Else():
|
||||
m.d.exi += drain_ctr.eq(drain_ctr + 1)
|
||||
|
||||
with m.State("SPRAM_STREAM"):
|
||||
# Stream SPRAM bytes until CS deasserts — handles both ≤4-byte
|
||||
# immediate reads and arbitrary-length DMA reads uniformly.
|
||||
# Issue read requests ahead (prefetch, bounded by SP_LIMIT) and
|
||||
# push responses into tx_fifo; the capture domain pops them as
|
||||
# the GC clocks. Drain rx dummies as they arrive.
|
||||
issue = Signal()
|
||||
pop = Signal()
|
||||
m.d.comb += issue.eq(self.cs_active & spram_req.w_rdy
|
||||
& (outstanding < SP_LIMIT))
|
||||
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)
|
||||
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)
|
||||
|
||||
with m.If(self.rx_rdy):
|
||||
m.d.comb += self.rx_en.eq(1) # drain dummy bytes
|
||||
|
||||
with m.If(~self.cs_active):
|
||||
m.next = "SPRAM_END"
|
||||
|
||||
with m.State("SPRAM_END"):
|
||||
# CS deasserted: drain in-flight SPRAM responses and rx dummies,
|
||||
# then idle. Leftover prefetch in tx_fifo is flushed by
|
||||
# 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)
|
||||
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):
|
||||
m.next = "HEADER0"
|
||||
|
||||
# ── Interrupt output ─────────────────────────────────────────────
|
||||
m.d.exi += 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)
|
||||
|
||||
# ── PulseSynchronizer arrivals ────────────────────────────────────
|
||||
with m.If(rx_irq_ps.o):
|
||||
m.d.exi += r_ir[1].eq(1) # RI bit
|
||||
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
|
||||
|
||||
return m
|
||||
|
||||
|
||||
# ── Testbench ─────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
from amaranth.sim import Simulator, Period
|
||||
|
||||
reg = BBARegisterFile()
|
||||
|
||||
# Drive the byte-stream interface directly (the SPI bit cadence and FIFOs
|
||||
# live in ExiCapture; here we model the byte producer/consumer).
|
||||
async def push_rx(ctx, b):
|
||||
"""Present one received byte and wait for the register file to pop it."""
|
||||
ctx.set(reg.rx_data, b)
|
||||
ctx.set(reg.rx_rdy, 1)
|
||||
while True:
|
||||
en = ctx.get(reg.rx_en)
|
||||
await ctx.tick("exi")
|
||||
if en:
|
||||
break
|
||||
ctx.set(reg.rx_rdy, 0)
|
||||
|
||||
async def collect_tx(ctx, n):
|
||||
"""Collect n response bytes pushed by the register file (bounded)."""
|
||||
out = []
|
||||
for _ in range(3000):
|
||||
if ctx.get(reg.tx_en):
|
||||
out.append(ctx.get(reg.tx_data))
|
||||
if len(out) >= n:
|
||||
break
|
||||
await ctx.tick("exi")
|
||||
return out
|
||||
|
||||
async def exi_read(ctx, addr, length=1):
|
||||
hdr0 = (addr >> 6) & 0x7F
|
||||
hdr1 = ((addr & 0x3F) << 2) | (length - 1)
|
||||
await push_rx(ctx, hdr0)
|
||||
await push_rx(ctx, hdr1)
|
||||
result = await collect_tx(ctx, length) # READ pushes `length` bytes
|
||||
for _ in range(length): # READ_DRAIN dummies
|
||||
await push_rx(ctx, 0x00)
|
||||
return result
|
||||
|
||||
async def exi_write(ctx, addr, data):
|
||||
hdr0 = 0x80 | ((addr >> 6) & 0x7F)
|
||||
hdr1 = ((addr & 0x3F) << 2) | (len(data) - 1)
|
||||
await push_rx(ctx, hdr0)
|
||||
await push_rx(ctx, hdr1)
|
||||
for b in data:
|
||||
await push_rx(ctx, b)
|
||||
|
||||
# SPRAM contents the streaming-read test reads back (byte i = 0xA0+i).
|
||||
spram_mem = {0x100 + i: (0xA0 + i) & 0xFF for i in range(64)}
|
||||
|
||||
async def spram_model(ctx):
|
||||
"""Model the SPRAM (sync side): answer spram_req with mem[addr].
|
||||
|
||||
One request at a time, with cleanly-pulsed r_en/w_en so the FIFO pop
|
||||
and the response push stay in lock-step (no double-response races).
|
||||
"""
|
||||
state = "POP"
|
||||
held = 0
|
||||
async for vals in ctx.tick("sync").sample(
|
||||
reg.spram_req_r_rdy, reg.spram_req_r_data, reg.spram_rsp_w_rdy):
|
||||
rdy, addr, rsp_rdy = vals[-3:]
|
||||
ctx.set(reg.spram_req_r_en, 0)
|
||||
ctx.set(reg.spram_rsp_w_en, 0)
|
||||
if state == "POP":
|
||||
if rdy:
|
||||
held = spram_mem.get(addr, 0)
|
||||
ctx.set(reg.spram_req_r_en, 1) # consume the request
|
||||
state = "RESP"
|
||||
else: # RESP
|
||||
if rsp_rdy:
|
||||
ctx.set(reg.spram_rsp_w_data, held)
|
||||
ctx.set(reg.spram_rsp_w_en, 1) # deliver the response
|
||||
state = "POP"
|
||||
|
||||
errors = []
|
||||
|
||||
async def testbench(ctx):
|
||||
ctx.set(reg.tx_rdy, 1) # tx_fifo always has room in this model
|
||||
await ctx.tick("exi").repeat(8)
|
||||
|
||||
# T1: Device ID (addr=0, 4-byte read)
|
||||
result = await exi_read(ctx, 0x0000, length=4)
|
||||
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)
|
||||
|
||||
# T2: Write and read back PAR0-PAR3
|
||||
await exi_write(ctx, _PAR0, [0xDE, 0xAD, 0xBE, 0xEF])
|
||||
await ctx.tick("exi").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)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
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
|
||||
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)
|
||||
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}")
|
||||
print(f"T5 INT_N after IR clear: {int_n} (want 1)")
|
||||
|
||||
# T6: streaming SPRAM read (DMA) — read N>4 bytes from 0x100 by holding
|
||||
# cs_active and clocking past the header's 4-byte length field.
|
||||
N = 12
|
||||
ctx.set(reg.cs_active, 1)
|
||||
await push_rx(ctx, 0x04) # hdr0 → addr[12:6]; addr 0x100, read
|
||||
await push_rx(ctx, 0x00) # hdr1 → addr[5:0]=0, len field ignored
|
||||
got = []
|
||||
for _ in range(5000):
|
||||
if ctx.get(reg.tx_en):
|
||||
got.append(ctx.get(reg.tx_data))
|
||||
if len(got) >= N:
|
||||
break
|
||||
await ctx.tick("exi")
|
||||
ctx.set(reg.cs_active, 0) # end the transaction
|
||||
await ctx.tick("exi").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:
|
||||
errors.append(f"T6 streaming SPRAM read: got {got}, want {want}")
|
||||
|
||||
# T7: a normal register read still works after the streaming transaction
|
||||
# (FSM cleaned up and returned to HEADER0)
|
||||
result = await exi_read(ctx, _NWAYS, length=1)
|
||||
if result != [0x17]:
|
||||
errors.append(f"T7 NWAYS after DMA: got {result}")
|
||||
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)
|
||||
|
||||
sim.run()
|
||||
|
||||
if errors:
|
||||
print("\nFAILURES:")
|
||||
for e in errors:
|
||||
print(" ", e)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("\nAll tests passed.")
|
||||
Reference in New Issue
Block a user