Added full design created with Claude

This commit is contained in:
Dennis Brentjes
2026-06-13 18:35:38 +02:00
parent 57b5b471b8
commit 8d0ab1d948
30 changed files with 7424 additions and 395 deletions
+617
View File
@@ -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_len1 (0=1B, 1=2B, 2=3B, 3=4B)
Addresses 0x00000x00FF : register file (sparse individual Signals, exi domain).
Addresses 0x01000x1FFF : 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)
# PAR05 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.")