Added full design created with Claude
This commit is contained in:
@@ -0,0 +1,533 @@
|
||||
"""BBATop — top-level elaboratable for the GC BBA FPGA replacement.
|
||||
|
||||
Clock domains
|
||||
-------------
|
||||
capture : 54 MHz, from 12 MHz crystal via SB_PLL40_PAD (DIVR=0 DIVF=71 DIVQ=4)
|
||||
exi/sync : 24 MHz, from the iCE40UP5K internal SB_HFOSC (÷2, CLKHF_DIV=0b01)
|
||||
|
||||
Submodule instantiation and signal wiring
|
||||
-----------------------------------------
|
||||
See CLAUDE.md "Module Breakdown" and "CDC Signal Inventory" for the full list.
|
||||
"""
|
||||
|
||||
from amaranth import *
|
||||
|
||||
from exi_bba.exi_capture import ExiCapture
|
||||
from exi_bba.bba_register_file import BBARegisterFile
|
||||
from exi_bba.spram_arbiter import SPRAMArbiter
|
||||
from exi_bba.rx_frame_assembler import RXFrameAssembler
|
||||
from exi_bba.tx_frame_drain import TXFrameDrain
|
||||
from exi_bba.w5500_spi_master import W5500SPIMaster
|
||||
from exi_bba.w5100_parallel_master import W5100ParallelMaster
|
||||
from exi_bba.status_panel import StatusPanel
|
||||
|
||||
from amaranth.lib.cdc import FFSynchronizer
|
||||
|
||||
__all__ = ["BBATop"]
|
||||
|
||||
|
||||
class BBATop(Elaboratable):
|
||||
"""Top-level module. Wires all submodules and defines clock domains.
|
||||
|
||||
External ports (exposed for platform or testbench connection)
|
||||
-------------------------------------------------------------
|
||||
EXI / GC interface (SPI Mode 3)
|
||||
exi_clk / exi_mosi / exi_cs_n : inputs from GC
|
||||
exi_miso : output to GC
|
||||
int_n : interrupt output (active low)
|
||||
|
||||
W5500 SPI interface (SPI Mode 0)
|
||||
w5500_clk / w5500_mosi / w5500_cs_n : outputs to W5500
|
||||
w5500_miso : input from W5500
|
||||
w5500_int_n : W5500 interrupt (input, active low)
|
||||
w5500_rst_n : W5500 hardware reset (output, active low)
|
||||
"""
|
||||
|
||||
def __init__(self, eth="w5100", reset_cycles=24000, status_panel=False):
|
||||
# Ethernet back-end: "w5100" (indirect parallel bus, reaches the EXI
|
||||
# ceiling) or "w5500" (SPI, ~12 Mbit/s). Both expose the identical
|
||||
# tx/rx/init/par interface, so only the physical pins differ.
|
||||
self._eth = eth
|
||||
# MR-reset settle wait passed to the ethernet master (~1 ms on hardware;
|
||||
# the testbench overrides with a small value for fast simulation).
|
||||
self._reset_cycles = reset_cycles
|
||||
# Optional bring-up status panel (drives onboard LEDs/button on the
|
||||
# iCEbreaker — see synth.py). panel_led bit order matches StatusPanel.
|
||||
self._status_panel = status_panel
|
||||
|
||||
# EXI (GC side)
|
||||
self.exi_clk = Signal(init=1)
|
||||
self.exi_mosi = Signal()
|
||||
self.exi_cs_n = Signal(init=1)
|
||||
self.exi_miso = Signal()
|
||||
self.int_n = Signal(init=1)
|
||||
|
||||
if eth == "w5500":
|
||||
# W5500 SPI
|
||||
self.w5500_clk = Signal()
|
||||
self.w5500_mosi = Signal()
|
||||
self.w5500_miso = Signal()
|
||||
self.w5500_cs_n = Signal(init=1)
|
||||
self.w5500_int_n = Signal(init=1)
|
||||
self.w5500_rst_n = Signal(init=1)
|
||||
else:
|
||||
# W5100 indirect parallel bus. data_o/data_oe/data_i are the FPGA
|
||||
# side of a bidirectional D[7:0] (wrapped in a tristate SB_IO at the
|
||||
# platform level); a board ties the upper address lines to 0 so only
|
||||
# A[1:0] are wired.
|
||||
self.w5100_addr = Signal(2)
|
||||
self.w5100_data_o = Signal(8)
|
||||
self.w5100_data_oe = Signal()
|
||||
self.w5100_data_i = Signal(8)
|
||||
self.w5100_cs_n = Signal(init=1)
|
||||
self.w5100_rd_n = Signal(init=1)
|
||||
self.w5100_wr_n = Signal(init=1)
|
||||
self.w5100_int_n = Signal(init=1)
|
||||
self.w5100_rst_n = Signal(init=1)
|
||||
|
||||
if status_panel:
|
||||
self.panel_led = Signal(5) # to onboard LEDs (see StatusPanel)
|
||||
self.panel_btn = Signal(3) # from onboard button(s)
|
||||
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
# ── Clock domain generation ───────────────────────────────────────
|
||||
# Three domains, two physical sources (1 PLL + 1 internal HFOSC):
|
||||
# capture @ 54 MHz (PLL) — SPI bit engine only; oversamples the
|
||||
# 27 MHz EXI clock 2× (robust Mode-3).
|
||||
# exi @ 24 MHz (HFOSC) — register file / transaction FSM.
|
||||
# sync @ 24 MHz (HFOSC) — SPRAM, RX/TX engines, ethernet master.
|
||||
# exi and sync share the HFOSC net (frequency- and phase-matched); the
|
||||
# AsyncFIFOs between them are still valid CDC and keep the module
|
||||
# boundaries clean. Only the tiny capture front-end needs the fast
|
||||
# clock — which is why 27 MHz-EXI / OG performance is reachable on the
|
||||
# iCE40UP5K even though the register-file logic tops out ~44 MHz.
|
||||
if platform is not None:
|
||||
# capture @ 54 MHz: icepll -i 12 -o 54 → DIVR=0 DIVF=71 DIVQ=4.
|
||||
# 54 MHz = 2× the 27 MHz EXI clock — the minimum oversampling that
|
||||
# cleanly implements SPI Mode 3. The isolated SPI bit engine closes
|
||||
# ~91 MHz on this device; the byte-FIFO read path brings the
|
||||
# integrated capture domain to ~62 MHz, so 54 closes with margin.
|
||||
m.domains += ClockDomain("capture")
|
||||
platform.lookup(platform.default_clk).attrs["GLOBAL"] = False
|
||||
m.submodules.pll = Instance(
|
||||
"SB_PLL40_PAD",
|
||||
p_FEEDBACK_PATH = "SIMPLE",
|
||||
p_DIVR = 0,
|
||||
p_DIVF = 71,
|
||||
p_DIVQ = 4,
|
||||
p_FILTER_RANGE = 1,
|
||||
i_PACKAGEPIN = platform.request("clk12", dir="-").io,
|
||||
i_RESETB = Const(1, 1),
|
||||
i_BYPASS = Const(0, 1),
|
||||
o_PLLOUTGLOBAL = ClockSignal("capture"),
|
||||
)
|
||||
|
||||
# exi & sync @ 24 MHz: one SB_HFOSC (÷2) drives both slow domains.
|
||||
# The bulky register-file / SPRAM / W5500 logic is routing-bound at
|
||||
# ~33–44 MHz on the UP5K; 24 MHz closes with large margin. The byte
|
||||
# rate (27 MHz EXI ÷ 8 ≈ 3.4 MHz) leaves ~7 slow cycles per byte.
|
||||
m.domains += ClockDomain("exi")
|
||||
m.domains += ClockDomain("sync")
|
||||
m.submodules.hfosc = Instance(
|
||||
"SB_HFOSC",
|
||||
p_CLKHF_DIV = "0b01", # 48 ÷ 2 → 24 MHz
|
||||
i_CLKHFEN = Const(1, 1),
|
||||
i_CLKHFPU = Const(1, 1),
|
||||
o_CLKHF = ClockSignal("exi"),
|
||||
)
|
||||
m.d.comb += ClockSignal("sync").eq(ClockSignal("exi"))
|
||||
# (simulation: test harness provides capture/exi/sync clocks via add_clock)
|
||||
|
||||
# ── Submodules ────────────────────────────────────────────────────
|
||||
cap = ExiCapture() # SPI bit engine (capture) + byte FIFOs
|
||||
reg = BBARegisterFile()
|
||||
arb = SPRAMArbiter()
|
||||
asm = RXFrameAssembler()
|
||||
drain = TXFrameDrain()
|
||||
eth = (W5500SPIMaster(reset_cycles=self._reset_cycles)
|
||||
if self._eth == "w5500"
|
||||
else W5100ParallelMaster(reset_cycles=self._reset_cycles))
|
||||
|
||||
m.submodules.cap = cap
|
||||
m.submodules.reg = reg
|
||||
m.submodules.arb = arb
|
||||
m.submodules.asm = asm
|
||||
m.submodules.drain = drain
|
||||
m.submodules.eth = eth
|
||||
|
||||
# ── External pin connections ──────────────────────────────────────
|
||||
m.d.comb += [
|
||||
# EXI inputs (to capture-domain front-end)
|
||||
cap.spi_clk .eq(self.exi_clk),
|
||||
cap.spi_mosi.eq(self.exi_mosi),
|
||||
cap.spi_cs_n.eq(self.exi_cs_n),
|
||||
# EXI outputs
|
||||
self.exi_miso.eq(cap.spi_miso),
|
||||
self.int_n .eq(reg.exi_int_n),
|
||||
]
|
||||
|
||||
# Ethernet back-end physical pins
|
||||
if self._eth == "w5500":
|
||||
m.d.comb += [
|
||||
self.w5500_clk .eq(eth.spi_clk),
|
||||
self.w5500_mosi.eq(eth.spi_mosi),
|
||||
self.w5500_cs_n.eq(eth.spi_cs_n),
|
||||
eth.spi_miso .eq(self.w5500_miso),
|
||||
eth.w5500_int_n.eq(self.w5500_int_n),
|
||||
self.w5500_rst_n.eq(eth.w5500_rst_n),
|
||||
]
|
||||
else:
|
||||
m.d.comb += [
|
||||
self.w5100_addr .eq(eth.bus_addr),
|
||||
self.w5100_data_o .eq(eth.bus_data_o),
|
||||
self.w5100_data_oe.eq(eth.bus_data_oe),
|
||||
eth.bus_data_i .eq(self.w5100_data_i),
|
||||
self.w5100_cs_n .eq(eth.cs_n),
|
||||
self.w5100_rd_n .eq(eth.rd_n),
|
||||
self.w5100_wr_n .eq(eth.wr_n),
|
||||
eth.w5100_int_n .eq(self.w5100_int_n),
|
||||
self.w5100_rst_n .eq(eth.w5100_rst_n),
|
||||
]
|
||||
|
||||
# ── ExiCapture byte stream ↔ BBARegisterFile (exi domain) ────────
|
||||
m.d.comb += [
|
||||
reg.rx_data .eq(cap.rx_data),
|
||||
reg.rx_rdy .eq(cap.rx_rdy),
|
||||
cap.rx_en .eq(reg.rx_en),
|
||||
|
||||
cap.tx_data .eq(reg.tx_data),
|
||||
cap.tx_en .eq(reg.tx_en),
|
||||
reg.tx_rdy .eq(cap.tx_rdy),
|
||||
|
||||
reg.cs_active.eq(cap.cs_active), # transaction-active (for DMA reads)
|
||||
]
|
||||
|
||||
# ── BBARegisterFile ↔ SPRAMArbiter (sync domain FIFO sides) ──────
|
||||
# SPRAM request: reg exi→sync FIFO read side → arb
|
||||
m.d.comb += [
|
||||
arb.exi_req_addr .eq(reg.spram_req_r_data),
|
||||
arb.exi_req_valid.eq(reg.spram_req_r_rdy),
|
||||
reg.spram_req_r_en.eq(arb.exi_req_ready),
|
||||
]
|
||||
# SPRAM response: arb result → reg sync→exi FIFO write side
|
||||
m.d.comb += [
|
||||
reg.spram_rsp_w_data.eq(arb.exi_rsp_data),
|
||||
reg.spram_rsp_w_en .eq(arb.exi_rsp_valid),
|
||||
# arb does not need w_rdy feedback (spram_rsp FIFO is deeper than latency)
|
||||
]
|
||||
|
||||
# ── BBARegisterFile ↔ TXFrameDrain (sync domain FIFO sides) ──────
|
||||
m.d.comb += [
|
||||
drain.tx_bytes_r_data.eq(reg.tx_bytes_r_data),
|
||||
drain.tx_bytes_r_rdy .eq(reg.tx_bytes_r_rdy),
|
||||
reg.tx_bytes_r_en .eq(drain.tx_bytes_r_en),
|
||||
|
||||
drain.tx_ctrl_r_data.eq(reg.tx_ctrl_r_data),
|
||||
drain.tx_ctrl_r_rdy .eq(reg.tx_ctrl_r_rdy),
|
||||
reg.tx_ctrl_r_en .eq(drain.tx_ctrl_r_en),
|
||||
]
|
||||
|
||||
# ── TXFrameDrain ↔ ethernet master (sync domain) ──────────────────
|
||||
m.d.comb += [
|
||||
eth.tx_data .eq(drain.tx_data),
|
||||
eth.tx_valid.eq(drain.tx_valid),
|
||||
drain.tx_ready.eq(eth.tx_ready),
|
||||
eth.tx_sof .eq(drain.tx_sof),
|
||||
eth.tx_eof .eq(drain.tx_eof),
|
||||
]
|
||||
|
||||
# ── ethernet master → RXFrameAssembler (sync domain) ─────────────
|
||||
m.d.comb += [
|
||||
asm.rx_data .eq(eth.rx_data),
|
||||
asm.rx_valid.eq(eth.rx_valid),
|
||||
eth.rx_ready.eq(asm.rx_ready),
|
||||
asm.rx_sof .eq(eth.rx_sof),
|
||||
asm.rx_eof .eq(eth.rx_eof),
|
||||
]
|
||||
|
||||
# ── RXFrameAssembler → SPRAMArbiter (ETH write, sync domain) ─────
|
||||
m.d.comb += [
|
||||
arb.eth_wr_addr .eq(asm.eth_wr_addr),
|
||||
arb.eth_wr_data .eq(asm.eth_wr_data),
|
||||
arb.eth_wr_valid.eq(asm.eth_wr_valid),
|
||||
asm.eth_wr_ready.eq(arb.eth_wr_ready),
|
||||
]
|
||||
|
||||
# ── RXFrameAssembler → BBARegisterFile (rx_wptr FIFO write side) ─
|
||||
m.d.comb += [
|
||||
reg.rx_wptr_w_data.eq(asm.rx_wptr_w_data),
|
||||
reg.rx_wptr_w_en .eq(asm.rx_wptr_w_en),
|
||||
asm.rx_wptr_w_rdy .eq(reg.rx_wptr_w_rdy),
|
||||
]
|
||||
|
||||
# ── Pulse synchronizer connections ────────────────────────────────
|
||||
m.d.comb += [
|
||||
# RX irq: sync → exi (RXFrameAssembler → reg → PS → exi domain)
|
||||
reg.rx_irq_i.eq(asm.rx_irq),
|
||||
# TX irq: sync → exi
|
||||
reg.tx_irq_i.eq(drain.tx_irq),
|
||||
# MAC address (PAR0–5) → SHAR. exi and sync share the HFOSC net,
|
||||
# and par is quasi-static (sampled by the master at init_req).
|
||||
eth.par.eq(reg.par),
|
||||
]
|
||||
|
||||
# ── RX enabled gate (NCRA SR / start-receive bit) ─────────────────
|
||||
# The RX ring-buffer path is active only after the GC sets NCRA[3].
|
||||
m.d.comb += asm.rx_enabled.eq(reg.ncra_sr)
|
||||
|
||||
# ── Optional bring-up status panel (sync domain) ──────────────────
|
||||
# init_req = NCRA reset (exi→sync PS), OR'd with the panel's manual
|
||||
# re-init button when the panel is present.
|
||||
if self._status_panel:
|
||||
panel = StatusPanel()
|
||||
m.submodules.panel = panel
|
||||
|
||||
# cs_active lives in the exi domain; bring it to sync for the LED.
|
||||
cs_a_sync = Signal()
|
||||
m.submodules.panel_cs = FFSynchronizer(
|
||||
cap.cs_active, cs_a_sync, o_domain="sync")
|
||||
|
||||
# "ready" = ethernet init complete (latched until the next init).
|
||||
ready = Signal()
|
||||
with m.If(eth.init_done):
|
||||
m.d.sync += ready.eq(1)
|
||||
with m.Elif(reg.ncra_rst_o | panel.reinit):
|
||||
m.d.sync += ready.eq(0)
|
||||
|
||||
m.d.comb += [
|
||||
panel.cs_active.eq(cs_a_sync),
|
||||
panel.rx_pulse .eq(asm.rx_irq),
|
||||
panel.tx_pulse .eq(drain.tx_irq),
|
||||
panel.ready .eq(ready),
|
||||
panel.btn .eq(self.panel_btn),
|
||||
self.panel_led .eq(panel.led),
|
||||
eth.init_req .eq(reg.ncra_rst_o | panel.reinit),
|
||||
]
|
||||
else:
|
||||
m.d.comb += eth.init_req.eq(reg.ncra_rst_o)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
# ── Integration testbench ─────────────────────────────────────────────────
|
||||
# Drives real EXI Mode-3 transactions on the GC-facing pins and checks the
|
||||
# response — exercising the full chain ExiCapture (capture domain) ↔ byte FIFOs
|
||||
# ↔ BBARegisterFile (exi domain) ↔ sync modules, across all three clock domains.
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from amaranth.sim import Simulator, Period
|
||||
|
||||
dut = BBATop(eth="w5100", reset_cycles=20, # small reset wait for sim
|
||||
status_panel=True) # also exercise the panel wiring
|
||||
errors = []
|
||||
|
||||
HALF = 8 # capture ticks per SPI half-period (well-oversampled)
|
||||
|
||||
async def spi_byte(ctx, mosi_val):
|
||||
"""Drive one EXI Mode-3 byte; return the assembled MISO byte."""
|
||||
miso = 0
|
||||
for bit in range(7, -1, -1):
|
||||
ctx.set(dut.exi_mosi, (mosi_val >> bit) & 1)
|
||||
ctx.set(dut.exi_clk, 0) # falling: slave samples MOSI
|
||||
await ctx.tick("capture").repeat(HALF)
|
||||
miso = (miso << 1) | ctx.get(dut.exi_miso)
|
||||
ctx.set(dut.exi_clk, 1) # rising
|
||||
await ctx.tick("capture").repeat(HALF)
|
||||
return miso
|
||||
|
||||
async def exi_read(ctx, addr, length):
|
||||
"""EXI immediate read: 2-byte header, clock-idle gap, then `length` bytes."""
|
||||
hdr0 = (addr >> 6) & 0x7F
|
||||
# The header length field is only 2 bits ([1:0]); mask it so a long
|
||||
# (DMA) read doesn't overflow length-1 into the addr[5:0] bits. For
|
||||
# SPRAM reads the field is ignored anyway — the stream runs until CS.
|
||||
hdr1 = ((addr & 0x3F) << 2) | ((length - 1) & 0x3)
|
||||
ctx.set(dut.exi_cs_n, 0)
|
||||
ctx.set(dut.exi_clk, 1)
|
||||
await ctx.tick("capture").repeat(HALF)
|
||||
await spi_byte(ctx, hdr0)
|
||||
await spi_byte(ctx, hdr1)
|
||||
# EXI_Imm clock-idle gap: the core decodes the header and prefetches
|
||||
# responses into the tx FIFO before the GC clocks the data phase.
|
||||
await ctx.tick("capture").repeat(HALF * 12)
|
||||
result = [await spi_byte(ctx, 0x00) for _ in range(length)]
|
||||
ctx.set(dut.exi_cs_n, 1)
|
||||
await ctx.tick("capture").repeat(HALF)
|
||||
return result
|
||||
|
||||
async def exi_write(ctx, addr, data):
|
||||
"""EXI immediate write: 2-byte header then the data bytes."""
|
||||
hdr0 = 0x80 | ((addr >> 6) & 0x7F)
|
||||
hdr1 = ((addr & 0x3F) << 2) | (len(data) - 1)
|
||||
ctx.set(dut.exi_cs_n, 0)
|
||||
ctx.set(dut.exi_clk, 1)
|
||||
await ctx.tick("capture").repeat(HALF)
|
||||
await spi_byte(ctx, hdr0)
|
||||
await spi_byte(ctx, hdr1)
|
||||
for b in data:
|
||||
await spi_byte(ctx, b)
|
||||
ctx.set(dut.exi_cs_n, 1)
|
||||
await ctx.tick("capture").repeat(HALF)
|
||||
|
||||
# ── W5100 indirect-bus slave model (drives w5100_data_i) ─────────────
|
||||
# Pre-loads a known MACRAW packet in the RX buffer so we can verify the full
|
||||
# ethernet→SPRAM→GC path. Same protocol as the W5100ParallelMaster bench.
|
||||
RX_FRAME = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04]
|
||||
_W_RX_BASE = 0x6000
|
||||
_W_S0_CR = 0x0401
|
||||
_W_S0_RX_RSR = 0x0426
|
||||
_W_S0_RX_RD = 0x0428
|
||||
_W_CR_RECV = 0x40
|
||||
_A_MR, _A_AR0, _A_AR1, _A_DR = 0b00, 0b01, 0b10, 0b11
|
||||
|
||||
def w5100_preload():
|
||||
plen = len(RX_FRAME) + 2 # MACRAW length includes its header
|
||||
mem = {}
|
||||
for i, b in enumerate([(plen >> 8) & 0xFF, plen & 0xFF] + RX_FRAME):
|
||||
mem[_W_RX_BASE + i] = b
|
||||
mem[_W_S0_RX_RSR], mem[_W_S0_RX_RSR + 1] = (plen >> 8) & 0xFF, plen & 0xFF
|
||||
mem[_W_S0_RX_RD], mem[_W_S0_RX_RD + 1] = 0, 0
|
||||
return mem
|
||||
|
||||
w5100_mem = w5100_preload()
|
||||
|
||||
async def w5100_model(ctx):
|
||||
idm_ar = 0
|
||||
mr = 0
|
||||
prev_cs = prev_rd = prev_wr = 1
|
||||
async for vals in ctx.tick("sync").sample(
|
||||
dut.w5100_cs_n, dut.w5100_rd_n, dut.w5100_wr_n,
|
||||
dut.w5100_addr, dut.w5100_data_o):
|
||||
cs, rd, wr, a, do = vals[-5:]
|
||||
ai = (mr >> 1) & 1
|
||||
if cs == 0 and rd == 0: # drive read data
|
||||
if a == _A_MR:
|
||||
val = mr
|
||||
elif a == _A_AR0:
|
||||
val = (idm_ar >> 8) & 0xFF
|
||||
elif a == _A_AR1:
|
||||
val = idm_ar & 0xFF
|
||||
else:
|
||||
val = w5100_mem.get(idm_ar, 0)
|
||||
ctx.set(dut.w5100_data_i, val)
|
||||
if cs == 0 and prev_wr == 0 and wr == 1: # latch write on /WR rising
|
||||
if a == _A_MR:
|
||||
mr = do
|
||||
elif a == _A_AR0:
|
||||
idm_ar = (idm_ar & 0x00FF) | (do << 8)
|
||||
elif a == _A_AR1:
|
||||
idm_ar = (idm_ar & 0xFF00) | do
|
||||
else:
|
||||
w5100_mem[idm_ar] = do
|
||||
if idm_ar == _W_S0_CR and do == _W_CR_RECV:
|
||||
w5100_mem[_W_S0_RX_RSR] = 0
|
||||
w5100_mem[_W_S0_RX_RSR + 1] = 0
|
||||
if ai:
|
||||
idm_ar = (idm_ar + 1) & 0xFFFF
|
||||
if cs == 0 and prev_rd == 0 and rd == 1 and a == _A_DR and ai:
|
||||
idm_ar = (idm_ar + 1) & 0xFFFF
|
||||
prev_cs, prev_rd, prev_wr = cs, rd, wr
|
||||
|
||||
async def testbench(ctx):
|
||||
ctx.set(dut.exi_clk, 1)
|
||||
ctx.set(dut.exi_cs_n, 1)
|
||||
ctx.set(dut.panel_btn, 0b111) # all buttons released (active-low idle)
|
||||
await ctx.tick("capture").repeat(20)
|
||||
|
||||
# T1: device ID — read 4 bytes from addr 0 → 0x04 0x02 0x02 0x00
|
||||
dev = await exi_read(ctx, 0x0000, 4)
|
||||
print(f"T1 device ID: {[f'0x{b:02X}' for b in dev]}")
|
||||
if dev != [0x04, 0x02, 0x02, 0x00]:
|
||||
errors.append(f"T1 device ID: got {dev}")
|
||||
await ctx.tick("capture").repeat(HALF)
|
||||
|
||||
# T2: write PAR0–3, read them back through the full chain
|
||||
await exi_write(ctx, 0x20, [0xDE, 0xAD, 0xBE, 0xEF])
|
||||
await ctx.tick("capture").repeat(HALF * 4)
|
||||
par = await exi_read(ctx, 0x20, 4)
|
||||
print(f"T2 PAR0-3 readback: {[f'0x{b:02X}' for b in par]}")
|
||||
if par != [0xDE, 0xAD, 0xBE, 0xEF]:
|
||||
errors.append(f"T2 PAR readback: got {par}")
|
||||
await ctx.tick("capture").repeat(HALF)
|
||||
|
||||
# T3: NWAYS must read back the hardcoded 0x17 (link-up sentinel)
|
||||
nways = await exi_read(ctx, 0x31, 1)
|
||||
print(f"T3 NWAYS: 0x{nways[0]:02X} (want 0x17)")
|
||||
if nways != [0x17]:
|
||||
errors.append(f"T3 NWAYS: got {nways}")
|
||||
await ctx.tick("capture").repeat(HALF)
|
||||
|
||||
# T4: DMA-style SPRAM read — clock 8 data bytes (past the 4-byte header
|
||||
# limit) within one CS. Exercises the integrated streaming path:
|
||||
# ExiCapture(cs_active) → register file SPRAM_STREAM → SPRAMArbiter →
|
||||
# real SPRAM → MISO, plus the SPRAM_END cleanup. SPRAM is uninitialised
|
||||
# here, so we check the stream completes (8 bytes, no underrun/hang)
|
||||
# rather than specific data.
|
||||
dma = await exi_read(ctx, 0x0100, 8)
|
||||
print(f"T4 DMA read (8B from 0x100): {[f'0x{b:02X}' for b in dma]}")
|
||||
if len(dma) != 8:
|
||||
errors.append(f"T4 DMA read length: got {len(dma)}")
|
||||
await ctx.tick("capture").repeat(HALF)
|
||||
|
||||
# T5: a register read after the streaming read confirms the FSM cleaned
|
||||
# up (SPRAM_END → HEADER0) and the device is responsive again.
|
||||
nways2 = await exi_read(ctx, 0x31, 1)
|
||||
print(f"T5 NWAYS after DMA: 0x{nways2[0]:02X} (want 0x17)")
|
||||
if nways2 != [0x17]:
|
||||
errors.append(f"T5 NWAYS after DMA read: got {nways2}")
|
||||
await ctx.tick("capture").repeat(HALF)
|
||||
|
||||
# ── T6: FULL ETHERNET→SPRAM→GC LOOP ──────────────────────────────
|
||||
# A frame arrives from the network (W5500 model) → W5500 master reads it
|
||||
# → RXFrameAssembler writes it to the SPRAM ring → GC reads RWP then
|
||||
# DMA-reads the descriptor+frame back. Exercises the entire RX path.
|
||||
# The W5100 needs its init sequence (which sets MR.AI / opens socket 0)
|
||||
# before multi-byte bus accesses work — trigger it via NCRA reset, as
|
||||
# the real GC driver does, and let it run before enabling RX.
|
||||
await exi_write(ctx, 0x00, [0x01]) # NCRA reset → init_req pulse
|
||||
await ctx.tick("capture").repeat(2000) # let W5100 init run
|
||||
await exi_write(ctx, 0x00, [0x08]) # NCRA SR bit → enable RX
|
||||
await ctx.tick("capture").repeat(HALF * 2)
|
||||
ctx.set(dut.w5100_int_n, 0) # W5100: a packet was received
|
||||
await ctx.tick("capture").repeat(4000) # let the W5100 RX + SPRAM write run
|
||||
ctx.set(dut.w5100_int_n, 1)
|
||||
await ctx.tick("capture").repeat(HALF * 2)
|
||||
|
||||
rwp = await exi_read(ctx, 0x16, 1) # RX write pointer (page)
|
||||
total_len = len(RX_FRAME) + 4
|
||||
got = await exi_read(ctx, 0x0100, total_len) # descriptor + frame
|
||||
want = [0x00, 0x00, (total_len >> 8) & 0xFF, total_len & 0xFF] + RX_FRAME
|
||||
print(f"T6 RWP=0x{rwp[0]:02X} (want 0x02)")
|
||||
print(f"T6 SPRAM[0x100]: {[f'0x{b:02X}' for b in got]}")
|
||||
print(f"T6 expected : {[f'0x{b:02X}' for b in want]}")
|
||||
if rwp != [0x02]:
|
||||
errors.append(f"T6 RWP: got {rwp}, want [0x02]")
|
||||
if got != want:
|
||||
errors.append(f"T6 RX frame mismatch:\n got {got}\n want {want}")
|
||||
|
||||
# T7: status-panel integration — after all the EXI traffic above, the
|
||||
# EXI-activity LED (panel led[1] = stretched cs_active) must be lit,
|
||||
# proving cap.cs_active → FFSync → StatusPanel → LED is wired end-to-end.
|
||||
leds = ctx.get(dut.panel_led)
|
||||
if not (leds >> 1) & 1:
|
||||
errors.append(f"T7 panel: EXI-activity LED not lit (led=0b{leds:05b})")
|
||||
print(f"T7 panel led=0b{leds:05b} (bit1=EXI activity, expect 1)")
|
||||
|
||||
sim = Simulator(dut)
|
||||
sim.add_clock(Period(MHz=54), domain="capture")
|
||||
sim.add_clock(Period(MHz=24), domain="exi")
|
||||
sim.add_clock(Period(MHz=24), domain="sync")
|
||||
sim.add_testbench(testbench)
|
||||
sim.add_process(w5100_model)
|
||||
sim.run()
|
||||
|
||||
if errors:
|
||||
print("\nFAILURES:")
|
||||
for e in errors:
|
||||
print(" ", e)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("\nAll BBATop integration tests passed.")
|
||||
Reference in New Issue
Block a user