tests: add dma test

This commit is contained in:
Phil
2026-06-09 18:00:12 +03:00
parent f95f6b5b86
commit 35ed5d6e6e
2 changed files with 318 additions and 0 deletions

View File

@ -0,0 +1,81 @@
TOPLEVEL_LANG = verilog
SIM ?= verilator
PWD := $(shell pwd)
PROJECT_ROOT ?= $(abspath $(PWD)/../../..)
AXI_IF_RTL_DIR ?= $(PROJECT_ROOT)/axi/rtl
FORENCICH_AXI_RTL_DIR ?= $(PROJECT_ROOT)/external/verilog-axi/rtl
TB_DIR ?= $(PWD)
TOPLEVEL = tb_axi_dma_wrapper
MODULE = test_axi_dma_wrapper
export PYTHONPATH := $(TB_DIR):$(PYTHONPATH)
# Parameters for a quick make-based run. The pytest entrypoint can be used for
# wider parameter sweeps.
AXI_DATA_WIDTH ?= 32
AXI_ADDR_WIDTH ?= 16
AXI_ID_WIDTH ?= 8
AXI_USER_WIDTH ?= 1
AXI_MAX_BURST_LEN ?= 16
ENABLE_UNALIGNED ?= 0
AXI_STRB_WIDTH := $(shell expr $(AXI_DATA_WIDTH) / 8)
AXIS_DATA_WIDTH ?= $(AXI_DATA_WIDTH)
AXIS_KEEP_WIDTH := $(shell expr $(AXIS_DATA_WIDTH) / 8)
AXIS_KEEP_ENABLE := $(shell [ $(AXIS_DATA_WIDTH) -gt 8 ] && echo 1 || echo 0)
VERILOG_SOURCES += $(AXI_IF_RTL_DIR)/axi_pkg.sv
VERILOG_SOURCES += $(AXI_IF_RTL_DIR)/axi_if.sv
VERILOG_SOURCES += $(AXI_IF_RTL_DIR)/axis_if.sv
VERILOG_SOURCES += $(AXI_IF_RTL_DIR)/axi4_flat_to_if.sv
VERILOG_SOURCES += $(AXI_IF_RTL_DIR)/axi4_if_to_flat.sv
VERILOG_SOURCES += $(AXI_IF_RTL_DIR)/axis_flat_to_if.sv
VERILOG_SOURCES += $(AXI_IF_RTL_DIR)/axis_if_to_flat.sv
VERILOG_SOURCES += $(FORENCICH_AXI_RTL_DIR)/axi_dma.v
VERILOG_SOURCES += $(FORENCICH_AXI_RTL_DIR)/axi_dma_rd.v
VERILOG_SOURCES += $(FORENCICH_AXI_RTL_DIR)/axi_dma_wr.v
VERILOG_SOURCES += $(AXI_IF_RTL_DIR)/forencich_axi_dma_wrapper.sv
VERILOG_SOURCES += $(TB_DIR)/tb_axi_dma_wrapper.sv
COMPILE_ARGS += -I$(AXI_IF_RTL_DIR)
COMPILE_ARGS += -I$(WRAPPER_RTL_DIR)
COMPILE_ARGS += -I$(FORENCICH_AXI_RTL_DIR)
# took this from forencich to silence 100+ warnings
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH -Wno-CASEINCOMPLETE
ifeq ($(SIM),verilator)
EXTRA_ARGS += --trace
EXTRA_ARGS += --trace-structs
EXTRA_ARGS += -GAXI_DATA_WIDTH=$(AXI_DATA_WIDTH)
EXTRA_ARGS += -GAXI_ADDR_WIDTH=$(AXI_ADDR_WIDTH)
EXTRA_ARGS += -GAXI_STRB_WIDTH=$(AXI_STRB_WIDTH)
EXTRA_ARGS += -GAXI_ID_WIDTH=$(AXI_ID_WIDTH)
EXTRA_ARGS += -GAXI_USER_WIDTH=$(AXI_USER_WIDTH)
EXTRA_ARGS += -GAXI_MAX_BURST_LEN=$(AXI_MAX_BURST_LEN)
EXTRA_ARGS += -GAXIS_DATA_WIDTH=$(AXIS_DATA_WIDTH)
EXTRA_ARGS += -GAXIS_KEEP_ENABLE=$(AXIS_KEEP_ENABLE)
EXTRA_ARGS += -GAXIS_KEEP_WIDTH=$(AXIS_KEEP_WIDTH)
EXTRA_ARGS += -GAXIS_LAST_ENABLE=1
EXTRA_ARGS += -GAXIS_ID_ENABLE=1
EXTRA_ARGS += -GAXIS_ID_WIDTH=8
EXTRA_ARGS += -GAXIS_DEST_ENABLE=0
EXTRA_ARGS += -GAXIS_DEST_WIDTH=8
EXTRA_ARGS += -GAXIS_USER_ENABLE=1
EXTRA_ARGS += -GAXIS_USER_WIDTH=1
EXTRA_ARGS += -GLEN_WIDTH=20
EXTRA_ARGS += -GTAG_WIDTH=8
EXTRA_ARGS += -GENABLE_SG=0
EXTRA_ARGS += -GENABLE_UNALIGNED=$(ENABLE_UNALIGNED)
endif
export PARAM_AXI_DATA_WIDTH=$(AXI_DATA_WIDTH)
export PARAM_ENABLE_UNALIGNED=$(ENABLE_UNALIGNED)
include $(shell cocotb-config --makefiles)/Makefile.sim

View File

@ -0,0 +1,237 @@
# SPDX-License-Identifier: MIT
"""
Small cocotb/pytest test for tb_axi_dma_wrapper.
It intentionally keeps the same flat prefixes that alexforencich/verilog-axi
uses for axi_dma, while the SystemVerilog top routes the traffic through
local axi4_if/axis_if adapters and forencich_axi_dma_wrapper.
"""
import os
import logging
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge
from cocotbext.axi import AxiBus, AxiRam
from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSource, AxiStreamSink
from cocotbext.axi.stream import define_stream
DescBus, DescTransaction, DescSource, DescSink, DescMonitor = define_stream(
"Desc",
signals=["addr", "len", "tag", "valid", "ready"],
optional_signals=["id", "dest", "user"],
)
DescStatusBus, DescStatusTransaction, DescStatusSource, DescStatusSink, DescStatusMonitor = define_stream(
"DescStatus",
signals=["tag", "error", "valid"],
optional_signals=["len", "id", "dest", "user"],
)
class TB:
def __init__(self, dut):
self.dut = dut
self.log = logging.getLogger("cocotb.tb")
self.log.setLevel(logging.DEBUG)
cocotb.start_soon(Clock(dut.clk, 10, units="ns").start())
# Descriptor and status streams are flat, exactly like Forencich tests.
self.read_desc_source = DescSource(
DescBus.from_prefix(dut, "s_axis_read_desc"), dut.clk, dut.rst
)
self.read_desc_status_sink = DescStatusSink(
DescStatusBus.from_prefix(dut, "m_axis_read_desc_status"), dut.clk, dut.rst
)
self.write_desc_source = DescSource(
DescBus.from_prefix(dut, "s_axis_write_desc"), dut.clk, dut.rst
)
self.write_desc_status_sink = DescStatusSink(
DescStatusBus.from_prefix(dut, "m_axis_write_desc_status"), dut.clk, dut.rst
)
# Data streams are also flat from cocotb point of view. The SV top
# converts them to axis_if before reaching the wrapper.
self.read_data_sink = AxiStreamSink(
AxiStreamBus.from_prefix(dut, "m_axis_read_data"), dut.clk, dut.rst
)
self.write_data_source = AxiStreamSource(
AxiStreamBus.from_prefix(dut, "s_axis_write_data"), dut.clk, dut.rst
)
# AXI memory model. The SV top converts m_axi flat signals to axi4_if
# and back, so this also tests the AXI converters.
self.axi_ram = AxiRam(AxiBus.from_prefix(dut, "m_axi"), dut.clk, dut.rst, size=2**16)
dut.read_enable.setimmediatevalue(0)
dut.write_enable.setimmediatevalue(0)
dut.write_abort.setimmediatevalue(0)
async def reset(self):
self.dut.rst.setimmediatevalue(0)
await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk)
self.dut.rst.value = 1
await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk)
self.dut.rst.value = 0
await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk)
def _make_data(length, seed=0):
return bytearray(((x + seed) & 0xFF) for x in range(length))
@cocotb.test()
async def test_axi_dma_wrapper_write(dut):
tb = TB(dut)
await tb.reset()
tb.dut.write_enable.value = 1
addr = 0x1000
data = _make_data(96, seed=0x10)
tag = 3
# Guard bytes make it easier to catch wrong offsets/strobes.
tb.axi_ram.write(addr - 16, b"\xaa" * (len(data) + 32))
await tb.write_desc_source.send(DescTransaction(addr=addr, len=len(data), tag=tag))
await tb.write_data_source.send(AxiStreamFrame(data, tid=tag))
status = await tb.write_desc_status_sink.recv()
tb.log.info("write status: %s", status)
assert int(status.error) == 0
assert int(status.tag) == tag
assert int(status.len) == len(data)
assert tb.axi_ram.read(addr - 8, len(data) + 16) == b"\xaa" * 8 + data + b"\xaa" * 8
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
@cocotb.test()
async def test_axi_dma_wrapper_read(dut):
tb = TB(dut)
await tb.reset()
tb.dut.read_enable.value = 1
addr = 0x1800
data = _make_data(113, seed=0x40)
tag = 5
stream_id = 7
tb.axi_ram.write(addr - 16, b"\xcc" * (len(data) + 32))
tb.axi_ram.write(addr, data)
await tb.read_desc_source.send(
DescTransaction(addr=addr, len=len(data), tag=tag, id=stream_id)
)
status = await tb.read_desc_status_sink.recv()
frame = await tb.read_data_sink.recv()
tb.log.info("read status: %s", status)
tb.log.info("read frame: %s", frame)
assert int(status.error) == 0
assert int(status.tag) == tag
assert frame.tdata == data
assert int(frame.tid) == stream_id
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
# -----------------------------------------------------------------------------
# Optional pytest entrypoint via cocotb-test.
# Run from this directory with: pytest -q test_axi_dma_wrapper.py
# -----------------------------------------------------------------------------
def test_axi_dma_wrapper_pytest(request):
import cocotb_test.simulator
tests_dir = os.path.abspath(os.path.dirname(__file__))
project_root = os.path.abspath(os.path.join(tests_dir, "..", ".."))
axi_if_rtl_dir = os.environ.get(
"AXI_IF_RTL_DIR", os.path.join(project_root, "rtl", "axi")
)
wrapper_rtl_dir = os.environ.get(
"WRAPPER_RTL_DIR", os.path.join(project_root, "rtl", "wrappers")
)
forencich_rtl_dir = os.environ.get(
"FORENCICH_AXI_RTL_DIR",
os.path.join(project_root, "external", "verilog-axi", "rtl"),
)
dut = "tb_axi_dma_wrapper"
module = os.path.splitext(os.path.basename(__file__))[0]
toplevel = dut
parameters = {
"AXI_DATA_WIDTH": int(os.getenv("PARAM_AXI_DATA_WIDTH", "32")),
"AXI_ADDR_WIDTH": 16,
"AXI_ID_WIDTH": 8,
"AXI_USER_WIDTH": 1,
"AXI_MAX_BURST_LEN": 16,
"AXIS_ID_ENABLE": 1,
"AXIS_ID_WIDTH": 8,
"AXIS_DEST_ENABLE": 0,
"AXIS_DEST_WIDTH": 8,
"AXIS_USER_ENABLE": 1,
"AXIS_USER_WIDTH": 1,
"LEN_WIDTH": 20,
"TAG_WIDTH": 8,
"ENABLE_SG": 0,
"ENABLE_UNALIGNED": int(os.getenv("PARAM_ENABLE_UNALIGNED", "0")),
}
parameters["AXI_STRB_WIDTH"] = parameters["AXI_DATA_WIDTH"] // 8
parameters["AXIS_DATA_WIDTH"] = parameters["AXI_DATA_WIDTH"]
parameters["AXIS_KEEP_ENABLE"] = int(parameters["AXIS_DATA_WIDTH"] > 8)
parameters["AXIS_KEEP_WIDTH"] = parameters["AXIS_DATA_WIDTH"] // 8
parameters["AXIS_LAST_ENABLE"] = 1
verilog_sources = [
os.path.join(axi_if_rtl_dir, "axi_pkg.sv"),
os.path.join(axi_if_rtl_dir, "axi_if.sv"),
os.path.join(axi_if_rtl_dir, "axis_if.sv"),
os.path.join(axi_if_rtl_dir, "axi4_flat_to_if.sv"),
os.path.join(axi_if_rtl_dir, "axi4_if_to_flat.sv"),
os.path.join(axi_if_rtl_dir, "axis_flat_to_if.sv"),
os.path.join(axi_if_rtl_dir, "axis_if_to_flat.sv"),
os.path.join(forencich_rtl_dir, "axi_dma.v"),
os.path.join(forencich_rtl_dir, "axi_dma_rd.v"),
os.path.join(forencich_rtl_dir, "axi_dma_wr.v"),
os.path.join(wrapper_rtl_dir, "forencich_axi_dma_wrapper.sv"),
os.path.join(tests_dir, "tb_axi_dma_wrapper.sv"),
]
extra_env = {f"PARAM_{k}": str(v) for k, v in parameters.items()}
sim_build = os.path.join(
tests_dir,
"sim_build",
request.node.name.replace("[", "-").replace("]", ""),
)
cocotb_test.simulator.run(
python_search=[tests_dir],
verilog_sources=verilog_sources,
includes=[axi_if_rtl_dir, wrapper_rtl_dir, forencich_rtl_dir],
toplevel=toplevel,
module=module,
parameters=parameters,
sim_build=sim_build,
extra_env=extra_env,
waves=True,
extra_args=["--trace-structs"] if os.getenv("SIM", "verilator") == "verilator" else [],
)