Reorganize repository

Signed-off-by: Alex Forencich <alex@alexforencich.com>
This commit is contained in:
Alex Forencich
2025-05-18 12:25:59 -07:00
parent 8cdae180a1
commit 66b53d98a2
690 changed files with 2314 additions and 1581 deletions

1
src/stats/lib/taxi Symbolic link
View File

@@ -0,0 +1 @@
../../../

View File

@@ -0,0 +1,305 @@
// SPDX-License-Identifier: CERN-OHL-S-2.0
/*
Copyright (c) 2021-2025 FPGA Ninja, LLC
Authors:
- Alex Forencich
*/
`resetall
`timescale 1ns / 1ps
`default_nettype none
/*
* Statistics collector
*/
module taxi_stats_collect #
(
// Channel count
parameter CNT = 8,
// Increment width (bits)
parameter INC_W = 8,
// Base statistic ID
parameter ID_BASE = 0,
// Statistics counter update period (cycles)
parameter UPDATE_PERIOD = 1024,
// Enable strings
parameter logic STR_EN = 1'b0,
// Common prefix string (8 characters)
parameter logic [8*8-1:0] PREFIX_STR = "BLK"
)
(
input wire logic clk,
input wire logic rst,
/*
* Increment inputs
*/
input wire logic [INC_W-1:0] stat_inc[CNT],
input wire logic stat_valid[CNT] = '{CNT{1'b1}},
input wire logic [8*8-1:0] stat_str[CNT] = '{CNT{'0}},
/*
* Statistics increment output
*/
taxi_axis_if.src m_axis_stat,
/*
* Control inputs
*/
input wire logic gate = 1'b1,
input wire logic update = 1'b0
);
// check configuration
if (STR_EN) begin
if (!m_axis_stat.USER_EN)
$fatal(0, "Error: statistics strings requires tuser (instance %m)");
if (m_axis_stat.DATA_W < 16)
$fatal(0, "Error: statistics strings requires tdata width of at least 16 (instance %m)");
end
if (ID_BASE+CNT > 2**m_axis_stat.ID_W)
$fatal(0, "Error: insufficient tid width (instance %m)");
localparam STAT_INC_W = m_axis_stat.DATA_W;
localparam STAT_ID_W = m_axis_stat.ID_W;
localparam CNT_W = $clog2(CNT);
localparam PERIOD_CNT_W = $clog2(UPDATE_PERIOD+1);
localparam ACC_W = INC_W+CNT_W+1;
localparam [0:0]
STATE_READ = 1'd0,
STATE_WRITE = 1'd1;
logic [0:0] state_reg = STATE_READ, state_next;
logic [STAT_INC_W-1:0] m_axis_stat_tdata_reg = '0, m_axis_stat_tdata_next;
logic [STAT_ID_W-1:0] m_axis_stat_tid_reg = '0, m_axis_stat_tid_next;
logic m_axis_stat_tvalid_reg = 1'b0, m_axis_stat_tvalid_next;
logic m_axis_stat_tuser_reg = 1'b0, m_axis_stat_tuser_next;
logic [CNT_W-1:0] count_reg = '0, count_next;
logic [PERIOD_CNT_W-1:0] update_period_reg = PERIOD_CNT_W'(UPDATE_PERIOD), update_period_next;
logic zero_reg = 1'b1, zero_next;
logic update_req_reg = 1'b0, update_req_next;
logic update_reg = 1'b0, update_next;
logic [CNT-1:0] update_shift_reg = '0, update_shift_next;
logic [ACC_W-1:0] ch_reg = '0, ch_next;
function automatic [8*6-1:0] pack_str(logic [8*8-1:0] str);
// determine length
integer j = 0;
for (integer i = 0; i < 8; i = i + 1) begin
if (str[i*8 +: 8] != 0) begin
j = i;
end
end
// convert to 6 bit and pack
pack_str = '0;
for (integer i = 0; i < 8 && i <= j; i = i + 1) begin
pack_str[i*6 +: 6] = {str[8*(j-i) + 6], str[8*(j-i) +: 5]};
end
endfunction
logic [3:0][11:0] prefix_str_rom = pack_str(PREFIX_STR);
logic [CNT*4-1:0][11:0] str_rom;
logic [CNT_W-1:0] str_sel_reg = '0, str_sel_next;
logic str_prfx_reg = 1'b0, str_prfx_next;
logic [1:0] str_ptr_reg = '0, str_ptr_next;
logic str_update_reg = 1'b0, str_update_next;
for (genvar n = 0; n < CNT; n = n + 1) begin
assign str_rom[n*4 +: 4] = pack_str(stat_str[n]);
end
wire [ACC_W-1:0] acc_int[CNT];
logic [CNT-1:0] acc_clear;
(* ram_style = "distributed", ramstyle = "no_rw_check, mlab" *)
logic [STAT_INC_W-1:0] mem_reg[CNT];
logic [STAT_INC_W-1:0] mem_rd_data_reg = '0;
logic mem_rd_en;
logic mem_wr_en;
logic [STAT_INC_W-1:0] mem_wr_data;
assign m_axis_stat.tdata = m_axis_stat_tdata_reg;
assign m_axis_stat.tkeep = 1'b1;
assign m_axis_stat.tstrb = m_axis_stat.tkeep;
assign m_axis_stat.tvalid = m_axis_stat_tvalid_reg;
assign m_axis_stat.tlast = 1'b1;
assign m_axis_stat.tid = m_axis_stat_tid_reg;
assign m_axis_stat.tdest = '0;
assign m_axis_stat.tuser = STR_EN ? m_axis_stat_tuser_reg : '0;
for (genvar n = 0; n < CNT; n = n + 1) begin : ch
logic [ACC_W-1:0] acc_reg = '0;
assign acc_int[n] = acc_reg;
always_ff @(posedge clk) begin
if (acc_clear[n]) begin
if (stat_valid[n] && gate) begin
acc_reg <= ACC_W'(stat_inc[n]);
end else begin
acc_reg <= '0;
end
end else begin
if (stat_valid[n] && gate) begin
acc_reg <= acc_reg + ACC_W'(stat_inc[n]);
end
end
if (rst) begin
acc_reg <= '0;
end
end
end
always_comb begin
state_next = STATE_READ;
m_axis_stat_tdata_next = m_axis_stat_tdata_reg;
m_axis_stat_tid_next = m_axis_stat_tid_reg;
m_axis_stat_tvalid_next = m_axis_stat_tvalid_reg && !m_axis_stat.tready;
m_axis_stat_tuser_next = m_axis_stat_tuser_reg;
count_next = count_reg;
update_period_next = update_period_reg;
zero_next = zero_reg;
update_req_next = update_req_reg;
update_next = update_reg;
update_shift_next = update_shift_reg;
ch_next = ch_reg;
str_sel_next = str_sel_reg;
str_prfx_next = str_prfx_reg;
str_ptr_next = str_ptr_reg;
str_update_next = str_update_reg;
acc_clear = '0;
mem_rd_en = 1'b0;
mem_wr_en = 1'b0;
mem_wr_data = mem_rd_data_reg + STAT_INC_W'(ch_reg);
if (!m_axis_stat_tvalid_reg) begin
m_axis_stat_tdata_next = mem_rd_data_reg + STAT_INC_W'(ch_reg);
m_axis_stat_tid_next = STAT_ID_W'(count_reg+ID_BASE);
end
case (state_reg)
STATE_READ: begin
acc_clear[count_reg] = 1'b1;
ch_next = acc_int[count_reg];
mem_rd_en = 1'b1;
state_next = STATE_WRITE;
end
STATE_WRITE: begin
mem_wr_en = 1'b1;
update_shift_next = {update_reg || update_shift_reg[0], update_shift_reg[CNT-1:1]};
if (zero_reg) begin
mem_wr_data = STAT_INC_W'(ch_reg);
end else if (!m_axis_stat_tvalid_reg && (update_reg || update_shift_reg[0])) begin
update_shift_next[CNT-1] = 1'b0;
mem_wr_data = '0;
m_axis_stat_tdata_next = mem_rd_data_reg + STAT_INC_W'(ch_reg);
m_axis_stat_tid_next = STAT_ID_W'(count_reg+ID_BASE);
m_axis_stat_tvalid_next = mem_rd_data_reg != 0 || ch_reg != 0;
m_axis_stat_tuser_next = 1'b0;
end else begin
mem_wr_data = mem_rd_data_reg + STAT_INC_W'(ch_reg);
if (STR_EN && !m_axis_stat_tvalid_reg && str_update_reg && count_reg == str_sel_reg) begin
str_update_next = 1'b0;
m_axis_stat_tdata_next[15:4] = str_prfx_reg ? str_rom[{str_sel_reg, str_ptr_reg}] : prefix_str_rom[str_ptr_reg];
m_axis_stat_tdata_next[3:0] = {1'b0, str_prfx_reg, str_ptr_reg};
m_axis_stat_tid_next = STAT_ID_W'(count_reg+ID_BASE);
m_axis_stat_tvalid_next = 1'b1;
m_axis_stat_tuser_next = 1'b1;
str_ptr_next = str_ptr_reg + 1;
if (str_ptr_reg == 2'b11) begin
str_prfx_next = !str_prfx_reg;
if (str_prfx_reg) begin
str_sel_next = str_sel_reg + 1;
end
end
end
end
if (count_reg == CNT_W'(CNT-1)) begin
zero_next = 1'b0;
update_req_next = 1'b0;
update_next = update_req_reg;
if (update_req_reg) begin
str_update_next = 1'b1;
end
count_next = '0;
end else begin
count_next = count_reg + 1;
end
state_next = STATE_READ;
end
endcase
if (update_period_reg == 0 || update) begin
update_req_next = 1'b1;
update_period_next = PERIOD_CNT_W'(UPDATE_PERIOD);
end else begin
update_period_next = update_period_reg - 1;
end
end
always_ff @(posedge clk) begin
state_reg <= state_next;
m_axis_stat_tdata_reg <= m_axis_stat_tdata_next;
m_axis_stat_tid_reg <= m_axis_stat_tid_next;
m_axis_stat_tvalid_reg <= m_axis_stat_tvalid_next;
m_axis_stat_tuser_reg <= m_axis_stat_tuser_next;
count_reg <= count_next;
update_period_reg <= update_period_next;
zero_reg <= zero_next;
update_req_reg <= update_req_next;
update_reg <= update_next;
update_shift_reg <= update_shift_next;
ch_reg <= ch_next;
str_sel_reg <= str_sel_next;
str_prfx_reg <= str_prfx_next;
str_ptr_reg <= str_ptr_next;
str_update_reg <= str_update_next;
if (mem_wr_en) begin
mem_reg[count_reg] <= mem_wr_data;
end else if (mem_rd_en) begin
mem_rd_data_reg <= mem_reg[count_reg];
end
if (rst) begin
state_reg <= STATE_READ;
m_axis_stat_tvalid_reg <= 1'b0;
count_reg <= '0;
update_period_reg <= PERIOD_CNT_W'(UPDATE_PERIOD);
zero_reg <= 1'b1;
update_req_reg <= 1'b0;
update_reg <= 1'b0;
str_sel_reg <= '0;
str_prfx_reg <= 1'b0;
str_ptr_reg <= '0;
str_update_reg <= 1'b0;
end
end
endmodule
`resetall

View File

@@ -0,0 +1,268 @@
// SPDX-License-Identifier: CERN-OHL-S-2.0
/*
Copyright (c) 2021-2025 FPGA Ninja, LLC
Authors:
- Alex Forencich
*/
`resetall
`timescale 1ns / 1ps
`default_nettype none
/*
* Statistics counter
*/
module taxi_stats_counter #
(
// Statistics counter (bits)
parameter STAT_COUNT_W = 32,
// Pipeline length
parameter PIPELINE = 2
)
(
input wire logic clk,
input wire logic rst,
/*
* Statistics increment input
*/
taxi_axis_if.snk s_axis_stat,
/*
* AXI Lite register interface
*/
taxi_axil_if.wr_slv s_axil_wr,
taxi_axil_if.rd_slv s_axil_rd
);
localparam STAT_INC_W = s_axis_stat.DATA_W;
localparam STAT_ID_W = s_axis_stat.ID_W;
localparam AXIL_ADDR_W = s_axil_rd.ADDR_W;
localparam AXIL_DATA_W = s_axil_rd.DATA_W;
localparam ID_SHIFT = $clog2(((AXIL_DATA_W > STAT_COUNT_W ? AXIL_DATA_W : STAT_COUNT_W)+7)/8);
localparam WORD_SELECT_SHIFT = $clog2(AXIL_DATA_W/8);
localparam WORD_SELECT_W = STAT_COUNT_W > AXIL_DATA_W ? $clog2((STAT_COUNT_W+7)/8) - $clog2(AXIL_DATA_W/8) : 1;
// check configuration
if (AXIL_ADDR_W < STAT_ID_W+ID_SHIFT)
$fatal(0, "Error: AXI lite address width too narrow (instance %m)");
if (PIPELINE < 2)
$fatal(0, "Error: PIPELINE must be at least 2 (instance %m)");
logic init_reg = 1'b1, init_next;
logic [STAT_ID_W-1:0] init_ptr_reg = 0, init_ptr_next;
logic op_acc_pipe_hazard;
logic stage_active;
logic [PIPELINE-1:0] op_axil_read_pipe_reg = 0, op_axil_read_pipe_next;
logic [PIPELINE-1:0] op_acc_pipe_reg = 0, op_acc_pipe_next;
logic [STAT_ID_W-1:0] mem_addr_pipeline_reg[PIPELINE], mem_addr_pipeline_next[PIPELINE];
logic [WORD_SELECT_W-1:0] axil_shift_pipeline_reg[PIPELINE], axil_shift_pipeline_next[PIPELINE];
logic [STAT_INC_W-1:0] stat_inc_pipeline_reg[PIPELINE], stat_inc_pipeline_next[PIPELINE];
logic s_axis_stat_tready_reg = 1'b0, s_axis_stat_tready_next;
logic s_axil_awready_reg = 0, s_axil_awready_next;
logic s_axil_wready_reg = 0, s_axil_wready_next;
logic s_axil_bvalid_reg = 0, s_axil_bvalid_next;
logic s_axil_arready_reg = 0, s_axil_arready_next;
logic [AXIL_DATA_W-1:0] s_axil_rdata_reg = 0, s_axil_rdata_next;
logic s_axil_rvalid_reg = 0, s_axil_rvalid_next;
(* ramstyle = "no_rw_check" *)
logic [STAT_COUNT_W-1:0] mem[2**STAT_ID_W];
logic [STAT_ID_W-1:0] mem_rd_addr;
logic [STAT_ID_W-1:0] mem_wr_addr;
logic [STAT_COUNT_W-1:0] mem_wr_data;
logic mem_wr_en;
logic [STAT_COUNT_W-1:0] mem_read_data_reg = 0;
logic [STAT_COUNT_W-1:0] mem_read_data_pipeline_reg[PIPELINE-1:1];
assign s_axis_stat.tready = s_axis_stat_tready_reg;
assign s_axil_wr.awready = s_axil_awready_reg;
assign s_axil_wr.wready = s_axil_wready_reg;
assign s_axil_wr.bresp = 2'b00;
assign s_axil_wr.bvalid = s_axil_bvalid_reg;
assign s_axil_rd.arready = s_axil_arready_reg;
assign s_axil_rd.rdata = s_axil_rdata_reg;
assign s_axil_rd.rresp = 2'b00;
assign s_axil_rd.rvalid = s_axil_rvalid_reg;
wire [STAT_ID_W-1:0] s_axil_araddr_id = STAT_ID_W'(s_axil_rd.araddr >> ID_SHIFT);
wire [WORD_SELECT_W-1:0] s_axil_araddr_shift = WORD_SELECT_W'(s_axil_rd.araddr >> WORD_SELECT_SHIFT);
initial begin
// break up loop to work around iteration termination
for (integer i = 0; i < 2**STAT_ID_W; i = i + 2**(STAT_ID_W/2)) begin
for (integer j = i; j < i + 2**(STAT_ID_W/2); j = j + 1) begin
mem[j] = 0;
end
end
for (integer i = 0; i < PIPELINE; i = i + 1) begin
mem_addr_pipeline_reg[i] = 0;
axil_shift_pipeline_reg[i] = 0;
stat_inc_pipeline_reg[i] = 0;
end
end
always_comb begin
init_next = init_reg;
init_ptr_next = init_ptr_reg;
op_axil_read_pipe_next = PIPELINE'({op_axil_read_pipe_reg, 1'b0});
op_acc_pipe_next = PIPELINE'({op_acc_pipe_reg, 1'b0});
mem_addr_pipeline_next[0] = 0;
axil_shift_pipeline_next[0] = 0;
stat_inc_pipeline_next[0] = 0;
for (integer j = 1; j < PIPELINE; j = j + 1) begin
mem_addr_pipeline_next[j] = mem_addr_pipeline_reg[j-1];
axil_shift_pipeline_next[j] = axil_shift_pipeline_reg[j-1];
stat_inc_pipeline_next[j] = stat_inc_pipeline_reg[j-1];
end
s_axis_stat_tready_next = 1'b0;
s_axil_awready_next = 1'b0;
s_axil_wready_next = 1'b0;
s_axil_bvalid_next = s_axil_bvalid_reg && !s_axil_wr.bready;
s_axil_arready_next = 1'b0;
s_axil_rdata_next = s_axil_rdata_reg;
s_axil_rvalid_next = s_axil_rvalid_reg && !s_axil_rd.rready;
mem_rd_addr = 0;
mem_wr_addr = mem_addr_pipeline_reg[PIPELINE-1];
mem_wr_data = mem_read_data_pipeline_reg[PIPELINE-1] + STAT_COUNT_W'(stat_inc_pipeline_reg[PIPELINE-1]);
mem_wr_en = 0;
op_acc_pipe_hazard = 1'b0;
stage_active = 1'b0;
for (integer j = 0; j < PIPELINE; j = j + 1) begin
stage_active = op_axil_read_pipe_reg[j] || op_acc_pipe_reg[j];
op_acc_pipe_hazard = op_acc_pipe_hazard || (stage_active && mem_addr_pipeline_reg[j] == s_axis_stat.tid);
end
// discard writes
if (s_axil_wr.awvalid && s_axil_wr.wvalid && (!s_axil_wr.bvalid || s_axil_wr.bready) && (!s_axil_wr.awready && !s_axil_wr.wready)) begin
s_axil_awready_next = 1'b1;
s_axil_wready_next = 1'b1;
s_axil_bvalid_next = 1'b1;
end
// pipeline stage 0 - accept request
if (init_reg) begin
// zero all counters
init_ptr_next = init_ptr_reg + 1;
mem_wr_addr = init_ptr_reg;
mem_wr_data = 0;
mem_wr_en = 1'b1;
if (&init_ptr_reg) begin
init_next = 1'b0;
end
end else if (s_axil_rd.arvalid && (!s_axil_rd.rvalid || s_axil_rd.rready) && op_axil_read_pipe_reg == 0) begin
// AXIL read
op_axil_read_pipe_next[0] = 1'b1;
s_axil_arready_next = 1'b1;
mem_rd_addr = s_axil_araddr_id;
mem_addr_pipeline_next[0] = s_axil_araddr_id;
axil_shift_pipeline_next[0] = s_axil_araddr_shift;
end else if (s_axis_stat.tvalid && !s_axis_stat.tready && !op_acc_pipe_hazard) begin
// accumulate
op_acc_pipe_next[0] = !s_axis_stat.USER_EN || !s_axis_stat.tuser;
s_axis_stat_tready_next = 1'b1;
stat_inc_pipeline_next[0] = s_axis_stat.tdata;
mem_rd_addr = s_axis_stat.tid;
mem_addr_pipeline_next[0] = s_axis_stat.tid;
end
// read complete, perform operation
if (op_acc_pipe_reg[PIPELINE-1]) begin
// accumulate
mem_wr_addr = mem_addr_pipeline_reg[PIPELINE-1];
mem_wr_data = mem_read_data_pipeline_reg[PIPELINE-1] + STAT_COUNT_W'(stat_inc_pipeline_reg[PIPELINE-1]);
mem_wr_en = 1'b1;
end else if (op_axil_read_pipe_reg[PIPELINE-1]) begin
// AXIL read
s_axil_rvalid_next = 1'b1;
s_axil_rdata_next = 0;
if (STAT_COUNT_W > AXIL_DATA_W) begin
s_axil_rdata_next = AXIL_DATA_W'(mem_read_data_pipeline_reg[PIPELINE-1] >> axil_shift_pipeline_reg[PIPELINE-1]*AXIL_DATA_W);
end else begin
s_axil_rdata_next = AXIL_DATA_W'(mem_read_data_pipeline_reg[PIPELINE-1]);
end
end
end
always_ff @(posedge clk) begin
init_reg <= init_next;
init_ptr_reg <= init_ptr_next;
op_axil_read_pipe_reg <= op_axil_read_pipe_next;
op_acc_pipe_reg <= op_acc_pipe_next;
s_axis_stat_tready_reg <= s_axis_stat_tready_next;
s_axil_awready_reg <= s_axil_awready_next;
s_axil_wready_reg <= s_axil_wready_next;
s_axil_bvalid_reg <= s_axil_bvalid_next;
s_axil_arready_reg <= s_axil_arready_next;
s_axil_rdata_reg <= s_axil_rdata_next;
s_axil_rvalid_reg <= s_axil_rvalid_next;
for (integer i = 0; i < PIPELINE; i = i + 1) begin
mem_addr_pipeline_reg[i] <= mem_addr_pipeline_next[i];
axil_shift_pipeline_reg[i] <= axil_shift_pipeline_next[i];
stat_inc_pipeline_reg[i] <= stat_inc_pipeline_next[i];
end
if (mem_wr_en) begin
mem[mem_wr_addr] <= mem_wr_data;
end
mem_read_data_reg <= mem[mem_rd_addr];
mem_read_data_pipeline_reg[1] <= mem_read_data_reg;
for (integer i = 2; i < PIPELINE; i = i + 1) begin
mem_read_data_pipeline_reg[i] <= mem_read_data_pipeline_reg[i-1];
end
if (rst) begin
init_reg <= 1'b1;
init_ptr_reg <= 0;
op_axil_read_pipe_reg <= 0;
op_acc_pipe_reg <= 0;
s_axis_stat_tready_reg <= 1'b0;
s_axil_awready_reg <= 1'b0;
s_axil_wready_reg <= 1'b0;
s_axil_bvalid_reg <= 1'b0;
s_axil_arready_reg <= 1'b0;
s_axil_rvalid_reg <= 1'b0;
end
end
endmodule
`resetall

View File

@@ -0,0 +1,241 @@
// 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 strings collector (full)
*/
module taxi_stats_strings_full #
(
// Pipeline length
parameter PIPELINE = 2
)
(
input wire logic clk,
input wire logic rst,
/*
* Statistics increment input
*/
taxi_axis_if.mon s_axis_stat,
/*
* AXI Lite register interface
*/
taxi_axil_if.wr_slv s_axil_wr,
taxi_axil_if.rd_slv s_axil_rd
);
// localparam STAT_INC_W = s_axis_stat.DATA_W;
localparam STAT_ID_W = s_axis_stat.ID_W;
localparam AXIL_ADDR_W = s_axil_rd.ADDR_W;
localparam AXIL_DATA_W = s_axil_rd.DATA_W;
localparam ID_SHIFT = $clog2(((AXIL_DATA_W > 128 ? AXIL_DATA_W : 128)+7)/8);
localparam WORD_SELECT_SHIFT = $clog2(AXIL_DATA_W/8);
localparam WORD_SELECT_W = 128 > AXIL_DATA_W ? $clog2((128+7)/8) - $clog2(AXIL_DATA_W/8) : 1;
// check configuration
if (AXIL_ADDR_W < STAT_ID_W+ID_SHIFT)
$fatal(0, "Error: AXI lite address width too narrow (instance %m)");
if (PIPELINE < 2)
$fatal(0, "Error: PIPELINE must be at least 2 (instance %m)");
logic init_reg = 1'b1, init_next;
logic [STAT_ID_W-1:0] init_ptr_reg = 0, init_ptr_next;
logic op_acc_pipe_hazard;
logic stage_active;
logic [PIPELINE-1:0] op_axil_read_pipe_reg = 0, op_axil_read_pipe_next;
logic [STAT_ID_W-1:0] mem_addr_pipeline_reg[PIPELINE], mem_addr_pipeline_next[PIPELINE];
logic [WORD_SELECT_W-1:0] axil_shift_pipeline_reg[PIPELINE], axil_shift_pipeline_next[PIPELINE];
logic s_axil_awready_reg = 0, s_axil_awready_next;
logic s_axil_wready_reg = 0, s_axil_wready_next;
logic s_axil_bvalid_reg = 0, s_axil_bvalid_next;
logic s_axil_arready_reg = 0, s_axil_arready_next;
logic [AXIL_DATA_W-1:0] s_axil_rdata_reg = 0, s_axil_rdata_next;
logic s_axil_rvalid_reg = 0, s_axil_rvalid_next;
(* ramstyle = "no_rw_check" *)
logic [127:0] mem[2**STAT_ID_W];
logic [STAT_ID_W-1:0] mem_rd_addr;
logic [STAT_ID_W-1:0] mem_wr_addr;
logic [127:0] mem_wr_data;
logic [15:0] mem_wr_strb;
logic mem_wr_en;
logic [127:0] mem_read_data_reg = 0;
logic [127:0] mem_read_data_pipeline_reg[PIPELINE-1:1];
assign s_axil_wr.awready = s_axil_awready_reg;
assign s_axil_wr.wready = s_axil_wready_reg;
assign s_axil_wr.bresp = 2'b00;
assign s_axil_wr.bvalid = s_axil_bvalid_reg;
assign s_axil_rd.arready = s_axil_arready_reg;
assign s_axil_rd.rdata = s_axil_rdata_reg;
assign s_axil_rd.rresp = 2'b00;
assign s_axil_rd.rvalid = s_axil_rvalid_reg;
wire [STAT_ID_W-1:0] s_axil_araddr_id = STAT_ID_W'(s_axil_rd.araddr >> ID_SHIFT);
wire [WORD_SELECT_W-1:0] s_axil_araddr_shift = WORD_SELECT_W'(s_axil_rd.araddr >> WORD_SELECT_SHIFT);
initial begin
// break up loop to work around iteration termination
for (integer i = 0; i < 2**STAT_ID_W; i = i + 2**(STAT_ID_W/2)) begin
for (integer j = i; j < i + 2**(STAT_ID_W/2); j = j + 1) begin
mem[j] = 0;
end
end
for (integer i = 0; i < PIPELINE; i = i + 1) begin
mem_addr_pipeline_reg[i] = 0;
axil_shift_pipeline_reg[i] = 0;
end
end
always_comb begin
init_next = init_reg;
init_ptr_next = init_ptr_reg;
op_axil_read_pipe_next = PIPELINE'({op_axil_read_pipe_reg, 1'b0});
mem_addr_pipeline_next[0] = 0;
axil_shift_pipeline_next[0] = 0;
for (integer j = 1; j < PIPELINE; j = j + 1) begin
mem_addr_pipeline_next[j] = mem_addr_pipeline_reg[j-1];
axil_shift_pipeline_next[j] = axil_shift_pipeline_reg[j-1];
end
s_axil_awready_next = 1'b0;
s_axil_wready_next = 1'b0;
s_axil_bvalid_next = s_axil_bvalid_reg && !s_axil_wr.bready;
s_axil_arready_next = 1'b0;
s_axil_rdata_next = s_axil_rdata_reg;
s_axil_rvalid_next = s_axil_rvalid_reg && !s_axil_rd.rready;
mem_rd_addr = s_axil_araddr_id;
mem_wr_addr = s_axis_stat.tid;
mem_wr_data = {8{1'b0, s_axis_stat.tdata[15], ~s_axis_stat.tdata[15], s_axis_stat.tdata[14:10], 1'b0, s_axis_stat.tdata[9], ~s_axis_stat.tdata[9], s_axis_stat.tdata[8:4]}};
mem_wr_strb = '0;
mem_wr_strb[s_axis_stat.tdata[2:0]*2 +: 2] = 2'b11;
mem_wr_en = 0;
// discard writes
if (s_axil_wr.awvalid && s_axil_wr.wvalid && (!s_axil_wr.bvalid || s_axil_wr.bready) && (!s_axil_wr.awready && !s_axil_wr.wready)) begin
s_axil_awready_next = 1'b1;
s_axil_wready_next = 1'b1;
s_axil_bvalid_next = 1'b1;
end
// store string data
if (init_reg) begin
// zero strings
init_ptr_next = init_ptr_reg + 1;
mem_wr_addr = init_ptr_reg;
mem_wr_data = '0;
mem_wr_strb = '1;
mem_wr_en = 1'b1;
if (&init_ptr_reg) begin
init_next = 1'b0;
end
end else if (s_axis_stat.tvalid && s_axis_stat.tready && s_axis_stat.tuser) begin
// store string data
mem_wr_addr = s_axis_stat.tid;
mem_wr_data = {8{1'b0, s_axis_stat.tdata[15], ~s_axis_stat.tdata[15], s_axis_stat.tdata[14:10], 1'b0, s_axis_stat.tdata[9], ~s_axis_stat.tdata[9], s_axis_stat.tdata[8:4]}};
mem_wr_strb[s_axis_stat.tdata[2:0]*2 +: 2] = 2'b11;
mem_wr_en = 1'b1;
end
// pipeline stage 0 - accept request
if (s_axil_rd.arvalid && (!s_axil_rd.rvalid || s_axil_rd.rready) && op_axil_read_pipe_reg == 0) begin
// AXIL read
op_axil_read_pipe_next[0] = 1'b1;
s_axil_arready_next = 1'b1;
mem_rd_addr = s_axil_araddr_id;
mem_addr_pipeline_next[0] = s_axil_araddr_id;
axil_shift_pipeline_next[0] = s_axil_araddr_shift;
end
// read complete, perform operation
if (op_axil_read_pipe_reg[PIPELINE-1]) begin
// AXIL read
s_axil_rvalid_next = 1'b1;
s_axil_rdata_next = 0;
if (128 > AXIL_DATA_W) begin
s_axil_rdata_next = AXIL_DATA_W'(mem_read_data_pipeline_reg[PIPELINE-1] >> axil_shift_pipeline_reg[PIPELINE-1]*AXIL_DATA_W);
end else begin
s_axil_rdata_next = AXIL_DATA_W'(mem_read_data_pipeline_reg[PIPELINE-1]);
end
end
end
always_ff @(posedge clk) begin
init_reg <= init_next;
init_ptr_reg <= init_ptr_next;
op_axil_read_pipe_reg <= op_axil_read_pipe_next;
s_axil_awready_reg <= s_axil_awready_next;
s_axil_wready_reg <= s_axil_wready_next;
s_axil_bvalid_reg <= s_axil_bvalid_next;
s_axil_arready_reg <= s_axil_arready_next;
s_axil_rdata_reg <= s_axil_rdata_next;
s_axil_rvalid_reg <= s_axil_rvalid_next;
for (integer i = 0; i < PIPELINE; i = i + 1) begin
mem_addr_pipeline_reg[i] <= mem_addr_pipeline_next[i];
axil_shift_pipeline_reg[i] <= axil_shift_pipeline_next[i];
end
if (mem_wr_en) begin
for (integer i = 0; i < 16; i = i + 1) begin
if (mem_wr_strb[i]) begin
mem[mem_wr_addr][i*8 +: 8] <= mem_wr_data[i*8 +: 8];
end
end
end
mem_read_data_reg <= mem[mem_rd_addr];
mem_read_data_pipeline_reg[1] <= mem_read_data_reg;
for (integer i = 2; i < PIPELINE; i = i + 1) begin
mem_read_data_pipeline_reg[i] <= mem_read_data_pipeline_reg[i-1];
end
if (rst) begin
init_reg <= 1'b1;
init_ptr_reg <= 0;
op_axil_read_pipe_reg <= 0;
s_axil_awready_reg <= 1'b0;
s_axil_wready_reg <= 1'b0;
s_axil_bvalid_reg <= 1'b0;
s_axil_arready_reg <= 1'b0;
s_axil_rvalid_reg <= 1'b0;
end
end
endmodule
`resetall

View File

@@ -0,0 +1,58 @@
# 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
RTL_DIR = ../../rtl
LIB_DIR = ../../lib
TAXI_SRC_DIR = $(LIB_DIR)/taxi/src
DUT = taxi_stats_collect
COCOTB_TEST_MODULES = test_$(DUT)
COCOTB_TOPLEVEL = test_$(DUT)
MODULE = $(COCOTB_TEST_MODULES)
TOPLEVEL = $(COCOTB_TOPLEVEL)
VERILOG_SOURCES += $(COCOTB_TOPLEVEL).sv
VERILOG_SOURCES += $(RTL_DIR)/$(DUT).sv
VERILOG_SOURCES += $(TAXI_SRC_DIR)/axis/rtl/taxi_axis_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_CNT := 8
export PARAM_INC_W := 8
export PARAM_ID_BASE := 0
export PARAM_UPDATE_PERIOD := 128
export PARAM_STR_EN := 1
export PARAM_PREFIX_STR := "\"BLK\""
export PARAM_STAT_INC_W := 16
export PARAM_STAT_ID_W := $(shell python -c "print(($(PARAM_CNT)-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,351 @@
#!/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 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 AxiStreamBus, AxiStreamSink
def str2int(s):
return int.from_bytes(s.encode('utf-8'), 'big')
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_sink = AxiStreamSink(AxiStreamBus.from_entity(dut.m_axis_stat), dut.clk, dut.rst)
for k in range(len(dut.stat_inc)):
dut.stat_inc[k].setimmediatevalue(0)
dut.stat_valid[k].setimmediatevalue(0)
dut.stat_str[k].setimmediatevalue(str2int(f"STR_{k}"))
dut.gate.setimmediatevalue(1)
dut.update.setimmediatevalue(0)
def set_backpressure_generator(self, generator=None):
if generator:
self.stat_sink.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, backpressure_inserter=None):
tb = TB(dut)
stat_count = len(dut.stat_valid)
await tb.cycle_reset()
tb.set_backpressure_generator(backpressure_inserter)
for n in range(10):
await RisingEdge(dut.clk)
dut.stat_inc.value = [k for k in range(stat_count)]
dut.stat_valid.value = [1]*stat_count
await RisingEdge(dut.clk)
dut.stat_inc.value = [0]*stat_count
dut.stat_valid.value = [0]*stat_count
await Timer(1000, 'ns')
await Timer(1000, 'ns')
data = [0]*stat_count
while not tb.stat_sink.empty():
stat = await tb.stat_sink.recv()
if not stat.tuser:
assert stat.tdata[0] != 0
data[stat.tid] += stat.tdata[0]
print(data)
for n in range(stat_count):
assert data[n] == n*10
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
async def run_test_max(dut, backpressure_inserter=None):
tb = TB(dut)
stat_count = len(dut.stat_valid)
stat_inc_width = len(dut.stat_inc[0])
await tb.cycle_reset()
tb.set_backpressure_generator(backpressure_inserter)
dut.stat_inc.value = [2**stat_inc_width-1 for k in range(stat_count)]
dut.stat_valid.value = [1]*stat_count
for k in range(2048):
await RisingEdge(dut.clk)
dut.stat_inc.value = [0]*stat_count
dut.stat_valid.value = [0]*stat_count
await Timer(1000, 'ns')
data = [0]*stat_count
while not tb.stat_sink.empty():
stat = await tb.stat_sink.recv()
if not stat.tuser:
assert stat.tdata[0] != 0
data[stat.tid] += stat.tdata[0]
print(data)
for n in range(stat_count):
assert data[n] == 2048*(2**stat_inc_width-1)
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
async def run_test_str(dut, backpressure_inserter=None):
tb = TB(dut)
stat_count = len(dut.stat_valid)
await tb.cycle_reset()
tb.set_backpressure_generator(backpressure_inserter)
strings = [bytearray() for x in range(stat_count)]
done_cnt = 0
while done_cnt < stat_count:
stat = await tb.stat_sink.recv()
print(stat)
val = stat.tdata[0]
index = stat.tid
ptr = (val & 0x7)*2
b = bytearray()
for k in range(2):
c = (val >> (k*6 + 4)) & 0x3f
if c & 0x20:
c = (c & 0x1f) | 0x40
else:
c = (c & 0x1f) | 0x20
b.append(c)
if len(strings[index]) == ptr:
strings[index].extend(b)
if ptr == 14:
done_cnt += 1
print(strings)
for i, s in enumerate(strings):
s = (s[0:8].strip() + b"." + s[8:].strip()).decode('ascii')
print(s)
assert s == f'BLK.STR_{i}'
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
async def run_stress_test(dut, backpressure_inserter=None):
tb = TB(dut)
stat_count = len(dut.stat_valid)
stat_inc_width = len(dut.stat_inc[0])
await tb.cycle_reset()
tb.set_backpressure_generator(backpressure_inserter)
async def worker(num, queue_ref, queue_drive, count=1024):
for k in range(count):
count = random.randrange(1, 2**stat_inc_width)
await queue_drive.put(count)
await queue_ref.put((num, count))
await Timer(random.randint(1, 100), 'ns')
workers = []
queue_ref = Queue()
queue_drive = [Queue() for k in range(stat_count)]
for k in range(stat_count):
workers.append(cocotb.start_soon(worker(k, queue_ref, queue_drive[k], count=1024)))
async def driver(dut, queues):
while True:
await RisingEdge(dut.clk)
inc = [0]*stat_count
valid = [0]*stat_count
for num, queue in enumerate(queues):
if not queue.empty():
count = await queue.get()
inc[num] += count
valid[num] = 1
dut.stat_inc.value = inc
dut.stat_valid.value = valid
driver = cocotb.start_soon(driver(dut, queue_drive))
while workers:
await workers.pop(0).join()
await Timer(1000, 'ns')
driver.kill()
await Timer(1000, 'ns')
data_ref = [0]*stat_count
while not queue_ref.empty():
num, count = await queue_ref.get()
data_ref[num] += count
print(data_ref)
data = [0]*stat_count
while not tb.stat_sink.empty():
stat = await tb.stat_sink.recv()
if not stat.tuser:
assert stat.tdata[0] != 0
data[stat.tid] += stat.tdata[0]
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,
run_test_max,
run_test_str,
run_stress_test,
]:
factory = TestFactory(test)
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'))
lib_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'lib'))
taxi_src_dir = os.path.abspath(os.path.join(lib_dir, 'taxi', 'src'))
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())
def test_taxi_stats_collect(request):
dut = "taxi_stats_collect"
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, f"{dut}.sv"),
os.path.join(taxi_src_dir, "axis", "rtl", "taxi_axis_if.sv"),
]
verilog_sources = process_f_files(verilog_sources)
parameters = {}
parameters['CNT'] = 8
parameters['INC_W'] = 8
parameters['ID_BASE'] = 0
parameters['UPDATE_PERIOD'] = 128
parameters['STR_EN'] = 1
parameters['PREFIX_STR'] = "\"BLK\""
parameters['STAT_INC_W'] = 16
parameters['STAT_ID_W'] = (parameters['CNT']-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,86 @@
// 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 collector testbench
*/
module test_taxi_stats_collect #
(
/* verilator lint_off WIDTHTRUNC */
parameter CNT = 8,
parameter INC_W = 8,
parameter ID_BASE = 0,
parameter UPDATE_PERIOD = 128,
parameter logic STR_EN = 1'b1,
parameter logic [8*8-1:0] PREFIX_STR = "BLK",
parameter STAT_INC_W = 16,
parameter STAT_ID_W = $clog2(CNT)
/* verilator lint_on WIDTHTRUNC */
)
();
logic clk;
logic rst;
logic [INC_W-1:0] stat_inc[CNT];
logic [0:0] stat_valid[CNT];
logic [8*8-1:0] stat_str[CNT];
taxi_axis_if #(
.DATA_W(STAT_INC_W),
.KEEP_EN(0),
.KEEP_W(1),
.ID_EN(1),
.ID_W(STAT_ID_W),
.USER_EN(1),
.USER_W(1)
) m_axis_stat();
logic gate;
logic update;
taxi_stats_collect #(
.CNT(CNT),
.INC_W(INC_W),
.ID_BASE(ID_BASE),
.UPDATE_PERIOD(UPDATE_PERIOD),
.STR_EN(STR_EN),
.PREFIX_STR(PREFIX_STR)
)
uut (
.clk(clk),
.rst(rst),
/*
* Increment inputs
*/
.stat_inc(stat_inc),
.stat_valid(stat_valid),
.stat_str(stat_str),
/*
* Statistics increment output
*/
.m_axis_stat(m_axis_stat),
/*
* Control inputs
*/
.gate(gate),
.update(update)
);
endmodule
`resetall

View File

@@ -0,0 +1,57 @@
# 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
RTL_DIR = ../../rtl
LIB_DIR = ../../lib
TAXI_SRC_DIR = $(LIB_DIR)/taxi/src
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_DIR)/$(DUT).sv
VERILOG_SOURCES += $(TAXI_SRC_DIR)/axis/rtl/taxi_axis_if.sv
VERILOG_SOURCES += $(TAXI_SRC_DIR)/axi/rtl/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,230 @@
#!/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'))
lib_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'lib'))
taxi_src_dir = os.path.abspath(os.path.join(lib_dir, 'taxi', 'src'))
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, f"{dut}.sv"),
os.path.join(taxi_src_dir, "axis", "rtl", "taxi_axis_if.sv"),
os.path.join(taxi_src_dir, "axi", "rtl", "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

View File

@@ -0,0 +1,56 @@
# SPDX-License-Identifier: CERN-OHL-S-2.0
#
# Copyright (c) 2025 FPGA Ninja, LLC
#
# Authors:
# - Alex Forencich
TOPLEVEL_LANG = verilog
SIM ?= verilator
WAVES ?= 0
COCOTB_HDL_TIMEUNIT = 1ns
COCOTB_HDL_TIMEPRECISION = 1ps
RTL_DIR = ../../rtl
LIB_DIR = ../../lib
TAXI_SRC_DIR = $(LIB_DIR)/taxi/src
DUT = taxi_stats_strings_full
COCOTB_TEST_MODULES = test_$(DUT)
COCOTB_TOPLEVEL = test_$(DUT)
MODULE = $(COCOTB_TEST_MODULES)
TOPLEVEL = $(COCOTB_TOPLEVEL)
VERILOG_SOURCES += $(COCOTB_TOPLEVEL).sv
VERILOG_SOURCES += $(RTL_DIR)/$(DUT).sv
VERILOG_SOURCES += $(TAXI_SRC_DIR)/axis/rtl/taxi_axis_if.sv
VERILOG_SOURCES += $(TAXI_SRC_DIR)/axi/rtl/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_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)+4)")
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,183 @@
#!/usr/bin/env python
# SPDX-License-Identifier: CERN-OHL-S-2.0
"""
Copyright (c) 2025 FPGA Ninja, LLC
Authors:
- Alex Forencich
"""
import itertools
import logging
import os
import cocotb_test.simulator
import cocotb
from cocotb.clock import Clock
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_strings(dut, idle_inserter=None, backpressure_inserter=None):
tb = TB(dut)
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):
s1 = f'BLK'
s2 = f'STR_{n}'
s = f'{s1:8}{s2:8}'
print(s)
b = s.encode('ascii')
for k in range(0, 8):
val = k
for m in range(2):
c = b[k*2+m]
c = (c & 0x1f) | (0x20 if c & 0x40 else 0)
val |= c << (4+6*m)
await tb.stat_source.send(AxiStreamFrame([val], tid=n, tuser=1))
await tb.stat_source.send(AxiStreamFrame([0xdead], tid=n, tuser=0))
await Timer(12000, 'ns')
data = await tb.axil_master.read_words(0, 10, ws=16)
print(data)
for i, d in enumerate(data):
s = d.to_bytes(16, 'little')
print(s)
s = (s[0:8].strip() + b"." + s[8:].strip()).decode('ascii')
print(s)
assert s == f'BLK.STR_{i}'
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_strings]:
factory = TestFactory(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'))
lib_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'lib'))
taxi_src_dir = os.path.abspath(os.path.join(lib_dir, 'taxi', 'src'))
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())
def test_taxi_stats_strings_full(request):
dut = "taxi_stats_strings_full"
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, f"{dut}.sv"),
os.path.join(taxi_src_dir, "axis", "rtl", "taxi_axis_if.sv"),
os.path.join(taxi_src_dir, "axi", "rtl", "taxi_axil_if.sv"),
]
verilog_sources = process_f_files(verilog_sources)
parameters = {}
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'] + 4
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_strings_full #
(
/* verilator lint_off WIDTHTRUNC */
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 + 4
/* 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_strings_full #(
.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)
);
assign s_axis_stat.tready = 1'b1;
endmodule
`resetall