tests: use forencich test for DMA wrapper
This commit is contained in:
@ -1,18 +1,47 @@
|
|||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
"""
|
"""
|
||||||
Small cocotb/pytest test for tb_axi_dma_wrapper.
|
Adapted cocotb/pytest tests for tb_axi_dma_wrapper.
|
||||||
|
|
||||||
It intentionally keeps the same flat prefixes that alexforencich/verilog-axi
|
This file is based on alexforencich/verilog-axi/tb/axi_dma/test_axi_dma.py
|
||||||
uses for axi_dma, while the SystemVerilog top routes the traffic through
|
and keeps the same cocotb-facing flat prefixes. The SystemVerilog test top
|
||||||
local axi4_if/axis_if adapters and forencich_axi_dma_wrapper.
|
routes these flat signals through local axi4_if/axis_if converters and then
|
||||||
|
through axi_dma_if_wrapper.
|
||||||
|
|
||||||
|
Original copyright:
|
||||||
|
Copyright (c) 2020 Alex Forencich
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
import cocotb
|
import cocotb
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pytest
|
||||||
|
except ImportError: # pytest is only needed for the optional cocotb-test entrypoint
|
||||||
|
pytest = None
|
||||||
from cocotb.clock import Clock
|
from cocotb.clock import Clock
|
||||||
from cocotb.triggers import RisingEdge
|
from cocotb.triggers import RisingEdge
|
||||||
|
from cocotb.regression import TestFactory
|
||||||
|
|
||||||
from cocotbext.axi import AxiBus, AxiRam
|
from cocotbext.axi import AxiBus, AxiRam
|
||||||
from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSource, AxiStreamSink
|
from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSource, AxiStreamSink
|
||||||
@ -40,39 +69,57 @@ class TB:
|
|||||||
|
|
||||||
cocotb.start_soon(Clock(dut.clk, 10, units="ns").start())
|
cocotb.start_soon(Clock(dut.clk, 10, units="ns").start())
|
||||||
|
|
||||||
# Descriptor and status streams are flat, exactly like Forencich tests.
|
# Descriptor/status streams remain flat on purpose: they are specific
|
||||||
|
# to Forencich DMA, not generic AXIS interfaces in our library.
|
||||||
self.read_desc_source = DescSource(
|
self.read_desc_source = DescSource(
|
||||||
DescBus.from_prefix(dut, "s_axis_read_desc"), dut.clk, dut.rst
|
DescBus.from_prefix(dut, "s_axis_read_desc"), dut.clk, dut.rst
|
||||||
)
|
)
|
||||||
self.read_desc_status_sink = DescStatusSink(
|
self.read_desc_status_sink = DescStatusSink(
|
||||||
DescStatusBus.from_prefix(dut, "m_axis_read_desc_status"), dut.clk, dut.rst
|
DescStatusBus.from_prefix(
|
||||||
|
dut, "m_axis_read_desc_status"), dut.clk, dut.rst
|
||||||
)
|
)
|
||||||
|
|
||||||
self.write_desc_source = DescSource(
|
self.write_desc_source = DescSource(
|
||||||
DescBus.from_prefix(dut, "s_axis_write_desc"), dut.clk, dut.rst
|
DescBus.from_prefix(dut, "s_axis_write_desc"), dut.clk, dut.rst
|
||||||
)
|
)
|
||||||
self.write_desc_status_sink = DescStatusSink(
|
self.write_desc_status_sink = DescStatusSink(
|
||||||
DescStatusBus.from_prefix(dut, "m_axis_write_desc_status"), dut.clk, dut.rst
|
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
|
# Data streams are flat from cocotb's point of view, but the SV top
|
||||||
# converts them to axis_if before reaching the wrapper.
|
# sends them through axis_flat_to_if/axis_if_to_flat before/after DUT.
|
||||||
self.read_data_sink = AxiStreamSink(
|
self.read_data_sink = AxiStreamSink(
|
||||||
AxiStreamBus.from_prefix(dut, "m_axis_read_data"), dut.clk, dut.rst
|
AxiStreamBus.from_prefix(dut, "m_axis_read_data"), dut.clk, dut.rst
|
||||||
)
|
)
|
||||||
self.write_data_source = AxiStreamSource(
|
self.write_data_source = AxiStreamSource(
|
||||||
AxiStreamBus.from_prefix(dut, "s_axis_write_data"), dut.clk, dut.rst
|
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
|
# AXI memory model. The SV top routes this through the axi4_if adapters.
|
||||||
# and back, so this also tests the AXI converters.
|
self.axi_ram = AxiRam(AxiBus.from_prefix(
|
||||||
self.axi_ram = AxiRam(AxiBus.from_prefix(dut, "m_axi"), dut.clk, dut.rst, size=2**16)
|
dut, "m_axi"), dut.clk, dut.rst, size=2**16)
|
||||||
|
|
||||||
dut.read_enable.setimmediatevalue(0)
|
dut.read_enable.setimmediatevalue(0)
|
||||||
dut.write_enable.setimmediatevalue(0)
|
dut.write_enable.setimmediatevalue(0)
|
||||||
dut.write_abort.setimmediatevalue(0)
|
dut.write_abort.setimmediatevalue(0)
|
||||||
|
|
||||||
async def reset(self):
|
def set_idle_generator(self, generator=None):
|
||||||
|
if generator:
|
||||||
|
self.write_desc_source.set_pause_generator(generator())
|
||||||
|
self.write_data_source.set_pause_generator(generator())
|
||||||
|
self.read_desc_source.set_pause_generator(generator())
|
||||||
|
self.axi_ram.write_if.b_channel.set_pause_generator(generator())
|
||||||
|
self.axi_ram.read_if.r_channel.set_pause_generator(generator())
|
||||||
|
|
||||||
|
def set_backpressure_generator(self, generator=None):
|
||||||
|
if generator:
|
||||||
|
self.read_data_sink.set_pause_generator(generator())
|
||||||
|
self.axi_ram.write_if.aw_channel.set_pause_generator(generator())
|
||||||
|
self.axi_ram.write_if.w_channel.set_pause_generator(generator())
|
||||||
|
self.axi_ram.read_if.ar_channel.set_pause_generator(generator())
|
||||||
|
|
||||||
|
async def cycle_reset(self):
|
||||||
self.dut.rst.setimmediatevalue(0)
|
self.dut.rst.setimmediatevalue(0)
|
||||||
await RisingEdge(self.dut.clk)
|
await RisingEdge(self.dut.clk)
|
||||||
await RisingEdge(self.dut.clk)
|
await RisingEdge(self.dut.clk)
|
||||||
@ -84,79 +131,169 @@ class TB:
|
|||||||
await RisingEdge(self.dut.clk)
|
await RisingEdge(self.dut.clk)
|
||||||
|
|
||||||
|
|
||||||
def _make_data(length, seed=0):
|
async def run_test_write(dut, data_in=None, idle_inserter=None, backpressure_inserter=None):
|
||||||
return bytearray(((x + seed) & 0xFF) for x in range(length))
|
"""DMA write path stress test adapted from Forencich's axi_dma test."""
|
||||||
|
|
||||||
|
|
||||||
@cocotb.test()
|
|
||||||
async def test_axi_dma_wrapper_write(dut):
|
|
||||||
tb = TB(dut)
|
tb = TB(dut)
|
||||||
await tb.reset()
|
|
||||||
|
|
||||||
tb.dut.write_enable.value = 1
|
byte_lanes = tb.axi_ram.write_if.byte_lanes
|
||||||
|
step_size = 1 if int(
|
||||||
|
os.getenv("PARAM_ENABLE_UNALIGNED", "0")) else byte_lanes
|
||||||
|
tag_count = 2 ** len(tb.write_desc_source.bus.tag)
|
||||||
|
cur_tag = 1
|
||||||
|
|
||||||
addr = 0x1000
|
await tb.cycle_reset()
|
||||||
data = _make_data(96, seed=0x10)
|
|
||||||
tag = 3
|
|
||||||
|
|
||||||
# Guard bytes make it easier to catch wrong offsets/strobes.
|
tb.set_idle_generator(idle_inserter)
|
||||||
tb.axi_ram.write(addr - 16, b"\xaa" * (len(data) + 32))
|
tb.set_backpressure_generator(backpressure_inserter)
|
||||||
|
|
||||||
await tb.write_desc_source.send(DescTransaction(addr=addr, len=len(data), tag=tag))
|
dut.write_enable.value = 1
|
||||||
await tb.write_data_source.send(AxiStreamFrame(data, tid=tag))
|
|
||||||
|
for length in list(range(1, byte_lanes * 4 + 1)) + [128]:
|
||||||
|
offsets = list(range(0, byte_lanes * 2, step_size))
|
||||||
|
offsets += list(range(4096 - byte_lanes * 2, 4096, step_size))
|
||||||
|
|
||||||
|
for offset in offsets:
|
||||||
|
for diff in [-8, -2, -1, 0, 1, 2, 8]:
|
||||||
|
if length + diff < 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tb.log.info("write: length=%d offset=%d diff=%d",
|
||||||
|
length, offset, diff)
|
||||||
|
|
||||||
|
addr = offset + 0x1000
|
||||||
|
expected_data = bytearray([x % 256 for x in range(length)])
|
||||||
|
stream_data = bytearray(
|
||||||
|
[x % 256 for x in range(length + diff)])
|
||||||
|
|
||||||
|
tb.axi_ram.write(addr - 128, b"\xaa" *
|
||||||
|
(len(expected_data) + 256))
|
||||||
|
|
||||||
|
await tb.write_desc_source.send(
|
||||||
|
DescTransaction(addr=addr, len=len(
|
||||||
|
expected_data), tag=cur_tag)
|
||||||
|
)
|
||||||
|
await tb.write_data_source.send(AxiStreamFrame(stream_data, tid=cur_tag))
|
||||||
|
|
||||||
status = await tb.write_desc_status_sink.recv()
|
status = await tb.write_desc_status_sink.recv()
|
||||||
tb.log.info("write status: %s", status)
|
tb.log.info("write status: %s", status)
|
||||||
|
|
||||||
|
transferred_len = min(len(expected_data), len(stream_data))
|
||||||
|
|
||||||
|
assert int(status.len) == transferred_len
|
||||||
|
assert int(status.tag) == cur_tag
|
||||||
|
assert int(status.id) == cur_tag
|
||||||
assert int(status.error) == 0
|
assert int(status.error) == 0
|
||||||
assert int(status.tag) == tag
|
|
||||||
assert int(status.len) == len(data)
|
tb.log.debug(
|
||||||
assert tb.axi_ram.read(addr - 8, len(data) + 16) == b"\xaa" * 8 + data + b"\xaa" * 8
|
"%s",
|
||||||
|
tb.axi_ram.hexdump_str(
|
||||||
|
(addr & ~0xF) - 16,
|
||||||
|
(((addr & 0xF) + length - 1) & ~0xF) + 48,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(expected_data) <= len(stream_data):
|
||||||
|
assert tb.axi_ram.read(addr - 8, len(expected_data) + 16) == (
|
||||||
|
b"\xaa" * 8 + expected_data + b"\xaa" * 8
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert tb.axi_ram.read(addr - 8, len(stream_data) + 16) == (
|
||||||
|
b"\xaa" * 8 + stream_data + b"\xaa" * 8
|
||||||
|
)
|
||||||
|
|
||||||
|
cur_tag = (cur_tag + 1) % tag_count
|
||||||
|
|
||||||
await RisingEdge(dut.clk)
|
await RisingEdge(dut.clk)
|
||||||
await RisingEdge(dut.clk)
|
await RisingEdge(dut.clk)
|
||||||
|
|
||||||
|
|
||||||
@cocotb.test()
|
async def run_test_read(dut, data_in=None, idle_inserter=None, backpressure_inserter=None):
|
||||||
async def test_axi_dma_wrapper_read(dut):
|
"""DMA read path stress test adapted from Forencich's axi_dma test."""
|
||||||
tb = TB(dut)
|
tb = TB(dut)
|
||||||
await tb.reset()
|
|
||||||
|
|
||||||
tb.dut.read_enable.value = 1
|
byte_lanes = tb.axi_ram.read_if.byte_lanes
|
||||||
|
step_size = 1 if int(
|
||||||
|
os.getenv("PARAM_ENABLE_UNALIGNED", "0")) else byte_lanes
|
||||||
|
tag_count = 2 ** len(tb.read_desc_source.bus.tag)
|
||||||
|
cur_tag = 1
|
||||||
|
|
||||||
addr = 0x1800
|
await tb.cycle_reset()
|
||||||
data = _make_data(113, seed=0x40)
|
|
||||||
tag = 5
|
|
||||||
stream_id = 7
|
|
||||||
|
|
||||||
tb.axi_ram.write(addr - 16, b"\xcc" * (len(data) + 32))
|
tb.set_idle_generator(idle_inserter)
|
||||||
tb.axi_ram.write(addr, data)
|
tb.set_backpressure_generator(backpressure_inserter)
|
||||||
|
|
||||||
|
dut.read_enable.value = 1
|
||||||
|
|
||||||
|
for length in list(range(1, byte_lanes * 4 + 1)) + [128]:
|
||||||
|
offsets = list(range(0, byte_lanes * 2, step_size))
|
||||||
|
offsets += list(range(4096 - byte_lanes * 2, 4096, step_size))
|
||||||
|
|
||||||
|
for offset in offsets:
|
||||||
|
tb.log.info("read: length=%d offset=%d", length, offset)
|
||||||
|
|
||||||
|
addr = offset + 0x1000
|
||||||
|
test_data = bytearray([x % 256 for x in range(length)])
|
||||||
|
|
||||||
|
tb.axi_ram.write(addr - 128, b"\xaa" * (len(test_data) + 256))
|
||||||
|
tb.axi_ram.write(addr, test_data)
|
||||||
|
|
||||||
|
tb.log.debug(
|
||||||
|
"%s",
|
||||||
|
tb.axi_ram.hexdump_str(
|
||||||
|
(addr & ~0xF) - 16,
|
||||||
|
(((addr & 0xF) + length - 1) & ~0xF) + 48,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
await tb.read_desc_source.send(
|
await tb.read_desc_source.send(
|
||||||
DescTransaction(addr=addr, len=len(data), tag=tag, id=stream_id)
|
DescTransaction(addr=addr, len=len(
|
||||||
|
test_data), tag=cur_tag, id=cur_tag)
|
||||||
)
|
)
|
||||||
|
|
||||||
status = await tb.read_desc_status_sink.recv()
|
status = await tb.read_desc_status_sink.recv()
|
||||||
frame = await tb.read_data_sink.recv()
|
read_data = await tb.read_data_sink.recv()
|
||||||
|
|
||||||
tb.log.info("read status: %s", status)
|
tb.log.info("read status: %s", status)
|
||||||
tb.log.info("read frame: %s", frame)
|
tb.log.info("read data: %s", read_data)
|
||||||
|
|
||||||
|
assert int(status.tag) == cur_tag
|
||||||
assert int(status.error) == 0
|
assert int(status.error) == 0
|
||||||
assert int(status.tag) == tag
|
assert read_data.tdata == test_data
|
||||||
assert frame.tdata == data
|
assert int(read_data.tid) == cur_tag
|
||||||
assert int(frame.tid) == stream_id
|
|
||||||
|
cur_tag = (cur_tag + 1) % tag_count
|
||||||
|
|
||||||
await RisingEdge(dut.clk)
|
await RisingEdge(dut.clk)
|
||||||
await RisingEdge(dut.clk)
|
await RisingEdge(dut.clk)
|
||||||
|
|
||||||
|
|
||||||
|
def cycle_pause():
|
||||||
|
return itertools.cycle([1, 1, 1, 0])
|
||||||
|
|
||||||
|
|
||||||
|
# When imported by cocotb inside a simulator, generate the actual cocotb tests.
|
||||||
|
if cocotb.SIM_NAME:
|
||||||
|
for test in [run_test_write, run_test_read]:
|
||||||
|
factory = TestFactory(test)
|
||||||
|
factory.add_option("idle_inserter", [None, cycle_pause])
|
||||||
|
factory.add_option("backpressure_inserter", [None, cycle_pause])
|
||||||
|
factory.generate_tests()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Optional pytest entrypoint via cocotb-test.
|
# Optional pytest entrypoint via cocotb-test.
|
||||||
# Run from this directory with: pytest -q test_axi_dma_wrapper.py
|
# Run from this directory with: pytest -q test_axi_dma_wrapper.py
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_axi_dma_wrapper_pytest(request):
|
|
||||||
|
def _sanitize_node_name(name):
|
||||||
|
return name.replace("[", "-").replace("]", "").replace("/", "_")
|
||||||
|
|
||||||
|
|
||||||
|
if pytest is not None:
|
||||||
|
@pytest.mark.parametrize("axi_data_width", [8, 16, 32])
|
||||||
|
@pytest.mark.parametrize("unaligned", [0, 1])
|
||||||
|
def test_axi_dma_wrapper_pytest(request, axi_data_width, unaligned):
|
||||||
import cocotb_test.simulator
|
import cocotb_test.simulator
|
||||||
|
|
||||||
tests_dir = os.path.abspath(os.path.dirname(__file__))
|
tests_dir = os.path.abspath(os.path.dirname(__file__))
|
||||||
@ -178,7 +315,7 @@ def test_axi_dma_wrapper_pytest(request):
|
|||||||
toplevel = dut
|
toplevel = dut
|
||||||
|
|
||||||
parameters = {
|
parameters = {
|
||||||
"AXI_DATA_WIDTH": int(os.getenv("PARAM_AXI_DATA_WIDTH", "32")),
|
"AXI_DATA_WIDTH": axi_data_width,
|
||||||
"AXI_ADDR_WIDTH": 16,
|
"AXI_ADDR_WIDTH": 16,
|
||||||
"AXI_ID_WIDTH": 8,
|
"AXI_ID_WIDTH": 8,
|
||||||
"AXI_USER_WIDTH": 1,
|
"AXI_USER_WIDTH": 1,
|
||||||
@ -192,7 +329,7 @@ def test_axi_dma_wrapper_pytest(request):
|
|||||||
"LEN_WIDTH": 20,
|
"LEN_WIDTH": 20,
|
||||||
"TAG_WIDTH": 8,
|
"TAG_WIDTH": 8,
|
||||||
"ENABLE_SG": 0,
|
"ENABLE_SG": 0,
|
||||||
"ENABLE_UNALIGNED": int(os.getenv("PARAM_ENABLE_UNALIGNED", "0")),
|
"ENABLE_UNALIGNED": unaligned,
|
||||||
}
|
}
|
||||||
|
|
||||||
parameters["AXI_STRB_WIDTH"] = parameters["AXI_DATA_WIDTH"] // 8
|
parameters["AXI_STRB_WIDTH"] = parameters["AXI_DATA_WIDTH"] // 8
|
||||||
@ -212,16 +349,17 @@ def test_axi_dma_wrapper_pytest(request):
|
|||||||
os.path.join(forencich_rtl_dir, "axi_dma.v"),
|
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_rd.v"),
|
||||||
os.path.join(forencich_rtl_dir, "axi_dma_wr.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(wrapper_rtl_dir, "axi_dma_if_wrapper.sv"),
|
||||||
os.path.join(tests_dir, "tb_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()}
|
extra_env = {f"PARAM_{k}": str(v) for k, v in parameters.items()}
|
||||||
sim_build = os.path.join(
|
sim_build = os.path.join(
|
||||||
tests_dir,
|
tests_dir, "sim_build", _sanitize_node_name(request.node.name))
|
||||||
"sim_build",
|
|
||||||
request.node.name.replace("[", "-").replace("]", ""),
|
extra_args = []
|
||||||
)
|
if os.getenv("SIM", "verilator") == "verilator":
|
||||||
|
extra_args += ["--trace-structs"]
|
||||||
|
|
||||||
cocotb_test.simulator.run(
|
cocotb_test.simulator.run(
|
||||||
python_search=[tests_dir],
|
python_search=[tests_dir],
|
||||||
@ -232,6 +370,6 @@ def test_axi_dma_wrapper_pytest(request):
|
|||||||
parameters=parameters,
|
parameters=parameters,
|
||||||
sim_build=sim_build,
|
sim_build=sim_build,
|
||||||
extra_env=extra_env,
|
extra_env=extra_env,
|
||||||
waves=True,
|
waves=bool(int(os.getenv("WAVES", "0"))),
|
||||||
extra_args=["--trace-structs"] if os.getenv("SIM", "verilator") == "verilator" else [],
|
extra_args=extra_args,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user