Files
rebbarb/exi_bba/spram_arbiter.py
T
2026-06-13 18:35:38 +02:00

277 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""SPRAM arbiter — sync domain (24 MHz).
Owns the iCE40UP5K 128 KB SPRAM (SB_SPRAM256KA, 16-bit wide) and arbitrates
between two clients:
Client A (EXI read) : prefetch pipeline; low priority.
Client B (ETH write): RXFrameAssembler; high priority.
ETH writes win when both clients are active. This is safe because the GC only
reads pages that the ETH engine has already finished writing (ring-buffer
invariant).
SPRAM addressing
-----------------
SB_SPRAM256KA is 64 K × 16-bit. Byte addressing:
ADDRESS = byte_addr >> 1
MASKWREN[3:0]:
0b0011 → write lower byte (byte_addr even)
0b1100 → write upper byte (byte_addr odd)
Read: both bytes returned; pick the right one from DATAOUT based on addr bit 0.
Read latency: 1 synchronous cycle — result of cycle N is valid at N+1.
In simulation (platform is None) a behavioural Array model is used instead of
the SB_SPRAM256KA Instance so tests run without IceStorm.
"""
from amaranth import *
from amaranth.lib.memory import Memory
__all__ = ["SPRAMArbiter"]
_SPRAM_WORDS = 65536 # 64 K 16-bit words = 128 KB
class SPRAMArbiter(Elaboratable):
"""Arbitrated SPRAM controller in the sync domain.
EXI read interface (from BBARegisterFile spram_req / spram_rsp FIFOs)
----------------------------------------------------------------------
exi_req_addr : 16-bit byte address to read
exi_req_valid : FIFO r_rdy — a request is waiting
exi_req_ready : FIFO r_en — pop the request (asserted when serviced)
exi_rsp_data : 8-bit result byte
exi_rsp_valid : FIFO w_en — push result when valid
ETH write interface (from RXFrameAssembler)
-------------------------------------------
eth_wr_addr : 16-bit byte address to write
eth_wr_data : 8-bit byte value
eth_wr_valid : write request present
eth_wr_ready : write accepted this cycle
"""
def __init__(self):
# EXI read interface
self.exi_req_addr = Signal(16)
self.exi_req_valid = Signal()
self.exi_req_ready = Signal()
self.exi_rsp_data = Signal(8)
self.exi_rsp_valid = Signal()
# ETH write interface
self.eth_wr_addr = Signal(16)
self.eth_wr_data = Signal(8)
self.eth_wr_valid = Signal()
self.eth_wr_ready = Signal()
def elaborate(self, platform):
m = Module()
# ── SPRAM instantiation (hardware vs simulation) ──────────────────
spram_addr = Signal(14) # word address (byte_addr >> 1)
spram_din = Signal(16)
spram_dout = Signal(16)
spram_wren = Signal()
spram_mask = Signal(4) # MASKWREN
if platform is None:
# Behavioural model: synchronous read with 1-cycle latency.
# Memory is a Component; read/write ports are obtained from it
# and wired via its submodule ports (not added as separate submodules).
mem = Memory(shape=16, depth=_SPRAM_WORDS, init=[])
m.submodules.mem = mem
mem_rd = mem.read_port(domain="sync", transparent_for=[])
mem_wr = mem.write_port(domain="sync", granularity=8)
# en[0] = lower byte enable, en[1] = upper byte enable
byte0_en = Signal()
byte1_en = Signal()
m.d.comb += [
byte0_en .eq(spram_wren & (spram_mask[0] | spram_mask[1])),
byte1_en .eq(spram_wren & (spram_mask[2] | spram_mask[3])),
mem_rd.addr .eq(spram_addr),
mem_rd.en .eq(1),
spram_dout .eq(mem_rd.data),
mem_wr.addr .eq(spram_addr),
mem_wr.data .eq(spram_din),
mem_wr.en .eq(Cat(byte0_en, byte1_en)),
]
else:
# Hardware: instantiate two SB_SPRAM256KA (64K×16 each; use one)
m.submodules.spram = Instance(
"SB_SPRAM256KA",
i_ADDRESS = spram_addr,
i_DATAIN = spram_din,
i_MASKWREN = spram_mask,
i_WREN = spram_wren,
i_CHIPSELECT = Const(1, 1),
i_CLOCK = ClockSignal("sync"),
i_STANDBY = Const(0, 1),
i_SLEEP = Const(0, 1),
i_POWEROFF = Const(1, 1),
o_DATAOUT = spram_dout,
)
# ── Arbiter pipeline ─────────────────────────────────────────────
# Stage 1: issue SPRAM address and control signals (combinatorial)
# Stage 2: capture SPRAM output into rsp_buf (synchronous, 1-cycle)
read_pending = Signal() # a read address was issued last cycle
read_was_odd = Signal() # byte address bit 0 of the pending read
rsp_buf = Signal(8) # registered response byte; valid when exi_rsp_valid
# Combinatorial defaults
m.d.comb += [
spram_wren .eq(0),
spram_mask .eq(0),
spram_din .eq(0),
spram_addr .eq(0),
self.exi_req_ready.eq(0),
self.eth_wr_ready .eq(0),
self.exi_rsp_data .eq(rsp_buf), # always sourced from registered buffer
]
# Registered defaults
m.d.sync += [
self.exi_rsp_valid.eq(0),
read_pending .eq(0),
]
# ETH write has priority
with m.If(self.eth_wr_valid):
m.d.comb += [
spram_addr .eq(self.eth_wr_addr[1:]),
spram_wren .eq(1),
self.eth_wr_ready.eq(1),
]
with m.If(self.eth_wr_addr[0]):
m.d.comb += [
spram_din [8:16].eq(self.eth_wr_data),
spram_mask .eq(0b1100),
]
with m.Else():
m.d.comb += [
spram_din [0:8].eq(self.eth_wr_data),
spram_mask .eq(0b0011),
]
# EXI read (lower priority)
with m.Elif(self.exi_req_valid):
m.d.comb += [
spram_addr .eq(self.exi_req_addr[1:]),
self.exi_req_ready.eq(1),
]
m.d.sync += [
read_pending.eq(1),
read_was_odd.eq(self.exi_req_addr[0]),
]
# Capture SPRAM output into registered buffer after 1-cycle latency
with m.If(read_pending):
with m.If(read_was_odd):
m.d.sync += rsp_buf.eq(spram_dout[8:16])
with m.Else():
m.d.sync += rsp_buf.eq(spram_dout[0:8])
m.d.sync += self.exi_rsp_valid.eq(1)
return m
# ── Testbench ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
import sys
from amaranth.sim import Simulator, Period
dut = SPRAMArbiter()
errors = []
async def testbench(ctx):
await ctx.tick("sync").repeat(2)
# T1: ETH write to even byte address 0x0100, then EXI read it back
ctx.set(dut.eth_wr_addr, 0x0100)
ctx.set(dut.eth_wr_data, 0xAB)
ctx.set(dut.eth_wr_valid, 1)
await ctx.tick("sync").repeat(1)
accepted = ctx.get(dut.eth_wr_ready)
if not accepted:
errors.append("T1 eth write not accepted")
ctx.set(dut.eth_wr_valid, 0)
await ctx.tick("sync").repeat(1)
# Issue EXI read of the same address
ctx.set(dut.exi_req_addr, 0x0100)
ctx.set(dut.exi_req_valid, 1)
await ctx.tick("sync").repeat(1) # clock A: read issued, read_pending=1
ctx.set(dut.exi_req_valid, 0)
await ctx.tick("sync").repeat(1) # clock B: SPRAM output captured, valid=1
# Check HERE — exi_rsp_valid is 1 for exactly this one cycle
rdata = ctx.get(dut.exi_rsp_data)
rvalid = ctx.get(dut.exi_rsp_valid)
if rdata != 0xAB:
errors.append(f"T1 read back: expected 0xAB, got 0x{rdata:02X}")
if not rvalid:
errors.append("T1 exi_rsp_valid not set")
print(f"T1 even addr read-back: data=0x{rdata:02X} valid={rvalid}")
await ctx.tick("sync").repeat(2)
# T2: ETH write to ODD byte address 0x0101, read back
ctx.set(dut.eth_wr_addr, 0x0101)
ctx.set(dut.eth_wr_data, 0xCD)
ctx.set(dut.eth_wr_valid, 1)
await ctx.tick("sync").repeat(1)
ctx.set(dut.eth_wr_valid, 0)
await ctx.tick("sync").repeat(1)
ctx.set(dut.exi_req_addr, 0x0101)
ctx.set(dut.exi_req_valid, 1)
await ctx.tick("sync").repeat(1)
ctx.set(dut.exi_req_valid, 0)
await ctx.tick("sync").repeat(1)
rdata = ctx.get(dut.exi_rsp_data)
if rdata != 0xCD:
errors.append(f"T2 odd addr read-back: expected 0xCD, got 0x{rdata:02X}")
print(f"T2 odd addr read-back: data=0x{rdata:02X}")
await ctx.tick("sync").repeat(2)
# T3: ETH write wins when both clients active simultaneously
# Write 0xEE to 0x0200
ctx.set(dut.eth_wr_addr, 0x0200)
ctx.set(dut.eth_wr_data, 0xEE)
ctx.set(dut.eth_wr_valid, 1)
ctx.set(dut.exi_req_addr, 0x0100) # also wants to read
ctx.set(dut.exi_req_valid, 1)
await ctx.tick("sync").repeat(1)
eth_won = ctx.get(dut.eth_wr_ready)
exi_blocked = not ctx.get(dut.exi_req_ready)
ctx.set(dut.eth_wr_valid, 0)
ctx.set(dut.exi_req_valid, 0)
if not eth_won:
errors.append("T3 ETH priority: ETH write not accepted")
if not exi_blocked:
errors.append("T3 ETH priority: EXI read was not blocked")
print(f"T3 ETH priority: eth_won={eth_won} exi_blocked={exi_blocked}")
sim = Simulator(dut)
sim.add_clock(Period(MHz=24), domain="sync")
sim.add_testbench(testbench)
with sim.write_vcd("SPRAMArbiter.vcd"):
sim.run()
if errors:
print("\nFAILURES:")
for e in errors:
print(" ", e)
sys.exit(1)
else:
print("\nAll tests passed.")