"""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.")