b175cb0e77
First file of the documentation cleanup. Comments 403 -> 83 lines (46% ->
12% of the module), total 1563 -> 1266, with the real documentation moved into
structured docstrings instead of inline prose.
What went:
* every account of how a bug was found, what it looked like on a console,
and which fix attempts were falsified -- all of that lives in CLAUDE.md
* every description of how Swiss or libogc is written internally; the file
now documents what the GATEWAY does and what a host observes, not how any
particular host is implemented. Zero occurrences of Swiss/libogc/gcif
remain, in comments, docstrings, helper names or test output
* the module docstring, which still described the GUESSED header format
(addr[12:6] plus a 2-bit length field) replaced long ago
What arrived:
* a register table giving each address its name AND its purpose, bit
meanings included (NCRA reset/ST0/ST1/SR, IR's RI/TI/RBFI, the page
pointers, which side owns RWP vs RRP)
* a command-register section, likewise
* named IR/IMR and NCRB bit constants instead of bare indices
* a class docstring grouping the ports into EXI stream / ethernet side /
register taps / events
* an FSM docstring listing all 13 states in visit order with what each one
is for, and the invariant that every data phase ends on ~cs_active
Structure: elaborate() is now a table of contents -- _make_fifos,
_make_registers, _register_taps, _interrupts, _read_mux, _transaction_fsm,
_spram_responses, _command_write, _ethernet_events -- with register storage,
transaction state and the FIFOs passed as three namespaces so each block's
inputs are visible. The register write decode is its own method rather than
80 lines nested inside a state.
Dead code removed: sp_req, byte_ctr, drain_ctr, SP_LIMIT and _DEVICE_ID were
all write-only or declaration-only.
Behaviour preserved, checked four ways: all 15 module tests pass, the full
module suite passes, bba_top passes at EXI_HALF=1/2/8, and the hardware is
materially identical -- flip-flops exactly equal (1177 SB_DFFE, 671 SB_CARRY),
RAM/IO/global buffers unchanged, ICESTORM_LC 4350 -> 4380 (+0.7%). That rise
is the read mux: its Switch cases are now in address order, which changes how
the mux tree maps. All cases are distinct constants so the decode is
semantically identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1267 lines
57 KiB
Python
1267 lines
57 KiB
Python
"""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"]
|
|
|
|
|
|
# ─────────────────────────── 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
|
|
_RWP_HI = 0x17 # frame assembler
|
|
_RRP_LO = 0x18 # RX Read Pointer (page) — driver-owned, written once the
|
|
_RRP_HI = 0x19 # host has consumed a frame
|
|
_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.
|
|
_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, read pointer out to it.
|
|
self.rx_wptr_w_data = Signal(8)
|
|
self.rx_wptr_w_en = Signal()
|
|
self.rx_wptr_w_rdy = Signal()
|
|
self.rx_rptr_r_data = Signal(8)
|
|
self.rx_rptr_r_en = Signal()
|
|
self.rx_rptr_r_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),
|
|
rx_rptr=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.submodules.rx_rptr = f.rx_rptr
|
|
|
|
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),
|
|
|
|
self.rx_rptr_r_data.eq(f.rx_rptr.r_data),
|
|
f.rx_rptr.r_en .eq(self.rx_rptr_r_en),
|
|
self.rx_rptr_r_rdy .eq(f.rx_rptr.r_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):
|
|
m.d.sync += r.rrp_lo.eq(self.rx_data)
|
|
with m.If(f.rx_rptr.w_rdy): # forward to the assembler
|
|
m.d.sync += [f.rx_rptr.w_data.eq(self.rx_data),
|
|
f.rx_rptr.w_en.eq(1)]
|
|
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_rptr.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.")
|