Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
58322a1d2This file is pinned to the selected release. Download the original to open it in your design tool.
#
# This file is part of LiteX.
#
# Copyright (c) 2019-2022 Florent Kermarrec <florent@enjoy-digital.fr>
# SPDX-License-Identifier: BSD-2-Clause
import unittest
import random
from migen import *
from litex.gen import *
from litex.soc.interconnect.axi import *
from litex.soc.interconnect import wishbone
from litex.soc.integration.soc import SoCRegion
# Software Models ----------------------------------------------------------------------------------
class Burst:
def __init__(self, addr, type=BURST_FIXED, len=0, size=0):
self.addr = addr
self.type = type
self.len = len
self.size = size
def to_beats(self):
r = []
burst_length = self.len + 1
burst_size = 2**self.size
for i in range(burst_length):
if self.type == BURST_INCR:
offset = i*2**(self.size)
r += [Beat(self.addr + offset)]
elif self.type == BURST_WRAP:
assert burst_length in [2, 4, 8, 16]
assert (self.addr % burst_size) == 0
burst_base = self.addr - self.addr % (burst_length * burst_size)
burst_offset = self.addr % (burst_length * burst_size)
burst_addr = burst_base + (burst_offset + i*burst_size) % (burst_length * burst_size)
#print("0x{:08x}".format(burst_addr))
r += [Beat(burst_addr)]
else:
r += [Beat(self.addr)]
return r
class Beat:
def __init__(self, addr):
self.addr = addr
class Access(Burst):
def __init__(self, addr, data, id, **kwargs):
Burst.__init__(self, addr, **kwargs)
self.data = data
self.id = id
class Write(Access): pass
class Read(Access): pass
# AXI4 driver helpers (used by AXISRAM tests) ------------------------------------------------------
def axi_aw_send(axi, addr, burst_len=0, burst_type=BURST_INCR, size=2, id=0):
yield axi.aw.valid.eq(1)
yield axi.aw.addr.eq(addr)
yield axi.aw.burst.eq(burst_type)
yield axi.aw.len.eq(burst_len)
yield axi.aw.size.eq(size)
yield axi.aw.id.eq(id)
yield
while (yield axi.aw.ready) == 0:
yield
yield axi.aw.valid.eq(0)
def axi_w_send(axi, data, last, strb=None):
yield axi.w.valid.eq(1)
yield axi.w.data.eq(data)
yield axi.w.strb.eq(strb if strb is not None else 2**(axi.data_width//8) - 1)
yield axi.w.last.eq(int(last))
yield
while (yield axi.w.ready) == 0:
yield
yield axi.w.valid.eq(0)
def axi_b_recv(axi):
yield axi.b.ready.eq(1)
while (yield axi.b.valid) == 0:
yield
resp = (yield axi.b.resp)
yield axi.b.ready.eq(0)
yield
return resp
def axi_ar_send(axi, addr, burst_len=0, burst_type=BURST_INCR, size=2, id=0):
yield axi.ar.valid.eq(1)
yield axi.ar.addr.eq(addr)
yield axi.ar.burst.eq(burst_type)
yield axi.ar.len.eq(burst_len)
yield axi.ar.size.eq(size)
yield axi.ar.id.eq(id)
yield
while (yield axi.ar.ready) == 0:
yield
yield axi.ar.valid.eq(0)
def axi_r_recv_one(axi):
yield axi.r.ready.eq(1)
while (yield axi.r.valid) == 0:
yield
data = (yield axi.r.data)
resp = (yield axi.r.resp)
last = (yield axi.r.last)
yield axi.r.ready.eq(0)
yield
return data, resp, last
def axi_write_single(axi, addr, data, size=2):
yield from axi_aw_send(axi, addr, size=size)
yield from axi_w_send(axi, data, last=True)
return (yield from axi_b_recv(axi))
def axi_read_single(axi, addr, size=2):
yield from axi_ar_send(axi, addr, size=size)
data, resp, _ = yield from axi_r_recv_one(axi)
return data, resp
def axi_write_burst(axi, addr, beats, burst_type=BURST_INCR, size=2):
yield from axi_aw_send(axi, addr, burst_len=len(beats) - 1, burst_type=burst_type, size=size)
for i, beat in enumerate(beats):
yield from axi_w_send(axi, beat, last=(i == len(beats) - 1))
return (yield from axi_b_recv(axi))
def axi_read_burst(axi, addr, length, burst_type=BURST_INCR, size=2):
yield from axi_ar_send(axi, addr, burst_len=length - 1, burst_type=burst_type, size=size)
datas = []
for _ in range(length):
data, resp, _last = yield from axi_r_recv_one(axi)
if resp != RESP_OKAY:
return None
datas.append(data)
return datas
# TestAXI ------------------------------------------------------------------------------------------
class TestAXI(unittest.TestCase):
def test_axi_remapper_origin_region_remap(self):
class DUT(LiteXModule):
def __init__(self):
self.master = AXIInterface(data_width=32, address_width=32)
self.slave = AXIInterface(data_width=32, address_width=32)
self.remapper = AXIRemapper(self.master, self.slave,
origin = 0x6000_0000,
size = 0x0100_0000,
src_regions = [
SoCRegion(origin=0x6000_0000, size=0x1000),
SoCRegion(origin=0x6001_0000, size=0x1000),
],
dst_regions = [
SoCRegion(origin=0xe000_0000, size=0x1000),
SoCRegion(origin=0xe100_0000, size=0x1000),
],
)
def gen(dut):
remaps = [
(0x0000_0040, 0xe000_0040),
(0x0001_0040, 0xe100_0040),
(0x0002_0040, 0x6002_0040),
]
for addr, expected in remaps:
yield dut.master.aw.addr.eq(addr)
yield dut.master.ar.addr.eq(addr)
yield
self.assertEqual((yield dut.slave.aw.addr), expected)
self.assertEqual((yield dut.slave.ar.addr), expected)
dut = DUT()
run_simulation(dut, gen(dut))
with self.assertRaises(ValueError):
AXIRemapper(dut.master, dut.slave, size=0x3000)
with self.assertRaises(ValueError):
AXIRemapper(dut.master, dut.slave,
src_regions = [SoCRegion(origin=0x0000_0000, size=0x1000)],
dst_regions = [])
def test_burst2beat(self):
# Each burst is given a distinct id and the expected beats for each burst carry that id +
# an `is_last` flag asserted on the final beat. The checker verifies all three fields.
def bursts_generator(ax, bursts, valid_rand=50):
prng = random.Random(42)
for burst, burst_id in bursts:
yield ax.valid.eq(1)
yield ax.addr.eq(burst.addr)
yield ax.burst.eq(burst.type)
yield ax.len.eq(burst.len)
yield ax.size.eq(burst.size)
yield ax.id.eq(burst_id)
while (yield ax.ready) == 0:
yield
yield ax.valid.eq(0)
while prng.randrange(100) < valid_rand:
yield
yield
@passive
def beats_checker(ax, expected_beats, ready_rand=50):
self.errors = 0
yield ax.ready.eq(0)
prng = random.Random(42)
for beat_addr, beat_id, beat_last in expected_beats:
while ((yield ax.valid) and (yield ax.ready)) == 0:
if prng.randrange(100) > ready_rand:
yield ax.ready.eq(1)
else:
yield ax.ready.eq(0)
yield
if (yield ax.addr) != beat_addr:
self.errors += 1
if (yield ax.id) != beat_id:
self.errors += 1
if bool((yield ax.last)) != beat_last:
self.errors += 1
yield
# DUT
ax_burst = AXIStreamInterface(layout=ax_description(32), id_width=32)
ax_beat = AXIStreamInterface(layout=ax_description(32), id_width=32)
dut = AXIBurst2Beat(ax_burst, ax_beat)
# Generate DUT input (bursts) — random FIXED/INCR plus hand-crafted WRAP.
prng = random.Random(42)
bursts = []
for i in range(32):
bursts.append((Burst(prng.randrange(2**32), BURST_FIXED, prng.randrange(255), log2_int(32//8)), prng.randrange(2**32)))
bursts.append((Burst(prng.randrange(2**32), BURST_INCR, prng.randrange(255), log2_int(32//8)), prng.randrange(2**32)))
bursts.append((Burst(4, BURST_WRAP, 4-1, log2_int(2)), 0xa5a5a5a5))
bursts.append((Burst(0x80000160, BURST_WRAP, 0x3, 0b100), 0xdeadbeef))
# Edge cases: 1-beat (degenerate) and max-length (256) INCR bursts.
bursts.append((Burst(0x1000, BURST_INCR, 0, log2_int(32//8)), 0x1111))
bursts.append((Burst(0x2000, BURST_INCR, 255, log2_int(32//8)), 0x2222))
# Build expected beats with id + last carried through.
expected_beats = []
for burst, burst_id in bursts:
beats = burst.to_beats()
for i, beat in enumerate(beats):
expected_beats.append((beat.addr, burst_id, i == len(beats) - 1))
# Simulation
generators = [
bursts_generator(ax_burst, bursts),
beats_checker(ax_beat, expected_beats)
]
run_simulation(dut, generators)
self.assertEqual(self.errors, 0)
def test_axi2axilite_bridge(self):
# Focused AXI2AXILite test. We instantiate the bridge alone (its slave side feeds an
# AXILiteSRAM that we trust from the AXI-Lite tests) and verify that:
# - each AXI4 burst on the master side decomposes into the expected sequence of
# single-beat AXI-Lite transactions on the slave side,
# - the data returned for read bursts reassembles correctly,
# - response codes propagate (RESP_OKAY).
# We catch the AXI-Lite ar.addr / aw.addr per-beat with a passive observer so we can check
# the address pattern produced by the bridge for INCR / FIXED / WRAP bursts.
class DUT(LiteXModule):
def __init__(self, mem_bytes=4096, init=None):
self.axi = AXIInterface(data_width=32, address_width=32, id_width=4)
self.axi_lite = AXILiteInterface(data_width=32, address_width=32)
self.bridge = AXI2AXILite(self.axi, self.axi_lite)
self.sram = AXILiteSRAM(mem_bytes, init=init or [], bus=self.axi_lite)
self.errors = 0
@passive
def ar_addr_observer(axi_lite, log):
# Captures (addr) for each AR handshake.
while True:
if (yield axi_lite.ar.valid) and (yield axi_lite.ar.ready):
log.append((yield axi_lite.ar.addr))
yield
@passive
def aw_addr_observer(axi_lite, log):
# Captures (addr) for each AW handshake.
while True:
if (yield axi_lite.aw.valid) and (yield axi_lite.aw.ready):
log.append((yield axi_lite.aw.addr))
yield
# Compute expected AXI-Lite addresses produced by the bridge for a given AXI burst.
def _expected_axil_addrs(addr, length, burst_type, size_log2):
beats = []
size_bytes = 2**size_log2
burst_size_bytes = length * size_bytes
for i in range(length):
if burst_type == BURST_INCR:
beats.append(addr + i*size_bytes)
elif burst_type == BURST_FIXED:
beats.append(addr)
elif burst_type == BURST_WRAP:
base = addr - addr % burst_size_bytes
off = (addr % burst_size_bytes + i*size_bytes) % burst_size_bytes
beats.append(base + off)
return beats
def gen(dut, ar_log, aw_log):
prng = random.Random(0xb007)
scenarios = [
# (label, addr, len, burst_type, size_log2)
("INCR-1", 0x000, 1, BURST_INCR, 2),
("INCR-4", 0x010, 4, BURST_INCR, 2),
("INCR-16", 0x020, 16, BURST_INCR, 2),
("FIXED-4", 0x080, 4, BURST_FIXED, 2),
("FIXED-8", 0x080, 8, BURST_FIXED, 2),
("WRAP-4 mid-blk", 0x108, 4, BURST_WRAP, 2), # block 0x100..0x10F, start at +8
("WRAP-8 mid-blk", 0x214, 8, BURST_WRAP, 2),
]
for label, addr, length, burst_type, size_log2 in scenarios:
expected = _expected_axil_addrs(addr, length, burst_type, size_log2)
# WRITE burst: snapshot aw_log boundary, drive write, check log.
aw_before = len(aw_log)
beats = [prng.randrange(2**32) for _ in range(length)]
resp = yield from axi_write_burst(dut.axi, addr, beats,
burst_type=burst_type, size=size_log2)
if resp != RESP_OKAY:
dut.errors += 1
# Allow a few cycles for the bridge to fully drain.
for _ in range(8):
yield
got_aw = aw_log[aw_before:]
if got_aw != expected:
dut.errors += 1
# READ burst: snapshot ar_log boundary, drive read, check log + data.
ar_before = len(ar_log)
read = yield from axi_read_burst(dut.axi, addr, length,
burst_type=burst_type, size=size_log2)
for _ in range(8):
yield
got_ar = ar_log[ar_before:]
if got_ar != expected:
dut.errors += 1
# FIXED bursts re-write the same address each beat → memory holds beats[-1].
# INCR / WRAP write distinct cells → reads should return whatever was last written
# at each cell. We compute the model accordingly.
if read is None:
dut.errors += 1
else:
if burst_type == BURST_FIXED:
# All beats target the same address; only beats[-1] survives.
# The burst read also targets the same address each beat; the read
# model returns the same value back from the sram each beat.
if any(r != beats[-1] for r in read):
dut.errors += 1
else:
# INCR / WRAP: same physical addresses, same order — data must round-trip.
if read != beats:
dut.errors += 1
dut = DUT(mem_bytes=4096)
ar_log, aw_log = [], []
run_simulation(dut, [
gen(dut, ar_log, aw_log),
ar_addr_observer(dut.axi_lite, ar_log),
aw_addr_observer(dut.axi_lite, aw_log),
])
self.assertEqual(dut.errors, 0)
def _test_axi2wishbone(self,
naccesses=16, simultaneous_writes_reads=False,
# Random: 0: min (no random), 100: max.
# Burst randomness.
id_rand_enable = False,
len_rand_enable = False,
data_rand_enable = False,
# Flow valid randomness.
aw_valid_random = 0,
w_valid_random = 0,
ar_valid_random = 0,
r_valid_random = 0,
# Flow ready randomness.
w_ready_random = 0,
b_ready_random = 0,
r_ready_random = 0
):
def writes_cmd_generator(axi_port, writes):
prng = random.Random(42)
for write in writes:
while prng.randrange(100) < aw_valid_random:
yield
# Send command.
yield axi_port.aw.valid.eq(1)
yield axi_port.aw.addr.eq(write.addr<<2)
yield axi_port.aw.burst.eq(write.type)
yield axi_port.aw.len.eq(write.len)
yield axi_port.aw.size.eq(write.size)
yield axi_port.aw.id.eq(write.id)
yield
while (yield axi_port.aw.ready) == 0:
yield
yield axi_port.aw.valid.eq(0)
def writes_data_generator(axi_port, writes):
prng = random.Random(42)
yield axi_port.w.strb.eq(2**(len(axi_port.w.data)//8) - 1)
for write in writes:
for i, data in enumerate(write.data):
while prng.randrange(100) < w_valid_random:
yield
# Send data.
yield axi_port.w.valid.eq(1)
if (i == (len(write.data) - 1)):
yield axi_port.w.last.eq(1)
else:
yield axi_port.w.last.eq(0)
yield axi_port.w.data.eq(data)
yield
while (yield axi_port.w.ready) == 0:
yield
yield axi_port.w.valid.eq(0)
axi_port.reads_enable = True
def writes_response_generator(axi_port, writes):
prng = random.Random(42)
self.writes_id_errors = 0
for write in writes:
# wait response
yield axi_port.b.ready.eq(0)
yield
while (yield axi_port.b.valid) == 0:
yield
while prng.randrange(100) < b_ready_random:
yield
yield axi_port.b.ready.eq(1)
yield
if (yield axi_port.b.id) != write.id:
self.writes_id_errors += 1
def reads_cmd_generator(axi_port, reads):
prng = random.Random(42)
while not axi_port.reads_enable:
yield
for read in reads:
while prng.randrange(100) < ar_valid_random:
yield
# Send command.
yield axi_port.ar.valid.eq(1)
yield axi_port.ar.addr.eq(read.addr<<2)
yield axi_port.ar.burst.eq(read.type)
yield axi_port.ar.len.eq(read.len)
yield axi_port.ar.size.eq(read.size)
yield axi_port.ar.id.eq(read.id)
yield
while (yield axi_port.ar.ready) == 0:
yield
yield axi_port.ar.valid.eq(0)
def reads_response_data_generator(axi_port, reads):
prng = random.Random(42)
self.reads_data_errors = 0
self.reads_id_errors = 0
self.reads_last_errors = 0
while not axi_port.reads_enable:
yield
for read in reads:
for i, data in enumerate(read.data):
# Wait data / response.
yield axi_port.r.ready.eq(0)
yield
while (yield axi_port.r.valid) == 0:
yield
while prng.randrange(100) < r_ready_random:
yield
yield axi_port.r.ready.eq(1)
yield
if (yield axi_port.r.data) != data:
self.reads_data_errors += 1
if (yield axi_port.r.id) != read.id:
self.reads_id_errors += 1
if i == (len(read.data) - 1):
if (yield axi_port.r.last) != 1:
self.reads_last_errors += 1
else:
if (yield axi_port.r.last) != 0:
self.reads_last_errors += 1
# DUT
class DUT(Module):
def __init__(self):
self.axi = AXIInterface(data_width=32, address_width=32, id_width=8)
self.wishbone = wishbone.Interface(data_width=32, adr_width=30, addressing="word")
axi2wishbone = AXI2Wishbone(self.axi, self.wishbone)
self.submodules += axi2wishbone
wishbone_mem = wishbone.SRAM(1024, bus=self.wishbone)
self.submodules += wishbone_mem
dut = DUT()
# Generate writes/reads.
prng = random.Random(42)
writes = []
offset = 1
for i in range(naccesses):
_id = prng.randrange(2**8) if id_rand_enable else i
_len = prng.randrange(32) if len_rand_enable else i
_data = [prng.randrange(2**32) if data_rand_enable else j for j in range(_len + 1)]
writes.append(Write(offset, _data, _id, type=BURST_INCR, len=_len, size=log2_int(32//8)))
offset += _len + 1
# Dummy reads to ensure datas have been written before the effective reads start.
dummy_reads = [Read(1023, [0], 0, type=BURST_FIXED, len=0, size=log2_int(32//8)) for _ in range(32)]
reads = writes
# Simulation
if simultaneous_writes_reads:
dut.axi.reads_enable = True
else:
dut.axi.reads_enable = False # Will be set by writes_data_generator.
generators = [
writes_cmd_generator(dut.axi, writes),
writes_data_generator(dut.axi, writes),
writes_response_generator(dut.axi, writes),
reads_cmd_generator(dut.axi, reads),
reads_response_data_generator(dut.axi, reads)
]
run_simulation(dut, generators)
self.assertEqual(self.writes_id_errors, 0)
self.assertEqual(self.reads_data_errors, 0)
self.assertEqual(self.reads_id_errors, 0)
self.assertEqual(self.reads_last_errors, 0)
# Test with no randomness.
def test_axi2wishbone_writes_then_reads_no_random(self):
self._test_axi2wishbone(simultaneous_writes_reads=False)
# Test randomness one parameter at a time.
def test_axi2wishbone_writes_then_reads_random_bursts(self):
self._test_axi2wishbone(
simultaneous_writes_reads = False,
id_rand_enable = True,
len_rand_enable = True,
data_rand_enable = True)
def test_axi2wishbone_random_w_ready(self):
self._test_axi2wishbone(w_ready_random=90)
def test_axi2wishbone_random_b_ready(self):
self._test_axi2wishbone(b_ready_random=90)
def test_axi2wishbone_random_r_ready(self):
self._test_axi2wishbone(r_ready_random=90)
def test_axi2wishbone_random_aw_valid(self):
self._test_axi2wishbone(aw_valid_random=90)
def test_axi2wishbone_random_w_valid(self):
self._test_axi2wishbone(w_valid_random=90)
def test_axi2wishbone_random_ar_valid(self):
self._test_axi2wishbone(ar_valid_random=90)
def test_axi2wishbone_random_r_valid(self):
self._test_axi2wishbone(r_valid_random=90)
# Now let's stress things a bit... :)
def test_axi2wishbone_random_all(self):
self._test_axi2wishbone(
simultaneous_writes_reads = False,
id_rand_enable = True,
len_rand_enable = True,
aw_valid_random = 50,
w_ready_random = 50,
b_ready_random = 50,
w_valid_random = 50,
ar_valid_random = 90,
r_valid_random = 90,
r_ready_random = 90
)
def test_axi_down_converter(self):
class DUT(LiteXModule):
def __init__(self, dw_from=64, dw_to=32):
self.axi_master = AXIInterface(data_width=dw_from)
axi_slave = AXIInterface(data_width=dw_to)
wb_slave = wishbone.Interface(data_width=dw_to, address_width=axi_slave.address_width, addressing="word")
self.converter = AXIConverter(self.axi_master, axi_slave)
self.axi2wb = AXI2Wishbone(axi_slave, wb_slave)
self.mem = wishbone.SRAM(1024, bus=wb_slave, init=range(256))
def read_generator(dut):
axi_port = dut.axi_master
# AXI Read.
addr = 0x34
yield axi_port.ar.addr.eq(addr * 4)
yield axi_port.ar.valid.eq(1)
yield axi_port.ar.burst.eq(0b1)
yield axi_port.ar.len.eq(0)
yield axi_port.ar.size.eq(0b011)
yield axi_port.r.ready.eq(1)
yield
while (yield axi_port.r.valid) == 0:
yield
rd = (yield axi_port.r.data)
# Check Mem Content.
mem_content = 0
i = 0
while i < axi_port.data_width // dut.mem.bus.data_width:
mem_content |= (yield dut.mem.mem[addr + i]) << (i * dut.mem.bus.data_width)
i += 1
assert rd == mem_content, (hex(rd), hex(mem_content))
def write_generator(dut):
axi_port = dut.axi_master
# AXI Write.
addr = 0x24
data = 0x98761244
yield axi_port.aw.addr.eq(addr * 4)
yield axi_port.aw.valid.eq(1)
yield axi_port.aw.burst.eq(0b1)
yield axi_port.aw.len.eq(0)
yield axi_port.aw.size.eq(0b011)
yield axi_port.w.strb.eq(0b111111111)
yield axi_port.w.data.eq(data)
yield axi_port.w.valid.eq(1)
yield axi_port.w.last.eq(1)
yield
while (yield axi_port.aw.ready) == 0:
yield
yield axi_port.aw.valid.eq(0)
while (yield axi_port.w.ready) == 0:
yield
yield axi_port.w.valid.eq(0)
# Check Mem Content.
mem_content = 0
i = 0
while i < axi_port.data_width // dut.mem.bus.data_width:
mem_content |= (yield dut.mem.mem[addr + i]) << (i * dut.mem.bus.data_width)
i += 1
assert data == mem_content, (hex(data), hex(mem_content))
dut = DUT(64, 32)
run_simulation(dut, [read_generator(dut), write_generator(dut)], vcd_name="sim.vcd")
def test_axi_up_converter_single_beat_lane_writes(self):
class DUT(LiteXModule):
def __init__(self, dw_narrow=128, dw_wide=256, mem_bytes=4096):
self.axi_narrow = AXIInterface(data_width=dw_narrow, address_width=32, id_width=4)
self.axi_wide = AXIInterface(data_width=dw_wide, address_width=32, id_width=4)
self.converter = AXIUpConverter(self.axi_narrow, self.axi_wide)
self.sram = AXISRAM(mem_bytes, bus=self.axi_wide, init=[0]*(mem_bytes//(dw_wide//8)))
self.errors = 0
def gen(dut):
size = log2_int(dut.axi_narrow.data_width//8)
narrow_bytes = dut.axi_narrow.data_width//8
wide_bytes = dut.axi_wide.data_width//8
narrow_mask = (1 << dut.axi_narrow.data_width) - 1
lane_xor = sum(0x11 << (8*i) for i in range(narrow_bytes))
beats = [
0x0123456789abcdef0badf00d10203040 & narrow_mask,
0x123456789abcdef00badf00d20304050 & narrow_mask,
0x23456789abcdef010badf00d30405060 & narrow_mask,
0x3456789abcdef0120badf00d40506070 & narrow_mask,
]
# Single-beat writes hit alternating lanes of each wider target word. This is the
# pattern used by Rocket's L2 when accepting SDCard DMA writes.
for i, beat in enumerate(beats):
resp = yield from axi_write_single(dut.axi_narrow, i*narrow_bytes, beat, size=size)
if resp != RESP_OKAY:
dut.errors += 1
for i, expected in enumerate(beats):
data, resp = yield from axi_read_single(dut.axi_narrow, i*narrow_bytes, size=size)
if resp != RESP_OKAY or data != expected:
dut.errors += 1
burst = [beat ^ lane_xor for beat in beats]
resp = yield from axi_write_burst(dut.axi_narrow, 0x100, burst, size=size)
if resp != RESP_OKAY:
dut.errors += 1
mem0 = (yield dut.sram.mem[0x100//wide_bytes])
mem1 = (yield dut.sram.mem[(0x100 + wide_bytes)//wide_bytes])
assert mem0 == (burst[0] | (burst[1] << dut.axi_narrow.data_width)), hex(mem0)
assert mem1 == (burst[2] | (burst[3] << dut.axi_narrow.data_width)), hex(mem1)
read = yield from axi_read_burst(dut.axi_narrow, 0x100, len(burst), size=size)
assert read == burst, ([hex(d) for d in read], [hex(d) for d in burst])
for dw_narrow, dw_wide in [(32, 64), (128, 256)]:
with self.subTest(dw_narrow=dw_narrow, dw_wide=dw_wide):
dut = DUT(dw_narrow=dw_narrow, dw_wide=dw_wide)
run_simulation(dut, gen(dut))
self.assertEqual(dut.errors, 0)
def test_axi_down_converter_burst(self):
# Exercises AXIDownConverter (now also used by SoC.add_sdram for the
# CPU.mem_axi.data_width > LiteDRAM.port.data_width case) on real bursts:
# a wide-side AXI master writes/reads bursts of various lengths and types,
# the converter forwards them onto a narrower AXI bus, and an AXISRAM at
# the narrow side serves the actual storage. We check both that the
# bridge generates the right narrow-side request count (each wide beat
# turns into `ratio` narrow beats), and that the data round-trips correctly.
class DUT(LiteXModule):
def __init__(self, dw_wide=64, dw_narrow=32, mem_bytes=4096):
self.axi_wide = AXIInterface(data_width=dw_wide, address_width=32, id_width=4)
self.axi_narrow = AXIInterface(data_width=dw_narrow, address_width=32, id_width=4)
self.converter = AXIDownConverter(self.axi_wide, self.axi_narrow)
self.sram = AXISRAM(mem_bytes, bus=self.axi_narrow, init=[0]*(mem_bytes//(dw_narrow//8)))
self.errors = 0
@passive
def narrow_w_beats(axi_narrow, counter):
while True:
if (yield axi_narrow.w.valid) and (yield axi_narrow.w.ready):
counter[0] += 1
yield
@passive
def narrow_r_beats(axi_narrow, counter):
while True:
if (yield axi_narrow.r.valid) and (yield axi_narrow.r.ready):
counter[0] += 1
yield
def gen(dut, dw_wide, dw_narrow, w_counter, r_counter):
prng = random.Random(0xd06e)
ratio = dw_wide // dw_narrow
wide_size = log2_int(dw_wide // 8) # bytes-per-beat encoded as log2.
wide_bytes = dw_wide // 8
# Sweep INCR burst lengths. After each wide-side burst of `length` beats we expect
# exactly `length * ratio` narrow-side data beats (the converter issues one narrow
# AW/AR per wide burst with len*ratio extension; the multiplication shows up on the
# W and R channels). Combined with the data round-trip check this verifies both the
# length expansion and the data ordering through the StrideConverters.
for length in [1, 2, 4, 8, 16]:
base = (length * wide_bytes * 8) # spaced base address per length
beats = [prng.randrange(2**dw_wide) for _ in range(length)]
w_before = w_counter[0]
resp = yield from axi_write_burst(dut.axi_wide, base, beats, size=wide_size)
if resp != RESP_OKAY:
dut.errors += 1
for _ in range(16):
yield
if w_counter[0] - w_before != length * ratio:
dut.errors += 1
r_before = r_counter[0]
read = yield from axi_read_burst(dut.axi_wide, base, length, size=wide_size)
for _ in range(16):
yield
if r_counter[0] - r_before != length * ratio:
dut.errors += 1
if read != beats:
dut.errors += 1
for dw_wide, dw_narrow in [(64, 32), (128, 32), (128, 64), (64, 16)]:
with self.subTest(dw_wide=dw_wide, dw_narrow=dw_narrow):
dut = DUT(dw_wide=dw_wide, dw_narrow=dw_narrow, mem_bytes=4096)
w_counter = [0]
r_counter = [0]
run_simulation(dut, [
gen(dut, dw_wide, dw_narrow, w_counter, r_counter),
narrow_w_beats(dut.axi_narrow, w_counter),
narrow_r_beats(dut.axi_narrow, r_counter),
])
self.assertEqual(dut.errors, 0)
# Reusable DUT for the focused AXIDownConverter probes that follow. The narrow side is fed
# by an AXISRAM so the data path round-trips through real storage.
class _DownConvDUT(LiteXModule):
def __init__(self, dw_wide=64, dw_narrow=32, mem_bytes=4096, init=None):
self.axi_wide = AXIInterface(data_width=dw_wide, address_width=32, id_width=4)
self.axi_narrow = AXIInterface(data_width=dw_narrow, address_width=32, id_width=4)
self.converter = AXIDownConverter(self.axi_wide, self.axi_narrow)
mem_words = mem_bytes // (dw_narrow // 8)
self.sram = AXISRAM(mem_bytes, bus=self.axi_narrow, init=init or [0]*mem_words)
self.errors = 0
class _DownConvRawDUT(LiteXModule):
def __init__(self):
self.axi_wide = AXIInterface(data_width=64, address_width=32, id_width=4)
self.axi_narrow = AXIInterface(data_width=32, address_width=32, id_width=4)
self.converter = AXIDownConverter(self.axi_wide, self.axi_narrow)
def test_axi_down_converter_narrow_single_read(self):
# A narrow read on a wide master port must stay a single downstream access. Expanding it
# to a full wide-word burst can be destructive with MMIO/CSR/FIFO-like slaves.
dw_wide = 256
dw_narrow = 32
narrow_bytes = dw_narrow // 8
lane = 5
addr = lane * narrow_bytes
init = [0x10000000 + i for i in range(1024)]
ar_log = []
results = {}
@passive
def monitor(dut):
while True:
if (yield dut.axi_narrow.ar.valid) and (yield dut.axi_narrow.ar.ready):
ar_log.append((
(yield dut.axi_narrow.ar.addr),
(yield dut.axi_narrow.ar.len),
(yield dut.axi_narrow.ar.size),
(yield dut.axi_narrow.ar.burst),
))
yield
def gen(dut):
yield from axi_ar_send(dut.axi_wide, addr, size=log2_int(narrow_bytes))
data, resp, last = yield from axi_r_recv_one(dut.axi_wide)
results["data"] = data
results["resp"] = resp
results["last"] = last
dut = self._DownConvDUT(dw_wide=dw_wide, dw_narrow=dw_narrow, mem_bytes=4096, init=init)
run_simulation(dut, [gen(dut), monitor(dut)])
expected_data = init[addr//narrow_bytes] << (lane*dw_narrow)
self.assertEqual(ar_log, [(addr, 0, log2_int(narrow_bytes), BURST_INCR)])
self.assertEqual(results["resp"], RESP_OKAY)
self.assertEqual(results["last"], 1)
self.assertEqual(results["data"], expected_data)
def test_axi_down_converter_narrow_single_write(self):
# Same as above for writes: only the selected wide-lane data/strobes should reach the
# narrow slave.
dw_wide = 256
dw_narrow = 32
narrow_bytes = dw_narrow // 8
lane = 5
addr = lane * narrow_bytes
value = 0xa5a55a5a
aw_log = []
w_log = []
results = {}
@passive
def monitor(dut):
while True:
if (yield dut.axi_narrow.aw.valid) and (yield dut.axi_narrow.aw.ready):
aw_log.append((
(yield dut.axi_narrow.aw.addr),
(yield dut.axi_narrow.aw.len),
(yield dut.axi_narrow.aw.size),
(yield dut.axi_narrow.aw.burst),
))
if (yield dut.axi_narrow.w.valid) and (yield dut.axi_narrow.w.ready):
w_log.append((
(yield dut.axi_narrow.w.data),
(yield dut.axi_narrow.w.strb),
(yield dut.axi_narrow.w.last),
))
yield
def gen(dut):
size = log2_int(narrow_bytes)
data = value << (lane*dw_narrow)
strb = ((1 << narrow_bytes) - 1) << (lane*narrow_bytes)
yield from axi_aw_send(dut.axi_wide, addr, size=size)
yield from axi_w_send(dut.axi_wide, data, last=True, strb=strb)
results["resp"] = yield from axi_b_recv(dut.axi_wide)
results["mem"] = (yield dut.sram.mem[addr//narrow_bytes])
dut = self._DownConvDUT(dw_wide=dw_wide, dw_narrow=dw_narrow, mem_bytes=4096)
run_simulation(dut, [gen(dut), monitor(dut)])
self.assertEqual(aw_log, [(addr, 0, log2_int(narrow_bytes), BURST_INCR)])
self.assertEqual(w_log, [(value, (1 << narrow_bytes) - 1, 1)])
self.assertEqual(results["resp"], RESP_OKAY)
self.assertEqual(results["mem"], value)
def test_axi_down_converter_narrow_interleave_write(self):
aw_log, w_log, b_count = [], [], [0]
@passive
def monitor(dut):
while True:
if (yield dut.axi_narrow.aw.valid) and (yield dut.axi_narrow.aw.ready):
aw_log.append((yield dut.axi_narrow.aw.addr))
if (yield dut.axi_narrow.w.valid) and (yield dut.axi_narrow.w.ready):
w_log.append(((yield dut.axi_narrow.w.data), (yield dut.axi_narrow.w.last)))
if (yield dut.axi_wide.b.valid) and (yield dut.axi_wide.b.ready):
b_count[0] += 1
yield
@passive
def b_responder(dut):
narrow = dut.axi_narrow
while True:
if (yield narrow.w.valid) and (yield narrow.w.ready) and (yield narrow.w.last):
yield narrow.b.valid.eq(1)
yield
while not (yield narrow.b.ready):
yield
yield narrow.b.valid.eq(0)
yield
def gen(dut):
wide, narrow = dut.axi_wide, dut.axi_narrow
yield narrow.aw.ready.eq(1)
yield narrow.w.ready.eq(0)
yield wide.b.ready.eq(1)
yield from axi_aw_send(wide, 0x100, size=3)
yield wide.w.valid.eq(1)
yield wide.w.data.eq(0x1111222233334444)
yield wide.w.strb.eq(0xff)
yield wide.w.last.eq(1)
yield
yield wide.aw.valid.eq(1)
yield wide.aw.addr.eq(0x200)
yield wide.aw.len.eq(0)
yield wide.aw.size.eq(2)
yield wide.aw.burst.eq(BURST_INCR)
for _ in range(4):
yield
self.assertEqual((yield wide.aw.ready), 0)
yield narrow.w.ready.eq(1)
while not ((yield wide.w.valid) and (yield wide.w.ready)):
yield
yield wide.w.valid.eq(0)
while not (yield wide.aw.ready):
yield
yield wide.aw.valid.eq(0)
yield from axi_w_send(wide, 0xcafebabe, last=True, strb=0xf)
for _ in range(4):
yield
dut = self._DownConvRawDUT()
run_simulation(dut, [gen(dut), monitor(dut), b_responder(dut)])
self.assertEqual(aw_log, [0x100, 0x200])
self.assertEqual(w_log, [
(0x33334444, 0),
(0x11112222, 1),
(0xcafebabe, 1),
])
self.assertEqual(b_count[0], 2)
def test_axi_down_converter_narrow_interleave_read(self):
ar_log, r_log = [], []
@passive
def monitor(dut):
while True:
if (yield dut.axi_wide.ar.valid) and (yield dut.axi_wide.ar.ready):
ar_log.append((yield dut.axi_wide.ar.addr))
if (yield dut.axi_wide.r.valid) and (yield dut.axi_wide.r.ready):
r_log.append((yield dut.axi_wide.r.data))
yield
def send_r(axi, data, last):
yield axi.r.valid.eq(1)
yield axi.r.data.eq(data)
yield axi.r.last.eq(last)
while not (yield axi.r.ready):
yield
yield
yield axi.r.valid.eq(0)
def gen(dut):
wide, narrow = dut.axi_wide, dut.axi_narrow
yield narrow.ar.ready.eq(1)
yield wide.r.ready.eq(1)
yield from axi_ar_send(wide, 0x100, size=3)
yield from send_r(narrow, 0x11110000, last=0)
yield wide.ar.valid.eq(1)
yield wide.ar.addr.eq(0x204)
yield wide.ar.len.eq(0)
yield wide.ar.size.eq(2)
yield wide.ar.burst.eq(BURST_INCR)
for _ in range(4):
yield
self.assertEqual((yield wide.ar.ready), 0)
yield from send_r(narrow, 0x22220000, last=1)
while not (yield wide.ar.ready):
yield
yield wide.ar.valid.eq(0)
yield from send_r(narrow, 0x33330000, last=1)
for _ in range(4):
yield
dut = self._DownConvRawDUT()
run_simulation(dut, [gen(dut), monitor(dut)])
self.assertEqual(ar_log, [0x100, 0x204])
self.assertEqual(r_log, [
0x2222000011110000,
0x3333000000000000,
])
def test_axi_down_converter_request_limit(self):
accepted = {"aw": 0, "ar": 0}
def gen(dut):
wide, narrow = dut.axi_wide, dut.axi_narrow
yield narrow.aw.ready.eq(1)
yield narrow.ar.ready.eq(1)
yield wide.aw.valid.eq(1)
yield wide.aw.size.eq(3)
yield wide.ar.valid.eq(1)
yield wide.ar.size.eq(3)
yield
for _ in range(300):
accepted["aw"] += (yield wide.aw.ready)
accepted["ar"] += (yield wide.ar.ready)
yield
dut = self._DownConvRawDUT()
run_simulation(dut, gen(dut))
self.assertEqual(accepted, {"aw": 255, "ar": 255})
def test_axi_down_converter_wrap(self):
# WRAP bursts must wrap inside the wide-side wrap window even after data-width reduction:
# the narrow side carries `ratio` narrow beats per wide beat and the wrap boundary
# propagates via convert_size + convert_burst (BURST_WRAP -> BURST_WRAP).
# We verify by writing a wrap burst then reading the same range *single-beat* (without
# the converter wrapping our test reads) to confirm data landed at the expected wrapped
# physical addresses.
dw_wide = 64
dw_narrow = 32
ratio = dw_wide // dw_narrow
wide_size = log2_int(dw_wide // 8)
wide_byts = dw_wide // 8
def gen(dut):
prng = random.Random(0x77a9)
for wrap_len in [2, 4, 8, 16]:
block_byts = wrap_len * wide_byts
base_block = wrap_len * wide_byts * 4 # spaced per length
start_off = (wrap_len // 2) * wide_byts
start_addr = base_block + start_off
beats = [prng.randrange(2**dw_wide) for _ in range(wrap_len)]
resp = yield from axi_write_burst(dut.axi_wide, start_addr, beats,
burst_type=BURST_WRAP, size=wide_size)
if resp != RESP_OKAY:
dut.errors += 1
# Compute expected memory: each wide beat lands at start + i*wide_byts wrapped
# within block_byts.
expected = [None] * wrap_len
for i, beat in enumerate(beats):
off = (start_off + i*wide_byts) % block_byts
expected[off // wide_byts] = beat
# Single-beat read each wide-aligned address in the block via the down-converter
# (size=wide_size, len=0 -> 1 wide beat). This avoids issuing WRAP on the read,
# so we observe the underlying memory contents directly.
for i in range(wrap_len):
addr = base_block + i*wide_byts
data, resp = yield from axi_read_single(dut.axi_wide, addr, size=wide_size)
if resp != RESP_OKAY or data != expected[i]:
dut.errors += 1
dut = self._DownConvDUT(dw_wide=dw_wide, dw_narrow=dw_narrow, mem_bytes=4096)
run_simulation(dut, [gen(dut)])
self.assertEqual(dut.errors, 0)
def test_axi_down_converter_back_to_back(self):
# Stress: 30 random INCR bursts (writes and reads alternated), against a software model.
# Catches FSM-state-leak bugs in the converter or its StrideConverter that a single
# transaction can't expose.
dw_wide = 64
dw_narrow = 32
wide_size = log2_int(dw_wide // 8)
wide_byts = dw_wide // 8
def gen(dut):
prng = random.Random(0xfeed)
mem_byts = 4096
mem_w = mem_byts // wide_byts
model = [0] * mem_w # one entry per wide-beat slot
for _ in range(30):
op_is_write = prng.randrange(2)
length = prng.choice([1, 2, 4, 8])
base_w = prng.randrange(mem_w - length + 1)
addr = base_w * wide_byts
if op_is_write:
beats = [prng.randrange(2**dw_wide) for _ in range(length)]
resp = yield from axi_write_burst(dut.axi_wide, addr, beats, size=wide_size)
if resp != RESP_OKAY:
dut.errors += 1
for i, b in enumerate(beats):
model[base_w + i] = b
else:
read = yield from axi_read_burst(dut.axi_wide, addr, length, size=wide_size)
expect = model[base_w:base_w + length]
if read != expect:
dut.errors += 1
dut = self._DownConvDUT(dw_wide=dw_wide, dw_narrow=dw_narrow, mem_bytes=4096)
run_simulation(dut, [gen(dut)])
self.assertEqual(dut.errors, 0)
def test_axi_down_converter_id_and_last(self):
# Verify that the wide-side r.id matches what was issued on ar.id, and that r.last
# is asserted *only* on the final reassembled wide beat of each burst.
def gen(dut, recorded):
prng = random.Random(0xa11)
yield dut.axi_wide.r.ready.eq(1)
# Pre-write known data so reads return predictable values.
for length in [1, 2, 4, 8]:
addr = length * 64
beats = [prng.randrange(2**64) for _ in range(length)]
resp = yield from axi_write_burst(dut.axi_wide, addr, beats, size=3)
if resp != RESP_OKAY:
dut.errors += 1
# Read with a unique id per burst, manually capturing every r beat.
burst_id = (length * 0x11) & 0xf
yield from axi_ar_send(dut.axi_wide, addr,
burst_len=length-1, burst_type=BURST_INCR, size=3, id=burst_id)
got = []
for _ in range(length):
while (yield dut.axi_wide.r.valid) == 0:
yield
got.append((
(yield dut.axi_wide.r.data),
(yield dut.axi_wide.r.id),
bool((yield dut.axi_wide.r.last)),
))
yield
# last asserts only on the final beat.
for i, (data, rid, last) in enumerate(got):
expected_last = (i == length - 1)
if last != expected_last:
dut.errors += 1
if rid != burst_id:
dut.errors += 1
if data != beats[i]:
dut.errors += 1
recorded.append((length, got))
dut = self._DownConvDUT(dw_wide=64, dw_narrow=32, mem_bytes=4096)
recorded = []
run_simulation(dut, [gen(dut, recorded)])
self.assertEqual(dut.errors, 0)
def test_axi_down_converter_fixed_burst(self):
# FIXED bursts on a wide master target the same address every beat (think: pushing N
# items into a memory-mapped FIFO on the same register). After data-width reduction
# they must still resolve to repeated writes/reads at the same wide-beat-sized window.
#
# AXIDownConverter handles this via a per-FIXED-burst FSM that issues `len + 1`
# independent narrow INCR-`ratio` bursts back-to-back, all at the captured wide
# address (since AXI4 has no native "increment within a wide-beat window then reset"
# burst type). The W stream's `last` is overridden to fire every ratio-th narrow
# beat so each narrow burst terminates correctly, the R stream's `last` is overridden
# to fire only on the final wide beat, and the L narrow B responses are coalesced
# into a single wide B with the worst response winning.
# Sub-test 1: FIXED-1 — equivalent to single-beat INCR, takes the fast path.
def gen_len1(dut):
data = 0xdeadbeefcafebabe
yield from axi_aw_send(dut.axi_wide, 0x40,
burst_len=0, burst_type=BURST_FIXED, size=3)
yield from axi_w_send(dut.axi_wide, data, last=True)
resp = yield from axi_b_recv(dut.axi_wide)
if resp != RESP_OKAY:
dut.errors += 1
data_back, resp = yield from axi_read_single(dut.axi_wide, 0x40, size=3)
if resp != RESP_OKAY or data_back != data:
dut.errors += 1
dut = self._DownConvDUT(dw_wide=64, dw_narrow=32, mem_bytes=4096)
run_simulation(dut, [gen_len1(dut)])
self.assertEqual(dut.errors, 0)
# Sub-test 2: FIXED-2 — two wide beats, both at address X. After conversion, the
# second beat must overwrite the first at X (since both target the same wide
# address), and X+8 must be untouched (initial value).
def gen_len2(dut, results):
beats = [0xaaaaaaaaaaaaaaaa, 0xbbbbbbbbbbbbbbbb]
yield from axi_aw_send(dut.axi_wide, 0x80,
burst_len=1, burst_type=BURST_FIXED, size=3)
for i, beat in enumerate(beats):
yield from axi_w_send(dut.axi_wide, beat, last=(i == 1))
resp = yield from axi_b_recv(dut.axi_wide)
results["b_resp"] = resp
results["at_X"], _ = yield from axi_read_single(dut.axi_wide, 0x80, size=3)
results["at_X8"], _ = yield from axi_read_single(dut.axi_wide, 0x88, size=3)
dut = self._DownConvDUT(dw_wide=64, dw_narrow=32, mem_bytes=4096)
results = {}
run_simulation(dut, [gen_len2(dut, results)])
self.assertEqual(results["b_resp"], RESP_OKAY)
self.assertEqual(results["at_X"], 0xbbbbbbbbbbbbbbbb,
"AXIDownConverter FIXED-2: beat[1] should overwrite beat[0] at X.")
self.assertEqual(results["at_X8"], 0,
"AXIDownConverter FIXED-2: X+8 must be untouched (FIXED stays at X).")
# Sub-test 3: FIXED with read-back via FIXED-N read. The wide master issues a
# FIXED-4 read at X; all 4 wide R beats should return the same value (the last
# value written to X).
def gen_fixed_read(dut, results):
# Pre-write a known value to X.
yield from axi_write_single(dut.axi_wide, 0xc0, 0x1122334455667788, size=3)
# FIXED-4 read.
results["read"] = yield from axi_read_burst(dut.axi_wide, 0xc0, 4,
burst_type=BURST_FIXED, size=3)
dut = self._DownConvDUT(dw_wide=64, dw_narrow=32, mem_bytes=4096)
results = {}
run_simulation(dut, [gen_fixed_read(dut, results)])
self.assertEqual(results["read"], [0x1122334455667788] * 4,
"AXIDownConverter FIXED-4 read: every wide beat must return the value at X.")
# Sub-test 4: longer FIXED — 8 wide beats, alternating values, only beats[7] survives.
def gen_fixed_8(dut, results):
beats = [(i + 1) * 0x1111111111111111 for i in range(8)]
yield from axi_aw_send(dut.axi_wide, 0x100,
burst_len=7, burst_type=BURST_FIXED, size=3)
for i, beat in enumerate(beats):
yield from axi_w_send(dut.axi_wide, beat, last=(i == 7))
resp = yield from axi_b_recv(dut.axi_wide)
results["b_resp"] = resp
results["at_X"], _ = yield from axi_read_single(dut.axi_wide, 0x100, size=3)
results["beats_last"] = beats[-1]
dut = self._DownConvDUT(dw_wide=64, dw_narrow=32, mem_bytes=4096)
results = {}
run_simulation(dut, [gen_fixed_8(dut, results)])
self.assertEqual(results["b_resp"], RESP_OKAY)
self.assertEqual(results["at_X"], results["beats_last"],
"AXIDownConverter FIXED-8: only the last beat must survive at X.")
def test_axi_down_converter_fixed_lengths(self):
# Sweep FIXED-N for N = 1..32, write N distinct values to address X, then read X back
# single-beat. The expected memory contents at X is always the *last* beat written
# (every beat targets the same address, second overwrites first, etc).
# Each iteration uses a fresh address so we can also confirm neighbouring slots are
# untouched.
def gen(dut, results):
for n_beats in [1, 2, 3, 4, 5, 8, 16, 32]:
base = n_beats * 256 # plenty of headroom per length
beats = [(i + 1) * 0x0101010101010101 for i in range(n_beats)]
yield from axi_aw_send(dut.axi_wide, base,
burst_len=n_beats - 1, burst_type=BURST_FIXED, size=3)
for i, beat in enumerate(beats):
yield from axi_w_send(dut.axi_wide, beat, last=(i == n_beats - 1))
resp = yield from axi_b_recv(dut.axi_wide)
# Read back: only the last beat must survive at base; base+8 must be untouched (0).
at_X, _ = yield from axi_read_single(dut.axi_wide, base, size=3)
at_X8, _ = yield from axi_read_single(dut.axi_wide, base + 8, size=3)
results.append((n_beats, resp, at_X, beats[-1], at_X8))
dut = self._DownConvDUT(dw_wide=64, dw_narrow=32, mem_bytes=8192*2)
results = []
run_simulation(dut, [gen(dut, results)])
for n_beats, resp, at_X, want, at_X8 in results:
self.assertEqual(resp, RESP_OKAY,
f"FIXED-{n_beats}: B response should be RESP_OKAY")
self.assertEqual(at_X, want,
f"FIXED-{n_beats}: address X should hold beats[-1]")
self.assertEqual(at_X8, 0,
f"FIXED-{n_beats}: address X+8 must be untouched (got {at_X8:#x})")
def test_axi_down_converter_fixed_ratios(self):
# FIXED-4 across 4 width-pair ratios. Same expected outcome (only last beat survives at
# X) regardless of how many narrow beats we issue per wide beat (ratio 2, 4, or 8).
def gen(dut, dw_wide, results):
base = 0x80
beats = [(i + 1) * 0x1111111111111111 & ((1 << dw_wide) - 1) for i in range(4)]
wide_size = log2_int(dw_wide // 8)
yield from axi_aw_send(dut.axi_wide, base,
burst_len=3, burst_type=BURST_FIXED, size=wide_size)
for i, beat in enumerate(beats):
yield from axi_w_send(dut.axi_wide, beat, last=(i == 3))
resp = yield from axi_b_recv(dut.axi_wide)
at_X, _ = yield from axi_read_single(dut.axi_wide, base, size=wide_size)
results["resp"] = resp
results["at_X"] = at_X
results["beats_last"] = beats[-1]
for dw_wide, dw_narrow in [(64, 32), (128, 32), (128, 64), (256, 32)]:
with self.subTest(dw_wide=dw_wide, dw_narrow=dw_narrow):
dut = self._DownConvDUT(dw_wide=dw_wide, dw_narrow=dw_narrow, mem_bytes=4096)
results = {}
run_simulation(dut, [gen(dut, dw_wide, results)])
self.assertEqual(results["resp"], RESP_OKAY)
self.assertEqual(results["at_X"], results["beats_last"],
f"FIXED-4 at {dw_wide}->{dw_narrow}: X should hold beats[-1]")
def test_axi_down_converter_mixed_bursts(self):
# Alternate FIXED and INCR transactions to verify the FSM cleanly transitions back to
# the combinational fast path after every FIXED, and vice versa. The address space is
# split into independent slots (one per transaction) so we can validate each one
# independently afterwards via single-beat reads.
def gen(dut, log):
# Layout: 4 slots of 32 bytes each = 128 bytes total starting at 0x200.
slots = [0x200 + i*32 for i in range(8)]
# Transaction script: (op, slot_idx, burst_type, length).
script = [
("write_burst", 0, BURST_INCR, 4), # INCR-4 at slot 0
("write_fixed", 1, BURST_FIXED, 4), # FIXED-4 at slot 1
("write_burst", 2, BURST_INCR, 2), # INCR-2 at slot 2
("write_fixed", 3, BURST_FIXED, 8), # FIXED-8 at slot 3
("write_burst", 4, BURST_INCR, 1), # INCR-1 at slot 4
("write_fixed", 5, BURST_FIXED, 2), # FIXED-2 at slot 5
("write_fixed", 6, BURST_FIXED, 1), # FIXED-1 at slot 6 (fast path)
("write_burst", 7, BURST_INCR, 4), # INCR-4 at slot 7 (sanity after FIXED-1)
]
for kind, idx, btype, length in script:
base = slots[idx]
beats = [(idx + 1) * 0x0123456789abcdef + i for i in range(length)]
if kind == "write_burst":
resp = yield from axi_write_burst(dut.axi_wide, base, beats,
burst_type=btype, size=3)
log.append((kind, idx, btype, length, resp, list(beats)))
else:
yield from axi_aw_send(dut.axi_wide, base,
burst_len=length - 1, burst_type=btype, size=3)
for i, beat in enumerate(beats):
yield from axi_w_send(dut.axi_wide, beat, last=(i == length - 1))
resp = yield from axi_b_recv(dut.axi_wide)
log.append((kind, idx, btype, length, resp, list(beats)))
slot_bases = [0x200 + i*32 for i in range(8)]
def gen_full(dut, log, results):
yield from gen(dut, log)
for base in slot_bases:
cells = []
for off in [0, 8, 16, 24]:
data, _ = yield from axi_read_single(dut.axi_wide, base + off, size=3)
cells.append(data)
results.append(cells)
dut = self._DownConvDUT(dw_wide=64, dw_narrow=32, mem_bytes=8192)
log = []
results = []
run_simulation(dut, [gen_full(dut, log, results)])
# Slot 0: INCR-4 → all 4 cells distinct.
for i, b in enumerate(log[0][5]):
self.assertEqual(results[0][i], b, "slot 0 INCR-4 cell mismatch")
# Slot 1: FIXED-4 → only last beat at +0; +8/+16/+24 untouched (0).
self.assertEqual(results[1][0], log[1][5][-1], "slot 1 FIXED-4 base")
self.assertEqual(results[1][1], 0, "slot 1 FIXED-4 +8 must be 0")
self.assertEqual(results[1][2], 0, "slot 1 FIXED-4 +16 must be 0")
self.assertEqual(results[1][3], 0, "slot 1 FIXED-4 +24 must be 0")
# Slot 2: INCR-2 → 2 distinct cells, +16/+24 untouched.
for i, b in enumerate(log[2][5]):
self.assertEqual(results[2][i], b, "slot 2 INCR-2 cell mismatch")
self.assertEqual(results[2][2], 0, "slot 2 +16 must be 0")
self.assertEqual(results[2][3], 0, "slot 2 +24 must be 0")
# Slot 3: FIXED-8 → only last beat at +0; rest 0.
self.assertEqual(results[3][0], log[3][5][-1], "slot 3 FIXED-8 base")
for i in range(1, 4):
self.assertEqual(results[3][i], 0, f"slot 3 FIXED-8 +{i*8} must be 0")
# Slot 4: INCR-1 → only +0 written, rest 0.
self.assertEqual(results[4][0], log[4][5][0], "slot 4 INCR-1 base")
for i in range(1, 4):
self.assertEqual(results[4][i], 0, f"slot 4 INCR-1 +{i*8} must be 0")
# Slot 5: FIXED-2 → only beats[1] at +0, rest 0.
self.assertEqual(results[5][0], log[5][5][-1], "slot 5 FIXED-2 base")
for i in range(1, 4):
self.assertEqual(results[5][i], 0, f"slot 5 FIXED-2 +{i*8} must be 0")
# Slot 6: FIXED-1 fast path → single beat at +0.
self.assertEqual(results[6][0], log[6][5][0], "slot 6 FIXED-1 base")
for i in range(1, 4):
self.assertEqual(results[6][i], 0, f"slot 6 FIXED-1 +{i*8} must be 0")
# Slot 7: INCR-4 after FIXED — verify FSM cleanly returned to fast path.
for i, b in enumerate(log[7][5]):
self.assertEqual(results[7][i], b, "slot 7 INCR-4 (after FIXED) cell mismatch")
# AXISRAM helper: builds a DUT with a 32-bit AXI bus and runs the supplied generator.
def _axisram_run(self, generator, size=64*4, init=None, data_width=32):
class DUT(LiteXModule):
def __init__(self, size, init):
self.axi = AXIInterface(data_width=data_width, address_width=32, id_width=4)
self.sram = AXISRAM(size, init=init, bus=self.axi)
self.errors = 0
prng = random.Random(0)
if init is None:
init = [prng.randrange(2**data_width) for _ in range(size//(data_width//8))]
dut = DUT(size=size, init=init)
run_simulation(dut, [generator(dut, init)])
return dut
def test_axi_sram(self):
# Smoke test: single-beat init readback, single-beat writes, then one INCR-16 burst.
def gen(dut, ref_init):
prng = random.Random(42)
# Read each init cell back single-beat.
for word_addr, ref in enumerate(ref_init):
data, resp = yield from axi_read_single(dut.axi, word_addr * 4)
if resp != RESP_OKAY or data != ref:
dut.errors += 1
# Single-beat overwrite, single-beat readback.
new_values = [prng.randrange(2**32) for _ in ref_init]
for word_addr, value in enumerate(new_values):
resp = yield from axi_write_single(dut.axi, word_addr * 4, value)
if resp != RESP_OKAY:
dut.errors += 1
for word_addr, value in enumerate(new_values):
data, resp = yield from axi_read_single(dut.axi, word_addr * 4)
if resp != RESP_OKAY or data != value:
dut.errors += 1
# 16-beat INCR burst at a clean address.
base = 0x80
beats = [prng.randrange(2**32) for _ in range(16)]
resp = yield from axi_write_burst(dut.axi, base, beats)
if resp != RESP_OKAY:
dut.errors += 1
datas = yield from axi_read_burst(dut.axi, base, 16)
if datas != beats:
dut.errors += 1
dut = self._axisram_run(gen, size=64*4)
self.assertEqual(dut.errors, 0)
def test_axi_sram_burst_lengths(self):
# Sweep INCR burst length across all supported sizes. AXI4 allows len=0..255 (1..256 beats);
# cache controllers and DMAs typically use lengths matching their cache-line / payload size,
# so the interesting points are 1, 2, 4, 8, 16, 32, 64, 128, 256.
lengths = [1, 2, 4, 8, 16, 32, 64, 128, 256]
size_words = max(lengths) * 2 # leave headroom past the longest burst
def gen(dut, _ref_init):
prng = random.Random(123)
base = 0
for n in lengths:
beats = [prng.randrange(2**32) for _ in range(n)]
resp = yield from axi_write_burst(dut.axi, base, beats)
if resp != RESP_OKAY:
dut.errors += 1
read = yield from axi_read_burst(dut.axi, base, n)
if read != beats:
dut.errors += 1
base += n * 4
dut = self._axisram_run(gen, size=size_words * 4)
self.assertEqual(dut.errors, 0)
def test_axi_sram_wrap_burst(self):
# WRAP bursts are how cache-line refills are typically issued (AXI4-allowed lengths: 2/4/8/16,
# start address aligned to size). After issuing a WRAP-N burst at addr X, the data must land
# at the wrapped physical addresses. We verify by writing in WRAP order then reading back
# single-beat from each unwrapped address.
def gen(dut, _ref_init):
prng = random.Random(7)
for wrap_len in [2, 4, 8, 16]:
# 4 bytes per beat, total burst block = wrap_len * 4 bytes.
block_bytes = wrap_len * 4
# Pick a start that's not at the bottom of the wrap block to actually exercise wrap.
base_block = wrap_len * 4 * 4 # well-spaced per length
start_off = (wrap_len // 2) * 4
start_addr = base_block + start_off
beats = [prng.randrange(2**32) for _ in range(wrap_len)]
resp = yield from axi_write_burst(dut.axi, start_addr, beats, burst_type=BURST_WRAP)
if resp != RESP_OKAY:
dut.errors += 1
# Compute expected memory contents: WRAP start + i, modulo block_bytes within the block.
expected = [None] * wrap_len
for i, beat in enumerate(beats):
off_within_block = (start_off + i*4) % block_bytes
expected[off_within_block // 4] = beat
# Verify by single-beat reads from each block address.
for i, want in enumerate(expected):
addr = base_block + i*4
data, resp = yield from axi_read_single(dut.axi, addr)
if resp != RESP_OKAY or data != want:
dut.errors += 1
# Also verify a WRAP read returns the same beats we wrote, in WRAP order.
read = yield from axi_read_burst(dut.axi, start_addr, wrap_len, burst_type=BURST_WRAP)
if read != beats:
dut.errors += 1
dut = self._axisram_run(gen, size=4096)
self.assertEqual(dut.errors, 0)
def test_axi_sram_back_to_back(self):
# Run many alternating writes and reads of varied burst types and lengths to flush out FSM
# bugs in the AXI2AXILite bridge (state not reset between bursts, AW/AR contention, etc.).
def gen(dut, _ref_init):
prng = random.Random(31337)
mem_size = 1024 # bytes
# Software model of the SRAM that we keep in sync with each write so reads can be checked.
model = [0] * (mem_size // 4)
for _ in range(40):
op_is_write = prng.randrange(2)
length = prng.choice([1, 2, 4, 8, 16])
base_word = prng.randrange((mem_size // 4) - length + 1)
base_addr = base_word * 4
if op_is_write:
beats = [prng.randrange(2**32) for _ in range(length)]
resp = yield from axi_write_burst(dut.axi, base_addr, beats)
if resp != RESP_OKAY:
dut.errors += 1
for i, beat in enumerate(beats):
model[base_word + i] = beat
else:
read = yield from axi_read_burst(dut.axi, base_addr, length)
expect = model[base_word:base_word + length]
if read != expect:
dut.errors += 1
# Pre-fill the SRAM with zeros so the model matches.
dut = self._axisram_run(gen, size=1024, init=[0] * 256)
self.assertEqual(dut.errors, 0)
def test_axi_sram_64bit(self):
# Same shape as the basic test but on a 64-bit AXI bus; exercises wider data, wider strobe,
# and the AXILiteSRAM internal port being instantiated at width 64.
def gen(dut, ref_init):
prng = random.Random(8)
# Single-beat readback of init.
for word_addr, ref in enumerate(ref_init):
data, resp = yield from axi_read_single(dut.axi, word_addr * 8, size=3)
if resp != RESP_OKAY or data != ref:
dut.errors += 1
# 8-beat INCR burst at a clean address.
base = 0x80
beats = [prng.randrange(2**64) for _ in range(8)]
resp = yield from axi_write_burst(dut.axi, base, beats, size=3)
if resp != RESP_OKAY:
dut.errors += 1
read = yield from axi_read_burst(dut.axi, base, 8, size=3)
if read != beats:
dut.errors += 1
dut = self._axisram_run(gen, size=64 * 8, data_width=64)
self.assertEqual(dut.errors, 0)