"""BBA register file — the GameCube-facing side of the adapter. Sits between the EXI byte stream (`ExiCapture`) and the ethernet side, and does three jobs: * decode EXI transactions and drive the register space; * buffer TX frame bytes and RX ring pointers across to the ethernet engines through `SyncFIFO`s; * raise the two levels of BBA interrupt. Everything here runs in the `sync` domain (24 MHz). The per-bit SPI cadence is `SPIMode3Slave`'s problem; this module only ever sees whole bytes. EXI transaction formats ----------------------- Byte 0 selects the address space (bit 7) and the direction (bit 6). There is no length field: the data phase runs for as long as the host keeps the clock running, and ends when CS rises. command space (6-bit register) 2-byte header, then a data phase read [reg, 0x00] + N bytes out write [0x40|reg, 0x00] + N bytes in register space (16-bit register) 4-byte header, then a data phase read [0x80, hi, lo, 0x00] + N bytes out write [0xC0, hi, lo, 0x00] + N bytes in The trailing `0x00` of a register-space header is the device's prefetch window: the address is complete one whole byte before the data phase, which is the only slack available to fetch the first response byte. Address spaces -------------- command 0x00-0x3F EXI-level registers (interrupt mask/status, revision) register 0x0000-0x00FF MAC registers (this file) register 0x0100-0x1FFF SPRAM RX ring; any read here streams until CS Device-ID queries (command space, read, register 0) are answered by `SPIMode3Slave`, not here — a 2-byte header leaves too little time to fetch a reply through the byte FIFOs. This file only consumes the bytes. """ from types import SimpleNamespace from amaranth import * from amaranth.lib.fifo import SyncFIFO __all__ = ["BBARegisterFile", "NCRB_PR"] # ─────────────────────────── register space ────────────────────────────── # MAC-level registers. Multi-byte registers are a LO/HI pair of consecutive # addresses; only the low byte of each page pointer is ever non-zero here # because the ring is 15 pages. _NCRA = 0x00 # Network Control A: [0] RESET (self-clearing), # [1] ST0 / [2] ST1 start transmit, [3] SR start receive _NCRB = 0x01 # Network Control B: RX filtering (PR/PM/AB) and the # receive-interrupt packet counter _IMR = 0x08 # Interrupt Mask: enables the matching IR bits _IR = 0x09 # Interrupt status, write-1-to-clear: # [1] RI frame received, [2] TI frame transmitted, # [7] RBFI receive buffer full (a frame was dropped) _BP_LO = 0x0A # Boundary Page: first page of the RX ring _BP_HI = 0x0B _TLBP_LO = 0x0C # TX Low Boundary Page: start of the TX buffer region _TLBP_HI = 0x0D _RXINTT_LO = 0x14 # RX Interrupt Timer: interrupt coalescing delay _RXINTT_HI = 0x15 _RWP_LO = 0x16 # RX Write Pointer (page) — device-owned, follows the frame # assembler. The high byte (0x17) is not decoded: the # ring is 15 pages, so it is always zero and the read # mux's default already returns that. _RRP_LO = 0x18 # RX Read Pointer (page) — host-owned, written once it has # consumed a frame. High byte (0x19) as above. _RHBP_LO = 0x1A # RX High Boundary Page: last page of the RX ring _RHBP_HI = 0x1B _PAR0 = 0x20 # Physical Address Registers 0-5: the MAC, PAR0 first on _PAR1 = 0x21 # the wire. Also forwarded to the ethernet chip as its _PAR2 = 0x22 # own source address. _PAR3 = 0x23 _PAR4 = 0x24 _PAR5 = 0x25 _NWAYC = 0x30 # NWAY (autonegotiation) configuration _NWAYS = 0x31 # NWAY status — read-only, see _NWAYS_VAL _GCA = 0x32 # GMAC Configuration A _TWD_LO = 0x34 # Transmit frame length, written before streaming TXDATA. _TWD_HI = 0x35 # Optional: see the TX length note on `txdata_cnt`. _HIPR = 0x3A # Adapter-present sentinel — read-only, see _HIPR_VAL _MISC = 0x3D # MISC control 1 _TXDATA = 0x48 # TX FIFO data port: the host streams frame bytes here _MISC2 = 0x50 # MISC control 2 # First page of the SPRAM RX ring. Page 0 holds the register region, so the # ring is pages 1-15. Must match rx_frame_assembler._RX_PAGE_FIRST. _RX_PAGE_FIRST = 0x01 # Lowest address that reads from SPRAM rather than the register file. _SPRAM_BASE = 0x100 # Read-only register values. _NWAYS_VAL = 0x17 # 100 Mbit full duplex, link up, autonegotiation complete _HIPR_VAL = 0x01 # adapter present # Reset values. NWAYC and MISC are the documented hardware defaults; BP and # RHBP describe OUR ring layout, so a host that reads them without writing # sees bounds that match where frames actually land. _NWAYC_VAL = 0x84 _MISC_VAL = 0x3C _BP_VAL = _RX_PAGE_FIRST _RHBP_VAL = 0x0F # NCRB bit positions. PR is public because BBATop steers the ethernet chip's # address filter from it. NCRB_PR = 0 # promiscuous: accept every frame _NCRB_PM = 2 # pass multicast _NCRB_AB = 4 # accept broadcast # IR / IMR bit positions. _IR_RI = 1 # frame received _IR_TI = 2 # frame transmitted _IR_RBFI = 7 # receive buffer full, frame dropped # ─────────────────────────── command space ────────────────────────────── # EXI-level registers, addressed with the 2-byte header. Only three have # behaviour; the rest are storage so that a write is not silently lost. _CMD_REVID = 0x01 # revision ID — read-only _CMD_IMR = 0x02 # EXI interrupt mask, gates INT_N _CMD_IR = 0x03 # EXI interrupt status, write-1-to-clear. # Bit 7 is a live level: "a MAC interrupt is pending". _CMD_LAST = 0x0F # highest command register with storage; above this reads 0 _CMD_REVID_VAL = 0x00 # the BBA was never revised past 0 _CMD_IR_MAC = 7 # bit 7 of _CMD_IR # Interrupt-mask reset value: unmasked. Hosts set this explicitly during # init, so a permissive default costs them nothing and leaves INT_N usable for # software that never writes it. _CMD_IMR_VAL = 0xF8 class BBARegisterFile(Elaboratable): """EXI transaction decoder, BBA register space, and the FIFO bridges. Ports fall into four groups: EXI byte stream — `rx_*`/`tx_*`/`cs_active`, driven by `ExiCapture`. `rx_*` is a first-word-fallthrough read side; `tx_*` is a write side. `cs_active` is high for the duration of one EXI transaction and is what ends every data phase. Ethernet side — the `*_r_*` / `*_w_*` groups are the far sides of this module's FIFOs, wired by `BBATop` to the frame assembler, the TX drain and the SPRAM arbiter. Register taps — `par`, `ncra_sr`, `ncrb`, `bp`, `rhbp`, `rrp` expose register contents that other modules need directly. Events — `rx_irq_i`, `tx_irq_i`, `rx_overflow` in; `ncra_rst_o` and `exi_int_n` out. """ def __init__(self): # ── EXI byte stream (from/to ExiCapture) ───────────────────────── self.rx_data = Signal(8) self.rx_rdy = Signal() self.rx_en = Signal() self.tx_data = Signal(8) self.tx_en = Signal() self.tx_rdy = Signal() self.cs_active = Signal() # ── Register taps consumed by other modules ────────────────────── self.par = Signal(48) # PAR0-5 packed, PAR0 in the low byte self.ncra_sr = Signal() # NCRA[3]: gates the RX ring path self.ncrb = Signal(8) # RX filtering / interrupt-counter bits self.bp = Signal(12) # first page of the RX ring self.rhbp = Signal(12) # last page of the RX ring self.rrp = Signal(8, init=_RX_PAGE_FIRST) # host read pointer # ── Events ─────────────────────────────────────────────────────── self.rx_overflow = Signal() # in: a frame was dropped (RBFI) self.rx_irq_i = Signal() # in: frame received (RI) self.tx_irq_i = Signal() # in: frame transmitted (TI) self.ncra_rst_o = Signal() # out: host wrote the NCRA reset bit self.exi_int_n = Signal(init=1) # out: EXI interrupt, active low # ── FIFO far sides, wired by BBATop ────────────────────────────── # SPRAM read requests out to the arbiter. self.spram_req_r_data = Signal(16) self.spram_req_r_en = Signal() self.spram_req_r_rdy = Signal() # SPRAM read data back in. No FIFO: the response must be consumed in # the cycle it is valid, which is why only one request is in flight. self.spram_rsp_data = Signal(8) self.spram_rsp_valid = Signal() # TX frame bytes and the frame length, out to TXFrameDrain. self.tx_bytes_r_data = Signal(8) self.tx_bytes_r_en = Signal() self.tx_bytes_r_rdy = Signal() self.tx_ctrl_r_data = Signal(16) self.tx_ctrl_r_en = Signal() self.tx_ctrl_r_rdy = Signal() # RX ring write pointer in from the assembler. The read pointer goes # back out through the `rrp` tap above, not a FIFO. self.rx_wptr_w_data = Signal(8) self.rx_wptr_w_en = Signal() self.rx_wptr_w_rdy = Signal() # ──────────────────────────── structure ───────────────────────────── def _make_fifos(self, m): """Create the FIFOs and connect their far sides to this module's ports.""" f = SimpleNamespace( spram_req=SyncFIFO(width=16, depth=4), # Deep enough for a whole maximum-size frame: SPI cannot # back-pressure, so a byte that arrives when the FIFO is full is # dropped and the frame goes out corrupt. The depth is what # stands between a drain stall and a bad frame, not a tuning knob. tx_bytes=SyncFIFO(width=8, depth=2048), tx_ctrl=SyncFIFO(width=16, depth=4), rx_wptr=SyncFIFO(width=8, depth=4), ) m.submodules.spram_req = f.spram_req m.submodules.tx_bytes = f.tx_bytes m.submodules.tx_ctrl = f.tx_ctrl m.submodules.rx_wptr = f.rx_wptr m.d.comb += [ self.spram_req_r_data.eq(f.spram_req.r_data), f.spram_req.r_en .eq(self.spram_req_r_en), self.spram_req_r_rdy .eq(f.spram_req.r_rdy), self.tx_bytes_r_data.eq(f.tx_bytes.r_data), f.tx_bytes.r_en .eq(self.tx_bytes_r_en), self.tx_bytes_r_rdy .eq(f.tx_bytes.r_rdy), self.tx_ctrl_r_data.eq(f.tx_ctrl.r_data), f.tx_ctrl.r_en .eq(self.tx_ctrl_r_en), self.tx_ctrl_r_rdy .eq(f.tx_ctrl.r_rdy), f.rx_wptr.w_data .eq(self.rx_wptr_w_data), f.rx_wptr.w_en .eq(self.rx_wptr_w_en), self.rx_wptr_w_rdy .eq(f.rx_wptr.w_rdy), ] return f def _make_registers(self, m): """Create register storage and the taps that read straight out of it.""" # PAR resets to a valid Nintendo OUI so the adapter has a usable # source MAC before any host writes one. Hosts generally READ the MAC # from here rather than setting it, so this value is the one that ends # up on the wire. par_reset = [0x00, 0x09, 0xBF, 0x00, 0x00, 0x01] r = SimpleNamespace( ncra=Signal(8), ncrb=Signal(8), imr=Signal(8), ir=Signal(8), # Both ring pointers reset to the ring's first page, matching the # frame assembler. RRP == RWP == first page means "empty". rwp_lo=Signal(8, init=_RX_PAGE_FIRST), rrp_lo=Signal(8, init=_RX_PAGE_FIRST), par=Array([Signal(8, name=f"par{i}", init=par_reset[i]) for i in range(6)]), twd_lo=Signal(8), twd_hi=Signal(8), bp_lo=Signal(8, init=_BP_VAL), bp_hi=Signal(8), tlbp_lo=Signal(8), tlbp_hi=Signal(8), rxintt_lo=Signal(8), rxintt_hi=Signal(8), rhbp_lo=Signal(8, init=_RHBP_VAL), rhbp_hi=Signal(8), nwayc=Signal(8, init=_NWAYC_VAL), gca=Signal(8), misc=Signal(8, init=_MISC_VAL), misc2=Signal(8), # Command-space registers. cmd_imr=Signal(8, init=_CMD_IMR_VAL), cmd_ir=Signal(8), cmd_misc=Array([Signal(8, name=f"cmd_misc_{i}") for i in range(16)]), ) return r def _register_taps(self, m, r): """Drive the ports that mirror register contents.""" 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])), self.ncra_sr.eq(r.ncra[3]), self.ncrb .eq(r.ncrb), self.rrp .eq(r.rrp_lo), self.bp .eq(Cat(r.bp_lo, r.bp_hi[0:4])), self.rhbp .eq(Cat(r.rhbp_lo, r.rhbp_hi[0:4])), ] def _read_mux(self, m, r, t): """Combinational read data for the current register address. Returns `rd_val`, valid for both address spaces: `t.cmd_space` selects which decode applies. SPRAM reads do not come through here. """ reg_rd = Signal(8, name="reg_rdval") with m.Switch(t.rd_sel): with m.Case(_NCRA): m.d.comb += reg_rd.eq(r.ncra) with m.Case(_NCRB): m.d.comb += reg_rd.eq(r.ncrb) with m.Case(_IMR): m.d.comb += reg_rd.eq(r.imr) with m.Case(_IR): m.d.comb += reg_rd.eq(r.ir) with m.Case(_BP_LO): m.d.comb += reg_rd.eq(r.bp_lo) with m.Case(_BP_HI): m.d.comb += reg_rd.eq(r.bp_hi) with m.Case(_TLBP_LO): m.d.comb += reg_rd.eq(r.tlbp_lo) with m.Case(_TLBP_HI): m.d.comb += reg_rd.eq(r.tlbp_hi) with m.Case(_RXINTT_LO): m.d.comb += reg_rd.eq(r.rxintt_lo) with m.Case(_RXINTT_HI): m.d.comb += reg_rd.eq(r.rxintt_hi) with m.Case(_RWP_LO): m.d.comb += reg_rd.eq(r.rwp_lo) with m.Case(_RRP_LO): m.d.comb += reg_rd.eq(r.rrp_lo) with m.Case(_RHBP_LO): m.d.comb += reg_rd.eq(r.rhbp_lo) with m.Case(_RHBP_HI): m.d.comb += reg_rd.eq(r.rhbp_hi) with m.Case(_PAR0, _PAR1, _PAR2, _PAR3, _PAR4, _PAR5): # PAR0 is 0x20, so the low three address bits are the index. m.d.comb += reg_rd.eq(r.par[t.eff_addr[0:3]]) with m.Case(_NWAYC): m.d.comb += reg_rd.eq(r.nwayc) with m.Case(_NWAYS): m.d.comb += reg_rd.eq(_NWAYS_VAL) with m.Case(_GCA): m.d.comb += reg_rd.eq(r.gca) with m.Case(_TWD_LO): m.d.comb += reg_rd.eq(r.twd_lo) with m.Case(_TWD_HI): m.d.comb += reg_rd.eq(r.twd_hi) with m.Case(_HIPR): m.d.comb += reg_rd.eq(_HIPR_VAL) with m.Case(_MISC): m.d.comb += reg_rd.eq(r.misc) with m.Case(_MISC2): m.d.comb += reg_rd.eq(r.misc2) with m.Default(): m.d.comb += reg_rd.eq(0) cmd_rd = Signal(8, name="cmd_rdval") with m.Switch(t.cmd_reg): with m.Case(_CMD_REVID): m.d.comb += cmd_rd.eq(_CMD_REVID_VAL) with m.Case(_CMD_IMR): m.d.comb += cmd_rd.eq(r.cmd_imr) with m.Case(_CMD_IR): m.d.comb += cmd_rd.eq(t.cmd_ir_val) with m.Default(): with m.If(t.cmd_reg <= _CMD_LAST): m.d.comb += cmd_rd.eq(r.cmd_misc[t.cmd_reg[0:4]]) with m.Else(): m.d.comb += cmd_rd.eq(0) rd_val = Signal(8, name="rd_val") with m.If(t.cmd_space): m.d.comb += rd_val.eq(cmd_rd) with m.Else(): m.d.comb += rd_val.eq(reg_rd) return rd_val def _register_write(self, m, r, f, t): """Apply one register-space write of `rx_data` to address `rd_sel`. Called from the WRITE state. Addresses with no case are ignored. """ with m.Switch(t.rd_sel): with m.Case(_NCRA): m.d.sync += r.ncra.eq(self.rx_data) with m.If(self.rx_data[0]): # RESET: self-clearing m.d.sync += [ r.ncra[0].eq(0), t.ncra_rst.eq(1), # The reset rewinds the ring, so both pointers must # follow the assembler back to the first page. r.rwp_lo.eq(_RX_PAGE_FIRST), r.rrp_lo.eq(_RX_PAGE_FIRST), ] with m.If(self.rx_data[1:3].any() & ~t.tx_queued): # ST0/ST1: start transmit. Either bit will do; the buffer # select they encode is not implemented. The length is # the number of bytes the host actually streamed, since a # host that reaches this path never wrote TWD. with m.If(f.tx_ctrl.w_rdy): m.d.sync += [ f.tx_ctrl.w_data.eq(t.txdata_cnt), f.tx_ctrl.w_en.eq(1), t.tx_queued.eq(1), t.txdata_cnt.eq(0), ] with m.Case(_IMR): m.d.sync += r.imr.eq(self.rx_data) with m.Case(_IR): m.d.sync += r.ir.eq(r.ir & ~self.rx_data) # write-1-to-clear with m.Case(_RRP_LO): # The assembler sees this through the `rrp` tap. m.d.sync += r.rrp_lo.eq(self.rx_data) with m.Case(_PAR0, _PAR1, _PAR2, _PAR3, _PAR4, _PAR5): m.d.sync += r.par[t.eff_addr[0:3]].eq(self.rx_data) with m.Case(_TWD_LO): m.d.sync += [r.twd_lo.eq(self.rx_data), t.tx_frame_len[0:8].eq(self.rx_data)] with m.Case(_TWD_HI): # Arms the deferred queue one cycle later, by which point both # halves of the length are present. m.d.sync += [r.twd_hi.eq(self.rx_data), t.tx_frame_len[8:16].eq(self.rx_data), t.twd_armed.eq(1)] with m.Case(_TXDATA): with m.If(f.tx_bytes.w_rdy): m.d.sync += [f.tx_bytes.w_data.eq(self.rx_data), f.tx_bytes.w_en.eq(1), t.txdata_cnt.eq(t.txdata_cnt + 1)] with m.Case(_NCRB): m.d.sync += r.ncrb.eq(self.rx_data) with m.Case(_BP_LO): m.d.sync += r.bp_lo.eq(self.rx_data) with m.Case(_BP_HI): m.d.sync += r.bp_hi.eq(self.rx_data) with m.Case(_TLBP_LO): m.d.sync += r.tlbp_lo.eq(self.rx_data) with m.Case(_TLBP_HI): m.d.sync += r.tlbp_hi.eq(self.rx_data) with m.Case(_RXINTT_LO): m.d.sync += r.rxintt_lo.eq(self.rx_data) with m.Case(_RXINTT_HI): m.d.sync += r.rxintt_hi.eq(self.rx_data) with m.Case(_RHBP_LO): m.d.sync += r.rhbp_lo.eq(self.rx_data) with m.Case(_RHBP_HI): m.d.sync += r.rhbp_hi.eq(self.rx_data) with m.Case(_NWAYC): m.d.sync += r.nwayc.eq(self.rx_data) with m.Case(_GCA): m.d.sync += r.gca.eq(self.rx_data) with m.Case(_MISC): m.d.sync += r.misc.eq(self.rx_data) with m.Case(_MISC2): m.d.sync += r.misc2.eq(self.rx_data) # TXDATA is a FIFO port, not a register: the address must not advance # across it or a multi-byte frame write would spill into 0x49 upwards. with m.If(t.rd_sel != _TXDATA): m.d.sync += t.eff_addr.eq(t.eff_addr + 1) def _transaction_fsm(self, m, r, f, t, rd_val): """Decode one EXI transaction. States, in the order a transaction visits them: HEADER0 byte 0: pick address space and direction RHDR_HI register space, address high byte RHDR_LO register space, address low byte. The address is now complete, so an SPRAM read is issued here to use the pad byte as its prefetch window. RHDR_PAD register space, trailing pad byte. A register read pushes its first response byte during it. CHDR_1 command space, header byte 1 CMD_READ command space read data phase CMD_WRITE command space write data phase WRITE register space write data phase REG_READ register space read data phase SPRAM_STREAM register space read from the RX ring, one request in flight, streaming until CS rises SPRAM_END drain the in-flight SPRAM request after CS DEVID device-ID query; the reply comes from SPIMode3Slave so this only consumes the clocked bytes TRAILER consume anything else until CS rises Every data phase ends on `~cs_active`, never on a byte count. """ with m.FSM(domain="sync", name="exi_fsm"): with m.State("HEADER0"): with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) m.d.sync += [ t.is_write.eq(self.rx_data[6]), t.prefetched.eq(0), t.outstanding.eq(0), t.sp_pending.eq(0), t.cmd_have.eq(0), t.cmd_pushes.eq(0), ] with m.If(self.rx_data[7]): m.d.sync += t.cmd_space.eq(0) m.next = "RHDR_HI" with m.Else(): m.d.sync += [ t.cmd_space.eq(1), t.cmd_reg.eq(self.rx_data[0:6]), t.addr.eq(self.rx_data[0:6]), t.eff_addr.eq(self.rx_data[0:6]), ] m.next = "CHDR_1" with m.State("RHDR_HI"): with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) m.d.sync += t.addr[8:16].eq(self.rx_data) m.next = "RHDR_LO" with m.State("RHDR_LO"): with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) full = Cat(self.rx_data, t.addr[8:16]) m.d.sync += [t.addr.eq(full), t.eff_addr.eq(full), t.sp_addr.eq(full)] with m.If(~t.is_write & (full >= _SPRAM_BASE) & f.spram_req.w_rdy): # Combinational w_en: the pad byte is the whole budget # for this round trip and a registered strobe does not # fit in it. m.d.comb += [f.spram_req.w_data.eq(full), f.spram_req.w_en.eq(1)] m.d.sync += [t.sp_addr.eq(full + 1), t.outstanding.eq(1)] m.next = "RHDR_PAD" with m.State("RHDR_PAD"): with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) with m.If(t.is_write): m.next = "WRITE" with m.Elif(t.addr >= _SPRAM_BASE): m.next = "SPRAM_STREAM" with m.Else(): m.next = "REG_READ" with m.Elif(~t.is_write & (t.addr < _SPRAM_BASE) & self.tx_rdy & ~t.prefetched): # One-shot: this branch is re-evaluated every cycle the pad # byte has not arrived, and repeating it would consume the # register stream early. m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)] m.d.sync += [t.eff_addr.eq(t.eff_addr + 1), t.prefetched.eq(1)] with m.State("CHDR_1"): with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) with m.If(~t.is_write & (t.cmd_reg == 0)): m.next = "DEVID" with m.Elif(t.is_write): m.next = "CMD_WRITE" with m.Else(): m.next = "CMD_READ" with m.Elif(~t.is_write & self.tx_rdy & (t.cmd_pushes == 0)): m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)] m.d.sync += t.cmd_pushes.eq(1) with m.State("CMD_READ"): # The response is pushed TWICE so it appears in both of the # byte positions hosts read it from. with m.If(self.tx_rdy & (t.cmd_pushes < 2)): m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)] m.d.sync += t.cmd_pushes.eq(t.cmd_pushes + 1) with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) with m.If(~self.cs_active): m.next = "HEADER0" with m.State("CMD_WRITE"): # The LAST data byte before CS rise is the value: hosts send # either one data byte or a pad plus the value, and taking the # last one serves both without counting bytes. with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) m.d.sync += [t.cmd_wdata.eq(self.rx_data), t.cmd_wreg.eq(t.cmd_reg), t.cmd_have.eq(1)] with m.If(~self.cs_active): with m.If(t.cmd_have): m.d.sync += t.cmd_wr.eq(1) m.next = "HEADER0" with m.State("WRITE"): with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) self._register_write(m, r, f, t) with m.If(~self.cs_active): m.next = "HEADER0" with m.State("REG_READ"): 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.sync += t.eff_addr.eq(t.eff_addr + 1) with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) with m.If(~self.cs_active): m.next = "HEADER0" with m.State("DEVID"): with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) with m.If(~self.cs_active): m.next = "HEADER0" with m.State("SPRAM_STREAM"): # One request in flight: the response has nowhere to wait, so # it must be consumed in the cycle it is valid. with m.If(self.cs_active & f.spram_req.w_rdy & self.tx_rdy & (t.outstanding == 0) & ~t.sp_pending): m.d.comb += [f.spram_req.w_data.eq(t.sp_addr), f.spram_req.w_en.eq(1)] m.d.sync += [t.sp_addr.eq(t.sp_addr + 1), t.outstanding.eq(t.outstanding + 1)] with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) with m.If(~self.cs_active): m.next = "SPRAM_END" with m.State("SPRAM_END"): with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) with m.If((t.outstanding == 0) & ~t.sp_pending & ~self.rx_rdy): m.next = "HEADER0" with m.State("TRAILER"): with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) with m.If(~self.cs_active): m.next = "HEADER0" def _spram_responses(self, m, t): """Forward SPRAM read data to the EXI TX stream. Runs outside the FSM because a response can arrive while the FSM is still in a header state. `sp_hold` carries one byte for the case where the TX FIFO is momentarily full. """ mine = self.spram_rsp_valid & (t.outstanding != 0) with m.If(mine & self.tx_rdy & ~t.sp_pending): m.d.comb += [self.tx_data.eq(self.spram_rsp_data), self.tx_en.eq(1)] m.d.sync += t.outstanding.eq(t.outstanding - 1) with m.Elif(mine): m.d.sync += [t.sp_hold.eq(self.spram_rsp_data), t.sp_pending.eq(1), t.outstanding.eq(t.outstanding - 1)] with m.Elif(t.sp_pending & self.tx_rdy): m.d.comb += [self.tx_data.eq(t.sp_hold), self.tx_en.eq(1)] m.d.sync += t.sp_pending.eq(0) def _command_write(self, m, r, t): """Apply a pending command-space write, one cycle after the data byte.""" with m.If(t.cmd_wr): m.d.sync += t.cmd_wr.eq(0) with m.Switch(t.cmd_wreg): with m.Case(_CMD_REVID): pass # read-only with m.Case(_CMD_IMR): m.d.sync += r.cmd_imr.eq(t.cmd_wdata) with m.Case(_CMD_IR): m.d.sync += r.cmd_ir.eq(r.cmd_ir & ~t.cmd_wdata) with m.Default(): with m.If(t.cmd_wreg <= _CMD_LAST): m.d.sync += r.cmd_misc[t.cmd_wreg[0:4]].eq(t.cmd_wdata) def _interrupts(self, m, r, t): """The two interrupt levels. MAC level: `IR & IMR`, the per-event bits a host clears by writing 1. EXI level: command register `_CMD_IR`, whose bit 7 is a live level meaning "a MAC interrupt is pending", masked by `_CMD_IMR`. INT_N comes from the EXI level, so masking there silences the line while a host works through its handler. Bit 7 is a level rather than a latch because hosts clear the MAC IR first and the EXI status afterwards; a latch would lose an event that arrived in between. """ mac_int = Signal() m.d.comb += [ mac_int.eq((r.ir & r.imr).any()), t.cmd_ir_val.eq(Cat(r.cmd_ir[0:_CMD_IR_MAC], mac_int)), ] m.d.sync += self.exi_int_n.eq(~(t.cmd_ir_val & r.cmd_imr).any()) def _ethernet_events(self, m, r, f, t): """Register-side effects of things the ethernet engines report.""" # The assembler's write pointer, so the host can see new frames. with m.If(f.rx_wptr.r_rdy): m.d.sync += [f.rx_wptr.r_en.eq(1), r.rwp_lo.eq(f.rx_wptr.r_data)] with m.If(self.rx_irq_i): m.d.sync += r.ir[_IR_RI].eq(1) with m.If(self.rx_overflow): m.d.sync += r.ir[_IR_RBFI].eq(1) # TX done: flag it, release the start bits, and allow the next frame # to be queued. with m.If(self.tx_irq_i): m.d.sync += [r.ir[_IR_TI].eq(1), r.ncra[1:3].eq(0), t.tx_queued.eq(0)] # Deferred TX queue, armed by a TWD_HI write. Held until tx_ctrl has # room so a full control FIFO cannot drop the trigger. with m.If(t.twd_armed & f.tx_ctrl.w_rdy): m.d.sync += [f.tx_ctrl.w_data.eq(t.tx_frame_len), f.tx_ctrl.w_en.eq(1), t.twd_armed.eq(0), t.tx_queued.eq(1), t.txdata_cnt.eq(0)] # ──────────────────────────── elaborate ───────────────────────────── def elaborate(self, platform): m = Module() f = self._make_fifos(m) r = self._make_registers(m) # Transaction and TX-path state, shared across the blocks below. t = SimpleNamespace( # Current transaction. addr=Signal(16), # register address from the header eff_addr=Signal(16), # running address of the current data byte is_write=Signal(), cmd_space=Signal(), cmd_reg=Signal(6), prefetched=Signal(), # first response byte already pushed # Command-space write, applied a cycle after the data byte. cmd_wr=Signal(), cmd_wreg=Signal(6), cmd_wdata=Signal(8), cmd_have=Signal(), # at least one data byte seen cmd_pushes=Signal(2), # command-read responses pushed so far cmd_ir_val=Signal(8), # command IR including the live MAC bit # SPRAM streaming reads. sp_addr=Signal(16), # next SPRAM byte address to request outstanding=Signal(4), # requests issued but not yet answered sp_hold=Signal(8), # response held while the TX FIFO was full sp_pending=Signal(), # TX frame length. `txdata_cnt` counts what the host actually # streamed and is what the start-transmit path uses, because a # host need not write TWD at all. tx_frame_len=Signal(16), txdata_cnt=Signal(16), twd_armed=Signal(), tx_queued=Signal(), ncra_rst=Signal(), ) t.rd_sel = t.eff_addr[0:8] m.d.comb += self.ncra_rst_o.eq(t.ncra_rst) self._register_taps(m, r) # Strobes default low; the blocks below assert them for one cycle. m.d.sync += [ f.tx_bytes.w_en.eq(0), f.tx_ctrl.w_en .eq(0), f.rx_wptr.r_en .eq(0), t.ncra_rst .eq(0), ] m.d.comb += [ f.spram_req.w_en .eq(0), f.spram_req.w_data.eq(0), self.rx_en .eq(0), self.tx_en .eq(0), self.tx_data.eq(0), ] self._interrupts(m, r, t) rd_val = self._read_mux(m, r, t) self._transaction_fsm(m, r, f, t, rd_val) self._spram_responses(m, t) self._command_write(m, r, t) self._ethernet_events(m, r, f, t) return m 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 tx_log = [] def sample_tx(ctx): if ctx.get(reg.tx_en): tx_log.append(ctx.get(reg.tx_data)) 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) sample_tx(ctx) await ctx.tick("sync") if en: break ctx.set(reg.rx_rdy, 0) async def collect_tx(ctx, n): """Collect n response bytes, including any pushed during the header.""" out = [] while tx_log and len(out) < n: out.append(tx_log.pop(0)) for _ in range(3000): if len(out) >= n: break if ctx.get(reg.tx_en): out.append(ctx.get(reg.tx_data)) await ctx.tick("sync") return out # Real protocol (docs/exi_protocol_real.md): register space is a 4-byte # header [0x80|w<<6, ah, al, 0x00] followed by a data phase that runs # until CS rises -- there is no length field. async def exi_read(ctx, addr, length=1): tx_log.clear() ctx.set(reg.cs_active, 1) await push_rx(ctx, 0x80) await push_rx(ctx, (addr >> 8) & 0xFF) await push_rx(ctx, addr & 0xFF) await push_rx(ctx, 0x00) # turnaround / prefetch window result = await collect_tx(ctx, length) ctx.set(reg.cs_active, 0) await ctx.tick("sync").repeat(6) return result async def exi_write(ctx, addr, data): ctx.set(reg.cs_active, 1) await push_rx(ctx, 0xC0) await push_rx(ctx, (addr >> 8) & 0xFF) await push_rx(ctx, addr & 0xFF) await push_rx(ctx, 0x00) for b in data: await push_rx(ctx, b) ctx.set(reg.cs_active, 0) await ctx.tick("sync").repeat(6) async def drain_tx_bytes(ctx, limit=80): """Pop everything the register file pushed into tx_bytes.""" out = [] for _ in range(limit): if not ctx.get(reg.tx_bytes_r_rdy): break out.append(ctx.get(reg.tx_bytes_r_data)) ctx.set(reg.tx_bytes_r_en, 1) await ctx.tick("sync").repeat(1) ctx.set(reg.tx_bytes_r_en, 0) await ctx.tick("sync").repeat(1) return out async def exi_devid(ctx): """EXI_GetID: write 0x0000 (2 bytes), then clock 4 read bytes.""" tx_log.clear() ctx.set(reg.cs_active, 1) await push_rx(ctx, 0x00) # command space, reg 0, read await push_rx(ctx, 0x00) result = await collect_tx(ctx, 4) ctx.set(reg.cs_active, 0) await ctx.tick("sync").repeat(6) return result async def cmd_write(ctx, creg, data): """Command-space write, short form: a 2-byte header [0x40|reg, 0x00] then the data bytes -- three bytes total for a single-byte write, NOT a 4-byte transfer.""" ctx.set(reg.cs_active, 1) await push_rx(ctx, 0x40 | (creg & 0x3F)) await push_rx(ctx, 0x00) for b in data: await push_rx(ctx, b) ctx.set(reg.cs_active, 0) await ctx.tick("sync").repeat(6) async def cmd_read(ctx, creg, length=1): """Command-space read: 2-byte header, then clock `length` bytes.""" tx_log.clear() ctx.set(reg.cs_active, 1) await push_rx(ctx, creg & 0x3F) # Idle while header byte 1 is "on the wire". The device must push its for _ in range(4): sample_tx(ctx) # the push lands HERE; a bare tick loses it await ctx.tick("sync") await push_rx(ctx, 0x00) result = await collect_tx(ctx, length) ctx.set(reg.cs_active, 0) await ctx.tick("sync").repeat(6) return result async def cmd_write_long(ctx, creg, val): """Command-space write, long form: ONE 4-byte transfer [0x40|reg, 0x00, 0x00, val] -- exi_imm_read_write(..., 4).""" ctx.set(reg.cs_active, 1) await push_rx(ctx, 0x40 | (creg & 0x3F)) await push_rx(ctx, 0x00) await push_rx(ctx, 0x00) # padding, NOT the value await push_rx(ctx, val) # the value is the LAST byte ctx.set(reg.cs_active, 0) await ctx.tick("sync").repeat(6) async def cmd_read_long(ctx, creg): """Command-space read, long form: a 4-byte read/write whose byte 3 is the response. Returns (byte2, byte3).""" tx_log.clear() ctx.set(reg.cs_active, 1) await push_rx(ctx, creg & 0x3F) for _ in range(4): sample_tx(ctx) await ctx.tick("sync") await push_rx(ctx, 0x00) got = await collect_tx(ctx, 2) ctx.set(reg.cs_active, 0) await ctx.tick("sync").repeat(6) return got # 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 + arbiter as a request/grant port. The response FIFO is gone: the arbiter drives spram_rsp_valid directly for exactly ONE cycle, one cycle after it takes a request. Mirror that precisely -- a response held high for two cycles makes the register file push the same byte twice. """ busy = False held = 0 async for vals in ctx.tick("sync").sample( reg.spram_req_r_rdy, reg.spram_req_r_data): rdy, addr = vals[-2:] ctx.set(reg.spram_req_r_en, 0) ctx.set(reg.spram_rsp_valid, 0) if busy: # deliver, one cycle after accept ctx.set(reg.spram_rsp_data, held) ctx.set(reg.spram_rsp_valid, 1) busy = False elif rdy: # accept one request held = spram_mem.get(addr, 0) ctx.set(reg.spram_req_r_en, 1) busy = True errors = [] async def testbench(ctx): ctx.set(reg.tx_rdy, 1) # tx_fifo always has room in this model await ctx.tick("sync").repeat(8) # T1: a device-ID query must push NOTHING. result = await exi_devid(ctx) if result != []: errors.append(f"T1 device-ID query must push nothing, got {result}") print(f"T1 device-ID query pushes nothing: {result == []}") # T1b: both ring pointers must come out of reset at the ring's FIRST for name, a in (("RWP", _RWP_LO), ("RRP", _RRP_LO)): got = await exi_read(ctx, a, 1) if got != [_RX_PAGE_FIRST]: errors.append(f"T1b {name} reset: expected " f"[{_RX_PAGE_FIRST}], got {got}") print(f"T1b {name} at reset: {got} (want [{_RX_PAGE_FIRST}])") # T1c: an NCRA reset must put BOTH pointers back to page 1, even after await exi_write(ctx, _RRP_LO, [7]) # move RRP off page 1 got = await exi_read(ctx, _RRP_LO, 1) if got != [7]: errors.append(f"T1c RRP should be writable, got {got}") # Move RWP too, the way the assembler does -- otherwise this half of # the test has no teeth, because RWP would read 1 whether or not the # reset clears it. ctx.set(reg.rx_wptr_w_data, 9) ctx.set(reg.rx_wptr_w_en, 1) await ctx.tick("sync").repeat(1) ctx.set(reg.rx_wptr_w_en, 0) await ctx.tick("sync").repeat(4) got = await exi_read(ctx, _RWP_LO, 1) if got != [9]: errors.append(f"T1c RWP should track the assembler, got {got}") await exi_write(ctx, _NCRA, [0x01]) # NCRA reset await ctx.tick("sync").repeat(4) for name, a in (("RWP", _RWP_LO), ("RRP", _RRP_LO)): got = await exi_read(ctx, a, 1) if got != [_RX_PAGE_FIRST]: errors.append(f"T1c {name} after NCRA reset: expected " f"[{_RX_PAGE_FIRST}], got {got}") print(f"T1c {name} after NCRA reset: {got} " f"(want [{_RX_PAGE_FIRST}])") # ...and the constant must not drift from the assembler's, which owns # the write pointer. Two copies of a page number is exactly how the # off-by-one got in. from exi_bba.rx_frame_assembler import _RX_PAGE_FIRST as _ASM_FIRST if _RX_PAGE_FIRST != _ASM_FIRST: errors.append(f"T1b ring-first-page mismatch: register file " f"{_RX_PAGE_FIRST} vs assembler {_ASM_FIRST}") print(f"T1b ring first page agrees with assembler: " f"{_RX_PAGE_FIRST == _ASM_FIRST}") 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("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("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("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("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("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("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("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}") print(f"T5 INT_N after IR clear: {int_n} (want 1)") # T6: streaming SPRAM read (DMA) — the real protocol has no length # field, so the data phase simply runs for as long as CS is held. N = 12 tx_log.clear() ctx.set(reg.cs_active, 1) await push_rx(ctx, 0x80) # register space, read await push_rx(ctx, 0x01) # addr[15:8] -> 0x0100 await push_rx(ctx, 0x00) # addr[7:0] await push_rx(ctx, 0x00) # turnaround byte 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("sync") ctx.set(reg.cs_active, 0) # end the transaction 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: 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}") # T8: a MULTI-BYTE write to TXDATA must push EVERY byte. payload = [(0x11 + i * 3) & 0xFF for i in range(64)] await exi_write(ctx, _TXDATA, payload) got = await drain_tx_bytes(ctx) if got != payload: errors.append(f"T8 multi-byte TXDATA write: got {got}, " f"want {payload}") print(f"T8 multi-byte TXDATA write: {len(got)}/{len(payload)} bytes " f"{'ok' if got == payload else 'MISMATCH'}") # T9: TX completion must clear the ST bits and re-arm for the next await exi_write(ctx, _NCRA, [0x08 | 0x02]) # SR + ST0 await ctx.tick("sync").repeat(2) ctx.set(reg.tx_irq_i, 1) # frame completed await ctx.tick("sync").repeat(1) ctx.set(reg.tx_irq_i, 0) await ctx.tick("sync").repeat(2) got = await exi_read(ctx, _NCRA, 1) if got and (got[0] & 0x06): errors.append(f"T9 ST bits not cleared on TX done: NCRA=0x{got[0]:02X}") print(f"T9 NCRA after TX done: 0x{got[0]:02X} (ST bits must be clear)") # ...and a SECOND ST-triggered transmit must still queue, which it # cannot if tx_queued was left set. tx_ctrl_before = ctx.get(reg.tx_ctrl_r_rdy) await exi_write(ctx, _NCRA, [0x08 | 0x02]) await ctx.tick("sync").repeat(4) if not ctx.get(reg.tx_ctrl_r_rdy): errors.append("T9 second ST trigger did not queue a frame") print(f"T9 second ST trigger queues: {bool(ctx.get(reg.tx_ctrl_r_rdy))} " f"(was {bool(tx_ctrl_before)})") # ── T10: command write, 2-byte header + a 1-byte data phase ────── await cmd_write(ctx, 0x02, [0xF8]) # unmask got = await cmd_read(ctx, 0x02, 1) print(f"T10 cmd write reg2=0xF8 -> read back {[hex(x) for x in got]}") if got != [0xF8]: errors.append(f"T10 command-space write dropped: read back {got}") await cmd_write(ctx, 0x02, [0x00]) # ... and IRMASKALL got2 = await cmd_read(ctx, 0x02, 1) print(f"T10b cmd write reg2=0x00 -> read back {[hex(x) for x in got2]}") if got2 != [0x00]: errors.append(f"T10b command-space rewrite failed: {got2}") # ── T11: start transmit with NO TWD write; length must be counted ─ await drain_tx_bytes(ctx) # clear anything pending while ctx.get(reg.tx_ctrl_r_rdy): # and any queued length ctx.set(reg.tx_ctrl_r_en, 1) await ctx.tick("sync") ctx.set(reg.tx_ctrl_r_en, 0) await ctx.tick("sync") ctx.set(reg.tx_irq_i, 1) # TX done -> clears tx_queued await ctx.tick("sync") ctx.set(reg.tx_irq_i, 0) await ctx.tick("sync").repeat(2) # ── T15: reset defaults, and every stored register reads back ──── defaults = ((_NWAYC, _NWAYC_VAL), (_MISC, _MISC_VAL), (_BP_LO, _BP_VAL), (_RHBP_LO, _RHBP_VAL)) for addr, want in defaults: got = (await exi_read(ctx, addr, 1))[0] if got != want: errors.append(f"T15 reg 0x{addr:02X} default 0x{got:02X}, " f"want 0x{want:02X}") print(f"T15 defaults: NWAYC/MISC/BP/RHBP = " f"{[hex((await exi_read(ctx, a, 1))[0]) for a, _ in defaults]}") # A host's init sequence, in order. init_writes = ((_NCRB, 0x52), (_RXINTT_LO, 0x00), (_RXINTT_HI, 0x06), (_MISC2, 0x80), (_TLBP_LO, 0x00), (_TLBP_HI, 0x00), (_BP_LO, 0x01), (_BP_HI, 0x00), (_RHBP_LO, 0x0F), (_RHBP_HI, 0x00), (_GCA, 0x08), (_NWAYC, 0x0C)) for addr, val in init_writes: await exi_write(ctx, addr, [val]) for addr, val in init_writes: got = (await exi_read(ctx, addr, 1))[0] if got != val: errors.append(f"T15 reg 0x{addr:02X}: wrote 0x{val:02X}, " f"read 0x{got:02X}") print(f"T15 all {len(init_writes)} init writes read back") # NCRB is what the RX filter will be steered from, so the bits must # survive: 0x52 = AB | CA | 2_PACKETS_PER_INT, with PR and PM CLEAR # (so the real BBA delivers own-MAC + broadcast only). ncrb = ctx.get(reg.ncrb) pr = (ncrb >> NCRB_PR) & 1 pm = (ncrb >> _NCRB_PM) & 1 ab = (ncrb >> _NCRB_AB) & 1 print(f"T15 ncrb export = 0x{ncrb:02X} (PR={pr} PM={pm} AB={ab})") if ncrb != 0x52 or pr or pm or not ab: errors.append(f"T15 ncrb export 0x{ncrb:02X}, want 0x52 " f"(PR=0 PM=0 AB=1)") if ctx.get(reg.rhbp) != 0x00F or ctx.get(reg.bp) != 0x001: errors.append(f"T15 ring geometry exports: bp=" f"0x{ctx.get(reg.bp):03X} rhbp=" f"0x{ctx.get(reg.rhbp):03X}, want 0x001/0x00F") print(f"T15 geometry exports: bp=0x{ctx.get(reg.bp):03X} " f"rhbp=0x{ctx.get(reg.rhbp):03X}") # ── T14: the command-register map ──────────────────────────────── rev = (await cmd_read(ctx, _CMD_REVID, 1))[0] print(f"T14 cmd 0x01 revision ID = 0x{rev:02X} (want 0x00, real HW)") if rev != _CMD_REVID_VAL: errors.append(f"T14 revision ID 0x{rev:02X}, want " f"0x{_CMD_REVID_VAL:02X}") # Read-only: a write must not change it. await cmd_write(ctx, _CMD_REVID, [0xA5]) rev2 = (await cmd_read(ctx, _CMD_REVID, 1))[0] if rev2 != _CMD_REVID_VAL: errors.append(f"T14 revision ID writable: became 0x{rev2:02X}") # The write-only ones must at least STICK, so software that writes # unconditionally. for creg, val in ((0x04, 0xD1), (0x05, 0x4E), (0x09, 0x3C), (0x0B, 0x77), (0x0F, 0x11)): await cmd_write(ctx, creg, [val]) rb = (await cmd_read(ctx, creg, 1))[0] if rb != val: errors.append(f"T14 cmd 0x{creg:02X}: wrote 0x{val:02X}, " f"read 0x{rb:02X}") print(f"T14 cmd 0x04/05/09/0B/0F all read back what was written") # Out of range still reads 0, so a stray high register cannot alias # onto a real one. hi = (await cmd_read(ctx, 0x20, 1))[0] if hi != 0x00: errors.append(f"T14 cmd 0x20 reads 0x{hi:02X}, want 0x00") # ── T13: the command-level interrupt and its mask ──────────────── await exi_write(ctx, _IR, [0xFF]) # clear MAC IR await exi_write(ctx, _IMR, [0x86]) # enable RBFI|TI|RI await cmd_write(ctx, 0x02, [0xF8]) # IRMASKNONE before = (await cmd_read(ctx, 0x03, 1))[0] int_idle = ctx.get(reg.exi_int_n) ctx.set(reg.rx_irq_i, 1) # a frame arrives await ctx.tick("sync") ctx.set(reg.rx_irq_i, 0) await ctx.tick("sync").repeat(3) after = (await cmd_read(ctx, 0x03, 1))[0] int_pend = ctx.get(reg.exi_int_n) print(f"T13 cmd 0x03: idle=0x{before:02X} after RI=0x{after:02X}; " f"INT_N {int_idle} -> {int_pend}") if before & 0x80: errors.append(f"T13 cmd 0x03 bit7 set while idle: 0x{before:02X}") if not (after & 0x80): errors.append(f"T13 cmd 0x03 bit7 NOT set after RI: 0x{after:02X} " f"- both drivers would skip bba_interrupt()") if int_pend: errors.append("T13 INT_N not asserted with an unmasked MAC event") # Masking at the COMMAND level must drop INT_N -- this is how both # drivers stop the line re-asserting while their handler runs. await cmd_write(ctx, 0x02, [0x00]) # IRMASKALL await ctx.tick("sync").repeat(3) masked = ctx.get(reg.exi_int_n) still = (await cmd_read(ctx, 0x03, 1))[0] print(f"T13b IRMASKALL: INT_N={masked} (want 1), " f"status still 0x{still:02X}") if not masked: errors.append("T13b INT_N still asserted after IRMASKALL") if not (still & 0x80): errors.append("T13b masking must not clear the pending status") await cmd_write(ctx, 0x02, [0xF8]) await exi_write(ctx, _IR, [0xFF]) # tidy up # ── T12: command write as ONE 4-byte transfer, value in byte 3 ─── await cmd_write_long(ctx, 0x02, 0x5A) both = await cmd_read_long(ctx, 0x02) print(f"T12 4-byte cmd write 0x5A -> bytes 2,3 = " f"{[hex(x) for x in both]}") if len(both) < 2 or both[1] != 0x5A: errors.append(f"T12 long-form cmd write: byte 3 = " f"{[hex(x) for x in both]}, want 0x5A") if len(both) >= 1 and both[0] != 0x5A: errors.append(f"T12 byte-2 slot also wrong: {hex(both[0])}") await cmd_write_long(ctx, 0x02, 0x00) # restore frame11 = [0x40 + i for i in range(24)] await exi_write(ctx, _TXDATA, frame11) # the frame, in ONE write await exi_write(ctx, _NCRA, [0x04]) # ST trigger, no TWD qlen = ctx.get(reg.tx_ctrl_r_data) if ctx.get(reg.tx_ctrl_r_rdy) else None body = await drain_tx_bytes(ctx) print(f"T11 TX without TWD: queued len={qlen} " f"bytes={len(body)}") if qlen != len(frame11): errors.append(f"T11 queued length {qlen}, want {len(frame11)} " f"(ST path must use the COUNTED length)") if body != frame11: errors.append(f"T11 frame bytes wrong: {len(body)} of " f"{len(frame11)}") # NO T10 FOR THE LATE-RESPONSE LEAK -- and that is deliberate. sim = Simulator(reg) 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.")