stats: Add statistics counter module and testbench

Signed-off-by: Alex Forencich <alex@alexforencich.com>
This commit is contained in:
Alex Forencich
2025-03-25 00:02:58 -07:00
parent fd3e23ef6e
commit d2b0fa4693
4 changed files with 618 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
# SPDX-License-Identifier: CERN-OHL-S-2.0
#
# Copyright (c) 2021-2025 FPGA Ninja, LLC
#
# Authors:
# - Alex Forencich
TOPLEVEL_LANG = verilog
SIM ?= verilator
WAVES ?= 0
COCOTB_HDL_TIMEUNIT = 1ns
COCOTB_HDL_TIMEPRECISION = 1ps
DUT = taxi_stats_counter
COCOTB_TEST_MODULES = test_$(DUT)
COCOTB_TOPLEVEL = test_$(DUT)
MODULE = $(COCOTB_TEST_MODULES)
TOPLEVEL = $(COCOTB_TOPLEVEL)
VERILOG_SOURCES += $(COCOTB_TOPLEVEL).sv
VERILOG_SOURCES += ../../../rtl/stats/$(DUT).sv
VERILOG_SOURCES += ../../../rtl/axis/taxi_axis_if.sv
VERILOG_SOURCES += ../../../rtl/axi/taxi_axil_if.sv
# handle file list files
process_f_file = $(call process_f_files,$(addprefix $(dir $1),$(shell cat $1)))
process_f_files = $(foreach f,$1,$(if $(filter %.f,$f),$(call process_f_file,$f),$f))
uniq_base = $(if $1,$(call uniq_base,$(foreach f,$1,$(if $(filter-out $(notdir $(lastword $1)),$(notdir $f)),$f,))) $(lastword $1))
VERILOG_SOURCES := $(call uniq_base,$(call process_f_files,$(VERILOG_SOURCES)))
# module parameters
export PARAM_STAT_COUNT_W := 32
export PARAM_PIPELINE := 2
export PARAM_STAT_INC_W := 16
export PARAM_STAT_ID_W := 8
export PARAM_AXIL_DATA_W := 32
export PARAM_AXIL_ADDR_W := $(shell python -c "print($(PARAM_STAT_ID_W) + (($(PARAM_STAT_COUNT_W)+7)//8-1).bit_length())")
ifeq ($(SIM), icarus)
PLUSARGS += -fst
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
else ifeq ($(SIM), verilator)
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
ifeq ($(WAVES), 1)
COMPILE_ARGS += --trace-fst
VERILATOR_TRACE = 1
endif
endif
include $(shell cocotb-config --makefiles)/Makefile.sim

View File

@@ -0,0 +1,228 @@
#!/usr/bin/env python
# SPDX-License-Identifier: CERN-OHL-S-2.0
"""
Copyright (c) 2021-2025 FPGA Ninja, LLC
Authors:
- Alex Forencich
"""
import itertools
import logging
import os
import random
import cocotb_test.simulator
import pytest
import cocotb
from cocotb.clock import Clock
from cocotb.queue import Queue
from cocotb.triggers import RisingEdge, Timer
from cocotb.regression import TestFactory
from cocotbext.axi import AxiLiteBus, AxiLiteMaster
from cocotbext.axi import AxiStreamBus, AxiStreamSource, AxiStreamFrame
class TB(object):
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())
self.stat_source = AxiStreamSource(AxiStreamBus.from_entity(dut.s_axis_stat), dut.clk, dut.rst)
self.axil_master = AxiLiteMaster(AxiLiteBus.from_entity(dut.s_axil), dut.clk, dut.rst)
def set_idle_generator(self, generator=None):
if generator:
self.stat_source.set_pause_generator(generator())
self.axil_master.write_if.aw_channel.set_pause_generator(generator())
self.axil_master.write_if.w_channel.set_pause_generator(generator())
self.axil_master.read_if.ar_channel.set_pause_generator(generator())
def set_backpressure_generator(self, generator=None):
if generator:
self.axil_master.write_if.b_channel.set_pause_generator(generator())
self.axil_master.read_if.r_channel.set_pause_generator(generator())
async def cycle_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)
async def run_test_acc(dut, idle_inserter=None, backpressure_inserter=None):
tb = TB(dut)
byte_lanes = tb.axil_master.read_if.byte_lanes
counter_size = max(dut.STAT_COUNT_W.value // 8, byte_lanes)
await tb.cycle_reset()
tb.set_idle_generator(idle_inserter)
tb.set_backpressure_generator(backpressure_inserter)
await Timer(4000, 'ns')
for n in range(10):
for k in range(10):
await tb.stat_source.send(AxiStreamFrame([k], tid=k))
await Timer(1000, 'ns')
data = await tb.axil_master.read_words(0, 10, ws=counter_size)
print(data)
for n in range(10):
assert data[n] == n*10
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.axil_master.read_if.byte_lanes
counter_size = max(dut.STAT_COUNT_W.value // 8, byte_lanes)
stat_inc_width = len(dut.s_axis_stat.tdata)
stat_id_width = len(dut.s_axis_stat.tid)
await tb.cycle_reset()
tb.set_idle_generator(idle_inserter)
tb.set_backpressure_generator(backpressure_inserter)
await Timer(4000, 'ns')
async def worker(source, queue, count=128):
for k in range(count):
count = random.randrange(1, 2**stat_inc_width)
num = random.randrange(0, 2**stat_id_width)
await tb.stat_source.send(AxiStreamFrame([count], tid=num))
await queue.put((num, count))
await Timer(random.randint(1, 1000), 'ns')
workers = []
queue = Queue()
for k in range(16):
workers.append(cocotb.start_soon(worker(tb.stat_source, queue, count=128)))
while workers:
await workers.pop(0).join()
await Timer(1000, 'ns')
data_ref = [0]*2**stat_id_width
while not queue.empty():
num, count = await queue.get()
data_ref[num] += count
print(data_ref)
data = await tb.axil_master.read_words(0, 2**stat_id_width, ws=counter_size)
print(data)
assert data == data_ref
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
def cycle_pause():
return itertools.cycle([1, 1, 1, 0])
if cocotb.SIM_NAME:
for test in [run_test_acc]:
factory = TestFactory(test)
factory.add_option("idle_inserter", [None, cycle_pause])
factory.add_option("backpressure_inserter", [None, cycle_pause])
factory.generate_tests()
factory = TestFactory(run_stress_test)
factory.generate_tests()
# cocotb-test
tests_dir = os.path.dirname(__file__)
rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', '..', 'rtl'))
def process_f_files(files):
lst = {}
for f in files:
if f[-2:].lower() == '.f':
with open(f, 'r') as fp:
l = fp.read().split()
for f in process_f_files([os.path.join(os.path.dirname(f), x) for x in l]):
lst[os.path.basename(f)] = f
else:
lst[os.path.basename(f)] = f
return list(lst.values())
@pytest.mark.parametrize("stat_count_w", [32, 64])
def test_taxi_stats_counter(request, stat_count_w):
dut = "taxi_stats_counter"
module = os.path.splitext(os.path.basename(__file__))[0]
toplevel = module
verilog_sources = [
os.path.join(tests_dir, f"{toplevel}.sv"),
os.path.join(rtl_dir, "stats", f"{dut}.sv"),
os.path.join(rtl_dir, "axis", "taxi_axis_if.sv"),
os.path.join(rtl_dir, "axi", "taxi_axil_if.sv"),
]
verilog_sources = process_f_files(verilog_sources)
parameters = {}
parameters['STAT_COUNT_W'] = stat_count_w
parameters['PIPELINE'] = 2
parameters['STAT_INC_W'] = 16
parameters['STAT_ID_W'] = 8
parameters['AXIL_DATA_W'] = 32
parameters['AXIL_ADDR_W'] = parameters['STAT_ID_W'] + ((parameters['STAT_COUNT_W']+7)//8-1).bit_length()
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(
simulator="verilator",
python_search=[tests_dir],
verilog_sources=verilog_sources,
toplevel=toplevel,
module=module,
parameters=parameters,
sim_build=sim_build,
extra_env=extra_env,
)

View File

@@ -0,0 +1,69 @@
// SPDX-License-Identifier: CERN-OHL-S-2.0
/*
Copyright (c) 2025 FPGA Ninja, LLC
Authors:
- Alex Forencich
*/
`resetall
`timescale 1ns / 1ps
`default_nettype none
/*
* Statistics counter testbench
*/
module test_taxi_stats_counter #
(
/* verilator lint_off WIDTHTRUNC */
parameter STAT_COUNT_W = 32,
parameter PIPELINE = 2,
parameter STAT_INC_W = 16,
parameter STAT_ID_W = 8,
parameter AXIL_DATA_W = 32,
parameter AXIL_ADDR_W = STAT_ID_W + $clog2((STAT_COUNT_W+7)/8)
/* verilator lint_on WIDTHTRUNC */
)
();
logic clk;
logic rst;
taxi_axis_if #(
.DATA_W(STAT_INC_W),
.KEEP_EN(0),
.KEEP_W(1),
.ID_EN(1),
.ID_W(STAT_ID_W)
) s_axis_stat();
taxi_axil_if #(
.DATA_W(AXIL_DATA_W),
.ADDR_W(AXIL_ADDR_W)
) s_axil();
taxi_stats_counter #(
.STAT_COUNT_W(STAT_COUNT_W),
.PIPELINE(PIPELINE)
)
uut (
.clk(clk),
.rst(rst),
/*
* Statistics increment input
*/
.s_axis_stat(s_axis_stat),
/*
* AXI Lite register interface
*/
.s_axil_wr(s_axil),
.s_axil_rd(s_axil)
);
endmodule
`resetall