diff --git a/tb/axis_ram_switch/Makefile b/tb/axis_ram_switch/Makefile new file mode 100644 index 000000000..79e898ce6 --- /dev/null +++ b/tb/axis_ram_switch/Makefile @@ -0,0 +1,134 @@ +# Copyright (c) 2021 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. + +TOPLEVEL_LANG = verilog + +SIM ?= icarus +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +export PARAM_S_COUNT ?= 4 +export PARAM_M_COUNT ?= 4 + +DUT = axis_ram_switch +WRAPPER = $(DUT)_wrap_$(PARAM_S_COUNT)x$(PARAM_M_COUNT) +TOPLEVEL = $(WRAPPER) +MODULE = test_$(DUT) +VERILOG_SOURCES += $(WRAPPER).v +VERILOG_SOURCES += ../../rtl/$(DUT).v +VERILOG_SOURCES += ../../rtl/axis_adapter.v +VERILOG_SOURCES += ../../rtl/arbiter.v +VERILOG_SOURCES += ../../rtl/priority_encoder.v + +# module parameters +export PARAM_FIFO_DEPTH ?= 4096 +export PARAM_CMD_FIFO_DEPTH ?= 32 +export PARAM_SPEEDUP ?= 0 +export PARAM_S_DATA_WIDTH ?= 8 +export PARAM_S_KEEP_ENABLE ?= $(shell expr $(PARAM_S_DATA_WIDTH) \> 8 ) +export PARAM_S_KEEP_WIDTH ?= $(shell expr $(PARAM_S_DATA_WIDTH) / 8 ) +export PARAM_M_DATA_WIDTH ?= 8 +export PARAM_M_KEEP_ENABLE ?= $(shell expr $(PARAM_M_DATA_WIDTH) \> 8 ) +export PARAM_M_KEEP_WIDTH ?= $(shell expr $(PARAM_M_DATA_WIDTH) / 8 ) +export PARAM_ID_ENABLE ?= 1 +export PARAM_ID_WIDTH ?= 16 +export PARAM_DEST_WIDTH ?= 8 +export PARAM_USER_ENABLE ?= 1 +export PARAM_USER_WIDTH ?= 1 +export PARAM_USER_BAD_FRAME_VALUE ?= 1 +export PARAM_USER_BAD_FRAME_MASK ?= 1 +export PARAM_DROP_BAD_FRAME ?= 0 +export PARAM_DROP_WHEN_FULL ?= 0 +export PARAM_RAM_PIPELINE ?= 2 + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += -P $(TOPLEVEL).FIFO_DEPTH=$(PARAM_FIFO_DEPTH) + COMPILE_ARGS += -P $(TOPLEVEL).CMD_FIFO_DEPTH=$(PARAM_CMD_FIFO_DEPTH) + COMPILE_ARGS += -P $(TOPLEVEL).SPEEDUP=$(PARAM_SPEEDUP) + COMPILE_ARGS += -P $(TOPLEVEL).S_DATA_WIDTH=$(PARAM_S_DATA_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).S_KEEP_ENABLE=$(PARAM_S_KEEP_ENABLE) + COMPILE_ARGS += -P $(TOPLEVEL).S_KEEP_WIDTH=$(PARAM_S_KEEP_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).M_DATA_WIDTH=$(PARAM_M_DATA_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).M_KEEP_ENABLE=$(PARAM_M_KEEP_ENABLE) + COMPILE_ARGS += -P $(TOPLEVEL).M_KEEP_WIDTH=$(PARAM_M_KEEP_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).ID_ENABLE=$(PARAM_ID_ENABLE) + COMPILE_ARGS += -P $(TOPLEVEL).ID_WIDTH=$(PARAM_ID_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).DEST_WIDTH=$(PARAM_DEST_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).USER_ENABLE=$(PARAM_USER_ENABLE) + COMPILE_ARGS += -P $(TOPLEVEL).USER_WIDTH=$(PARAM_USER_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).USER_BAD_FRAME_VALUE=$(PARAM_USER_BAD_FRAME_VALUE) + COMPILE_ARGS += -P $(TOPLEVEL).USER_BAD_FRAME_MASK=$(PARAM_USER_BAD_FRAME_MASK) + COMPILE_ARGS += -P $(TOPLEVEL).DROP_BAD_FRAME=$(PARAM_DROP_BAD_FRAME) + COMPILE_ARGS += -P $(TOPLEVEL).DROP_WHEN_FULL=$(PARAM_DROP_WHEN_FULL) + COMPILE_ARGS += -P $(TOPLEVEL).RAM_PIPELINE=$(PARAM_RAM_PIPELINE) + + ifeq ($(WAVES), 1) + VERILOG_SOURCES += iverilog_dump.v + COMPILE_ARGS += -s iverilog_dump + endif +else ifeq ($(SIM), verilator) + COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH + + COMPILE_ARGS += -GFIFO_DEPTH=$(PARAM_FIFO_DEPTH) + COMPILE_ARGS += -GCMD_FIFO_DEPTH=$(PARAM_CMD_FIFO_DEPTH) + COMPILE_ARGS += -GSPEEDUP=$(PARAM_SPEEDUP) + COMPILE_ARGS += -GS_DATA_WIDTH=$(PARAM_S_DATA_WIDTH) + COMPILE_ARGS += -GS_KEEP_ENABLE=$(PARAM_S_KEEP_ENABLE) + COMPILE_ARGS += -GS_KEEP_WIDTH=$(PARAM_S_KEEP_WIDTH) + COMPILE_ARGS += -GM_DATA_WIDTH=$(PARAM_M_DATA_WIDTH) + COMPILE_ARGS += -GM_KEEP_ENABLE=$(PARAM_M_KEEP_ENABLE) + COMPILE_ARGS += -GM_KEEP_WIDTH=$(PARAM_M_KEEP_WIDTH) + COMPILE_ARGS += -GID_ENABLE=$(PARAM_ID_ENABLE) + COMPILE_ARGS += -GID_WIDTH=$(PARAM_ID_WIDTH) + COMPILE_ARGS += -GDEST_WIDTH=$(PARAM_DEST_WIDTH) + COMPILE_ARGS += -GUSER_ENABLE=$(PARAM_USER_ENABLE) + COMPILE_ARGS += -GUSER_WIDTH=$(PARAM_USER_WIDTH) + COMPILE_ARGS += -GUSER_BAD_FRAME_VALUE=$(PARAM_USER_BAD_FRAME_VALUE) + COMPILE_ARGS += -GUSER_BAD_FRAME_MASK=$(PARAM_USER_BAD_FRAME_MASK) + COMPILE_ARGS += -GDROP_BAD_FRAME=$(PARAM_DROP_BAD_FRAME) + COMPILE_ARGS += -GDROP_WHEN_FULL=$(PARAM_DROP_WHEN_FULL) + COMPILE_ARGS += -GRAM_PIPELINE=$(PARAM_RAM_PIPELINE) + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace-fst + endif +endif + +include $(shell cocotb-config --makefiles)/Makefile.sim + +$(WRAPPER).v: ../../rtl/$(DUT)_wrap.py + $< -p $(PARAM_S_COUNT) $(PARAM_M_COUNT) + +iverilog_dump.v: + echo 'module iverilog_dump();' > $@ + echo 'initial begin' >> $@ + echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@ + echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@ + echo 'end' >> $@ + echo 'endmodule' >> $@ + +clean:: + @rm -rf iverilog_dump.v + @rm -rf dump.fst $(TOPLEVEL).fst + @rm -rf *_wrap_*.v diff --git a/tb/axis_ram_switch/test_axis_ram_switch.py b/tb/axis_ram_switch/test_axis_ram_switch.py new file mode 100644 index 000000000..adc4ab699 --- /dev/null +++ b/tb/axis_ram_switch/test_axis_ram_switch.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python +""" + +Copyright (c) 2021 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 itertools +import logging +import os +import random +import subprocess + +import cocotb_test.simulator +import pytest + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge, Event +from cocotb.regression import TestFactory + +from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSource, AxiStreamSink + + +class TB(object): + def __init__(self, dut): + self.dut = dut + + s_count = len(dut.axis_ram_switch_inst.s_axis_tvalid) + m_count = len(dut.axis_ram_switch_inst.m_axis_tvalid) + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.fork(Clock(dut.clk, 10, units="ns").start()) + + self.source = [AxiStreamSource(AxiStreamBus.from_prefix(dut, f"s{k:02d}_axis"), dut.clk, dut.rst) for k in range(s_count)] + self.sink = [AxiStreamSink(AxiStreamBus.from_prefix(dut, f"m{k:02d}_axis"), dut.clk, dut.rst) for k in range(m_count)] + + def set_idle_generator(self, generator=None): + if generator: + for source in self.source: + source.set_pause_generator(generator()) + + def set_backpressure_generator(self, generator=None): + if generator: + for sink in self.sink: + sink.set_pause_generator(generator()) + + async def reset(self): + self.dut.rst.setimmediatevalue(0) + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst <= 1 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst <= 0 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + + +async def run_test(dut, payload_lengths=None, payload_data=None, idle_inserter=None, backpressure_inserter=None, s=0, m=0): + + tb = TB(dut) + + id_count = 2**len(tb.source[s].bus.tid) + + cur_id = 1 + + await tb.reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_frames = [] + + for test_data in [payload_data(x) for x in payload_lengths()]: + test_frame = AxiStreamFrame(test_data) + test_frame.tid = cur_id + test_frame.tdest = m + + test_frames.append(test_frame) + await tb.source[s].send(test_frame) + + cur_id = (cur_id + 1) % id_count + + for test_frame in test_frames: + rx_frame = await tb.sink[m].recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tid == test_frame.tid + assert rx_frame.tdest == test_frame.tdest + assert not rx_frame.tuser + + assert all(s.empty() for s in tb.sink) + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_tuser_assert(dut, s=0, m=0): + + tb = TB(dut) + + await tb.reset() + + test_data = bytearray(itertools.islice(itertools.cycle(range(256)), 32)) + test_frame = AxiStreamFrame(test_data, tuser=1, tdest=m) + await tb.source[s].send(test_frame) + + rx_frame = await tb.sink[m].recv() + + assert rx_frame.tdata == test_data + assert rx_frame.tuser + + assert all(s.empty() for s in tb.sink) + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_arb_test(dut): + + tb = TB(dut) + + byte_lanes = max(tb.source[0].byte_lanes, tb.sink[0].byte_lanes) + id_count = 2**len(tb.source[0].bus.tid) + + cur_id = 1 + + await tb.reset() + + test_frames = [] + + length = byte_lanes*16 + test_data = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + for k in range(5): + test_frame = AxiStreamFrame(test_data, tx_complete=Event()) + test_frame.tid = cur_id + test_frame.tdest = 0 + + src_ind = 0 + + if k == 0: + src_ind = 0 + elif k == 4: + await test_frames[1].tx_complete.wait() + for j in range(8): + await RisingEdge(dut.clk) + src_ind = 0 + else: + src_ind = 1 + + test_frames.append(test_frame) + await tb.source[src_ind].send(test_frame) + + cur_id = (cur_id + 1) % id_count + + for k in [0, 1, 2, 4, 3]: + test_frame = test_frames[k] + rx_frame = await tb.sink[0].recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tid == test_frame.tid + assert rx_frame.tdest == test_frame.tdest + assert not rx_frame.tuser + + assert all(s.empty() for s in tb.sink) + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + byte_lanes = max(tb.source[0].byte_lanes, tb.sink[0].byte_lanes) + id_count = 2**len(tb.source[0].bus.tid) + + cur_id = 1 + + await tb.reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_frames = [[list() for y in tb.sink] for x in tb.source] + + for p in range(len(tb.source)): + for k in range(128): + length = random.randint(1, byte_lanes*16) + test_data = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + test_frame = AxiStreamFrame(test_data) + test_frame.tid = cur_id + test_frame.tdest = random.randrange(len(tb.sink)) + + test_frames[p][test_frame.tdest].append(test_frame) + await tb.source[p].send(test_frame) + + cur_id = (cur_id + 1) % id_count + + for lst in test_frames: + while any(lst): + rx_frame = await tb.sink[[x for x in lst if x][0][0].tdest].recv() + + test_frame = None + + for lst_a in test_frames: + for lst_b in lst_a: + if lst_b and lst_b[0].tid == rx_frame.tid: + test_frame = lst_b.pop(0) + break + + assert test_frame is not None + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tid == test_frame.tid + assert rx_frame.tdest == test_frame.tdest + assert not rx_frame.tuser + + assert all(s.empty() for s in tb.sink) + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +def cycle_pause(): + return itertools.cycle([1, 1, 1, 0]) + + +def size_list(): + data_width = max(len(cocotb.top.s00_axis_tdata), len(cocotb.top.m00_axis_tdata)) + byte_width = data_width // 8 + return list(range(1, byte_width*4+1))+[512]+[1]*64 + + +def incrementing_payload(length): + return bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + +if cocotb.SIM_NAME: + + s_count = len(cocotb.top.axis_ram_switch_inst.s_axis_tvalid) + m_count = len(cocotb.top.axis_ram_switch_inst.m_axis_tvalid) + + factory = TestFactory(run_test) + factory.add_option("payload_lengths", [size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.add_option("s", range(min(s_count, 2))) + factory.add_option("m", range(min(m_count, 2))) + factory.generate_tests() + + for test in [run_test_tuser_assert]: + factory = TestFactory(test) + factory.add_option("s", range(min(s_count, 2))) + factory.add_option("m", range(min(m_count, 2))) + factory.generate_tests() + + if s_count > 1: + factory = TestFactory(run_arb_test) + factory.generate_tests() + + factory = TestFactory(run_stress_test) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.dirname(__file__) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl')) + + +@pytest.mark.parametrize("m_data_width", [8, 32]) +@pytest.mark.parametrize("s_data_width", [8, 32]) +@pytest.mark.parametrize("m_count", [1, 4]) +@pytest.mark.parametrize("s_count", [1, 4]) +def test_axis_ram_switch(request, s_count, m_count, s_data_width, m_data_width): + dut = "axis_ram_switch" + wrapper = f"{dut}_wrap_{s_count}x{m_count}" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = wrapper + + # generate wrapper + wrapper_file = os.path.join(tests_dir, f"{wrapper}.v") + if not os.path.exists(wrapper_file): + subprocess.Popen( + [os.path.join(rtl_dir, f"{dut}_wrap.py"), "-p", f"{s_count}", f"{m_count}"], + cwd=tests_dir + ).wait() + + verilog_sources = [ + wrapper_file, + os.path.join(rtl_dir, f"{dut}.v"), + os.path.join(rtl_dir, "axis_adapter.v"), + os.path.join(rtl_dir, "arbiter.v"), + os.path.join(rtl_dir, "priority_encoder.v"), + ] + + parameters = {} + + parameters['S_COUNT'] = s_count + parameters['M_COUNT'] = m_count + + parameters['FIFO_DEPTH'] = 4096 + parameters['CMD_FIFO_DEPTH'] = 32 + parameters['SPEEDUP'] = 0 + parameters['S_DATA_WIDTH'] = s_data_width + parameters['S_KEEP_ENABLE'] = int(parameters['S_DATA_WIDTH'] > 8) + parameters['S_KEEP_WIDTH'] = parameters['S_DATA_WIDTH'] // 8 + parameters['M_DATA_WIDTH'] = m_data_width + parameters['M_KEEP_ENABLE'] = int(parameters['M_DATA_WIDTH'] > 8) + parameters['M_KEEP_WIDTH'] = parameters['M_DATA_WIDTH'] // 8 + parameters['ID_ENABLE'] = 1 + parameters['ID_WIDTH'] = 16 + parameters['DEST_WIDTH'] = 8 + parameters['USER_ENABLE'] = 1 + parameters['USER_WIDTH'] = 1 + parameters['USER_BAD_FRAME_VALUE'] = 1 + parameters['USER_BAD_FRAME_MASK'] = 1 + parameters['DROP_BAD_FRAME'] = 0 + parameters['DROP_WHEN_FULL'] = 0 + parameters['RAM_PIPELINE'] = 2 + + 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, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + ) diff --git a/tb/axis_switch/Makefile b/tb/axis_switch/Makefile new file mode 100644 index 000000000..cd925f645 --- /dev/null +++ b/tb/axis_switch/Makefile @@ -0,0 +1,107 @@ +# Copyright (c) 2021 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. + +TOPLEVEL_LANG = verilog + +SIM ?= icarus +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +export PARAM_S_COUNT ?= 4 +export PARAM_M_COUNT ?= 4 + +DUT = axis_switch +WRAPPER = $(DUT)_wrap_$(PARAM_S_COUNT)x$(PARAM_M_COUNT) +TOPLEVEL = $(WRAPPER) +MODULE = test_$(DUT) +VERILOG_SOURCES += $(WRAPPER).v +VERILOG_SOURCES += ../../rtl/$(DUT).v +VERILOG_SOURCES += ../../rtl/axis_register.v +VERILOG_SOURCES += ../../rtl/arbiter.v +VERILOG_SOURCES += ../../rtl/priority_encoder.v + +# module parameters +export PARAM_DATA_WIDTH ?= 8 +export PARAM_KEEP_ENABLE ?= $(shell expr $(PARAM_DATA_WIDTH) \> 8 ) +export PARAM_KEEP_WIDTH ?= $(shell expr $(PARAM_DATA_WIDTH) / 8 ) +export PARAM_ID_ENABLE ?= 1 +export PARAM_ID_WIDTH ?= 16 +export PARAM_DEST_WIDTH ?= 8 +export PARAM_USER_ENABLE ?= 1 +export PARAM_USER_WIDTH ?= 1 +export PARAM_S_REG_TYPE ?= 0 +export PARAM_M_REG_TYPE ?= 2 + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += -P $(TOPLEVEL).DATA_WIDTH=$(PARAM_DATA_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).KEEP_ENABLE=$(PARAM_KEEP_ENABLE) + COMPILE_ARGS += -P $(TOPLEVEL).KEEP_WIDTH=$(PARAM_KEEP_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).ID_ENABLE=$(PARAM_ID_ENABLE) + COMPILE_ARGS += -P $(TOPLEVEL).ID_WIDTH=$(PARAM_ID_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).DEST_WIDTH=$(PARAM_DEST_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).USER_ENABLE=$(PARAM_USER_ENABLE) + COMPILE_ARGS += -P $(TOPLEVEL).USER_WIDTH=$(PARAM_USER_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).S_REG_TYPE=$(PARAM_S_REG_TYPE) + COMPILE_ARGS += -P $(TOPLEVEL).M_REG_TYPE=$(PARAM_M_REG_TYPE) + + ifeq ($(WAVES), 1) + VERILOG_SOURCES += iverilog_dump.v + COMPILE_ARGS += -s iverilog_dump + endif +else ifeq ($(SIM), verilator) + COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH + + COMPILE_ARGS += -GDATA_WIDTH=$(PARAM_DATA_WIDTH) + COMPILE_ARGS += -GKEEP_ENABLE=$(PARAM_KEEP_ENABLE) + COMPILE_ARGS += -GKEEP_WIDTH=$(PARAM_KEEP_WIDTH) + COMPILE_ARGS += -GID_ENABLE=$(PARAM_ID_ENABLE) + COMPILE_ARGS += -GID_WIDTH=$(PARAM_ID_WIDTH) + COMPILE_ARGS += -GDEST_WIDTH=$(PARAM_DEST_WIDTH) + COMPILE_ARGS += -GUSER_ENABLE=$(PARAM_USER_ENABLE) + COMPILE_ARGS += -GUSER_WIDTH=$(PARAM_USER_WIDTH) + COMPILE_ARGS += -GS_REG_TYPE=$(PARAM_S_REG_TYPE) + COMPILE_ARGS += -GM_REG_TYPE=$(PARAM_M_REG_TYPE) + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace-fst + endif +endif + +include $(shell cocotb-config --makefiles)/Makefile.sim + +$(WRAPPER).v: ../../rtl/$(DUT)_wrap.py + $< -p $(PARAM_S_COUNT) $(PARAM_M_COUNT) + +iverilog_dump.v: + echo 'module iverilog_dump();' > $@ + echo 'initial begin' >> $@ + echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@ + echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@ + echo 'end' >> $@ + echo 'endmodule' >> $@ + +clean:: + @rm -rf iverilog_dump.v + @rm -rf dump.fst $(TOPLEVEL).fst + @rm -rf *_wrap_*.v diff --git a/tb/axis_switch/test_axis_switch.py b/tb/axis_switch/test_axis_switch.py new file mode 100644 index 000000000..1bdceceec --- /dev/null +++ b/tb/axis_switch/test_axis_switch.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python +""" + +Copyright (c) 2021 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 itertools +import logging +import os +import random +import subprocess + +import cocotb_test.simulator +import pytest + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge, Event +from cocotb.regression import TestFactory + +from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSource, AxiStreamSink + + +class TB(object): + def __init__(self, dut): + self.dut = dut + + s_count = len(dut.axis_switch_inst.s_axis_tvalid) + m_count = len(dut.axis_switch_inst.m_axis_tvalid) + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.fork(Clock(dut.clk, 10, units="ns").start()) + + self.source = [AxiStreamSource(AxiStreamBus.from_prefix(dut, f"s{k:02d}_axis"), dut.clk, dut.rst) for k in range(s_count)] + self.sink = [AxiStreamSink(AxiStreamBus.from_prefix(dut, f"m{k:02d}_axis"), dut.clk, dut.rst) for k in range(m_count)] + + def set_idle_generator(self, generator=None): + if generator: + for source in self.source: + source.set_pause_generator(generator()) + + def set_backpressure_generator(self, generator=None): + if generator: + for sink in self.sink: + sink.set_pause_generator(generator()) + + async def reset(self): + self.dut.rst.setimmediatevalue(0) + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst <= 1 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst <= 0 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + + +async def run_test(dut, payload_lengths=None, payload_data=None, idle_inserter=None, backpressure_inserter=None, s=0, m=0): + + tb = TB(dut) + + id_count = 2**len(tb.source[s].bus.tid) + + cur_id = 1 + + await tb.reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_frames = [] + + for test_data in [payload_data(x) for x in payload_lengths()]: + test_frame = AxiStreamFrame(test_data) + test_frame.tid = cur_id + test_frame.tdest = m + + test_frames.append(test_frame) + await tb.source[s].send(test_frame) + + cur_id = (cur_id + 1) % id_count + + for test_frame in test_frames: + rx_frame = await tb.sink[m].recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tid == test_frame.tid + assert rx_frame.tdest == test_frame.tdest + assert not rx_frame.tuser + + assert all(s.empty() for s in tb.sink) + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_tuser_assert(dut, s=0, m=0): + + tb = TB(dut) + + await tb.reset() + + test_data = bytearray(itertools.islice(itertools.cycle(range(256)), 32)) + test_frame = AxiStreamFrame(test_data, tuser=1, tdest=m) + await tb.source[s].send(test_frame) + + rx_frame = await tb.sink[m].recv() + + assert rx_frame.tdata == test_data + assert rx_frame.tuser + + assert all(s.empty() for s in tb.sink) + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_arb_test(dut): + + tb = TB(dut) + + byte_lanes = tb.source[0].byte_lanes + id_count = 2**len(tb.source[0].bus.tid) + + cur_id = 1 + + await tb.reset() + + test_frames = [] + + length = byte_lanes*16 + test_data = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + for k in range(5): + test_frame = AxiStreamFrame(test_data, tx_complete=Event()) + test_frame.tid = cur_id + test_frame.tdest = 0 + + src_ind = 0 + + if k == 0: + src_ind = 0 + elif k == 4: + await test_frames[1].tx_complete.wait() + for j in range(8): + await RisingEdge(dut.clk) + src_ind = 0 + else: + src_ind = 1 + + test_frames.append(test_frame) + await tb.source[src_ind].send(test_frame) + + cur_id = (cur_id + 1) % id_count + + for k in [0, 1, 2, 4, 3]: + test_frame = test_frames[k] + rx_frame = await tb.sink[0].recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tid == test_frame.tid + assert rx_frame.tdest == test_frame.tdest + assert not rx_frame.tuser + + assert all(s.empty() for s in tb.sink) + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + byte_lanes = tb.source[0].byte_lanes + id_count = 2**len(tb.source[0].bus.tid) + + cur_id = 1 + + await tb.reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_frames = [[list() for y in tb.sink] for x in tb.source] + + for p in range(len(tb.source)): + for k in range(128): + length = random.randint(1, byte_lanes*16) + test_data = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + test_frame = AxiStreamFrame(test_data) + test_frame.tid = cur_id + test_frame.tdest = random.randrange(len(tb.sink)) + + test_frames[p][test_frame.tdest].append(test_frame) + await tb.source[p].send(test_frame) + + cur_id = (cur_id + 1) % id_count + + for lst in test_frames: + while any(lst): + rx_frame = await tb.sink[[x for x in lst if x][0][0].tdest].recv() + + test_frame = None + + for lst_a in test_frames: + for lst_b in lst_a: + if lst_b and lst_b[0].tid == rx_frame.tid: + test_frame = lst_b.pop(0) + break + + assert test_frame is not None + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tid == test_frame.tid + assert rx_frame.tdest == test_frame.tdest + assert not rx_frame.tuser + + assert all(s.empty() for s in tb.sink) + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +def cycle_pause(): + return itertools.cycle([1, 1, 1, 0]) + + +def size_list(): + data_width = len(cocotb.top.s00_axis_tdata) + byte_width = data_width // 8 + return list(range(1, byte_width*4+1))+[512]+[1]*64 + + +def incrementing_payload(length): + return bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + +if cocotb.SIM_NAME: + + s_count = len(cocotb.top.axis_switch_inst.s_axis_tvalid) + m_count = len(cocotb.top.axis_switch_inst.m_axis_tvalid) + + factory = TestFactory(run_test) + factory.add_option("payload_lengths", [size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.add_option("s", range(min(s_count, 2))) + factory.add_option("m", range(min(m_count, 2))) + factory.generate_tests() + + for test in [run_test_tuser_assert]: + factory = TestFactory(test) + factory.add_option("s", range(min(s_count, 2))) + factory.add_option("m", range(min(m_count, 2))) + factory.generate_tests() + + if s_count > 1: + factory = TestFactory(run_arb_test) + factory.generate_tests() + + factory = TestFactory(run_stress_test) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.dirname(__file__) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl')) + + +@pytest.mark.parametrize("data_width", [8, 16, 32]) +@pytest.mark.parametrize("m_count", [1, 4]) +@pytest.mark.parametrize("s_count", [1, 4]) +def test_axis_switch(request, s_count, m_count, data_width): + dut = "axis_switch" + wrapper = f"{dut}_wrap_{s_count}x{m_count}" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = wrapper + + # generate wrapper + wrapper_file = os.path.join(tests_dir, f"{wrapper}.v") + if not os.path.exists(wrapper_file): + subprocess.Popen( + [os.path.join(rtl_dir, f"{dut}_wrap.py"), "-p", f"{s_count}", f"{m_count}"], + cwd=tests_dir + ).wait() + + verilog_sources = [ + wrapper_file, + os.path.join(rtl_dir, f"{dut}.v"), + os.path.join(rtl_dir, "axis_register.v"), + os.path.join(rtl_dir, "arbiter.v"), + os.path.join(rtl_dir, "priority_encoder.v"), + ] + + parameters = {} + + parameters['S_COUNT'] = s_count + parameters['M_COUNT'] = m_count + + parameters['DATA_WIDTH'] = data_width + parameters['KEEP_ENABLE'] = int(parameters['DATA_WIDTH'] > 8) + parameters['KEEP_WIDTH'] = parameters['DATA_WIDTH'] // 8 + parameters['ID_ENABLE'] = 1 + parameters['ID_WIDTH'] = 16 + parameters['DEST_WIDTH'] = 8 + parameters['USER_ENABLE'] = 1 + parameters['USER_WIDTH'] = 1 + parameters['S_REG_TYPE'] = 0 + parameters['M_REG_TYPE'] = 2 + + 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, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + )