diff --git a/axi/tb/axi_dma_wrapper/Makefile b/axi/tb/axi_dma_wrapper/Makefile new file mode 100644 index 0000000..9958202 --- /dev/null +++ b/axi/tb/axi_dma_wrapper/Makefile @@ -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 diff --git a/axi/tb/axi_dma_wrapper/test_axi_dma_wrapper.py b/axi/tb/axi_dma_wrapper/test_axi_dma_wrapper.py new file mode 100644 index 0000000..1a260d7 --- /dev/null +++ b/axi/tb/axi_dma_wrapper/test_axi_dma_wrapper.py @@ -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 [], + )