bit-string-accelerator / pcie /tb /tb_engine.sv
SNAPKITTYWEST's picture
October 2026 main drop: mirror from GitHub
0ba037b verified
Raw History Blame Contribute Delete
15.1 kB
// Equivalence testbench: the parallel engine (pcie/rtl/bitacc_engine.sv) against
// the single-op reference core (rtl/bit_accelerator.sv).
//
// Both start from the same random memory image and receive the same operation
// stream. The reference core runs the stream one operation at a time, range
// operations expanded into single-bit operations (tb/gold_ops.svh); the
// engine gets it as a stream with random gaps and random result backpressure.
// Every result (error flag; bit and value when there is no error) and every
// word of the final memory must match. Exits non-zero on any mismatch, or if
// a coverage goal (operation classes, out-of-order completion, full reorder
// buffer, range barriers in both directions) was not reached.
//
// +ops=N +seed=N stream length and seed
// -DLANES=n -DWPL=n engine geometry (default 4 lanes x 16 words: many conflicts)
`timescale 1ns/1ps
module tb_engine;
`ifndef LANES
`define LANES 4
`endif
`ifndef WPL
`define WPL 16
`endif
`ifndef MATCH
`define MATCH 1
`endif
`ifndef MLANES
`define MLANES `LANES
`endif
localparam int LANES = `LANES;
localparam int WPL = `WPL;
localparam int WORDS = LANES * WPL;
localparam bit WITH_MATCH = `MATCH;
localparam int MLANES = `MLANES; // MATCH units
localparam int MAXOPS = 20000;
logic clk = 1'b0, reset = 1'b1;
always #5 clk = ~clk;
integer checks = 0, fails = 0;
task automatic check(input bit cond, input string what);
checks++;
if (!cond) begin
fails++;
if (fails <= 20) $display("FAIL @%0t: %s", $time, what);
end
endtask
// ------------------------------------------------------------ stimulus
integer ops = 5000, seed = 1, seed0;
logic [3:0] s_op [MAXOPS];
logic [63:0] s_base[MAXOPS], s_off[MAXOPS];
logic [31:0] s_len [MAXOPS];
logic [23:0] s_src [MAXOPS];
logic [2:0] s_fn [MAXOPS];
logic s_dry [MAXOPS];
logic [63:0] init [WORDS];
// xorshift32, well distributed in every simulator. (Verilator 5.020's
// seeded system random function is degenerate: the seed roughly doubles on
// every call.) Simulators may evaluate operands such as rnd(a) + rnd(b) in
// different orders, so the same seed can give different streams in Icarus
// and Verilator; each is checked the same way.
function automatic logic [31:0] rnd32();
logic [31:0] x;
x = (seed == 0) ? 32'd1 : 32'(seed);
x = x ^ (x << 13); x = x ^ (x >> 17); x = x ^ (x << 5);
seed = x;
rnd32 = x;
endfunction
function automatic integer rnd(input integer n);
rnd = (n <= 1) ? 0 : integer'(rnd32() % 32'(n));
endfunction
// ------------------------------------------------------------ reference: core + memory
logic g_op_valid = 1'b0, g_op_ready;
logic [63:0] g_base = '0, g_off = '0;
logic [2:0] g_opc = '0;
logic g_res_valid, g_res_bit, g_error;
logic g_mem_valid, g_mem_write;
logic [63:0] g_mem_addr, g_mem_wdata;
logic [7:0] g_mem_wstrb;
logic g_mem_rvalid = 1'b0, g_mem_fault = 1'b0;
logic [63:0] g_mem_rdata = '0;
logic [63:0] gmem [WORDS];
bit_accelerator gold (
.clk, .reset,
.op_valid(g_op_valid), .op_ready(g_op_ready),
.base_address(g_base), .bit_offset(g_off), .operation(g_opc),
.result_valid(g_res_valid), .result_bit(g_res_bit), .error(g_error),
.mem_valid(g_mem_valid), .mem_write(g_mem_write), .mem_addr(g_mem_addr),
.mem_wdata(g_mem_wdata), .mem_wstrb(g_mem_wstrb),
.mem_ready(1'b1), .mem_rvalid(g_mem_rvalid), .mem_rdata(g_mem_rdata),
.mem_fault(g_mem_fault));
// Memory slave for the reference: byte addresses 0 .. WORDS*8-1, else fault.
always @(posedge clk) begin
g_mem_rvalid <= 1'b0;
g_mem_fault <= 1'b0;
if (!reset && g_mem_valid) begin
if ((g_mem_addr >> 3) >= WORDS) g_mem_fault <= 1'b1;
else if (g_mem_write) gmem[g_mem_addr >> 3] <= g_mem_wdata;
else begin g_mem_rvalid <= 1'b1; g_mem_rdata <= gmem[g_mem_addr >> 3]; end
end
end
logic g_err_r [MAXOPS];
logic g_bit_r [MAXOPS];
logic [55:0] g_val_r [MAXOPS];
bit gold_done = 0;
`include "gold_ops.svh"
`include "range_stim.svh"
task automatic run_gold();
logic e, b;
logic [55:0] v;
for (int n = 0; n < ops; n++) begin
if (s_op[n] < 4'd8) begin
gold_op(s_base[n], s_off[n], s_op[n][2:0], e, b);
v = '0;
end else
gold_range(s_op[n], (s_base[n] << 3) + s_off[n], s_len[n], s_src[n], s_fn[n], s_dry[n], e, b, v);
g_err_r[n] = e;
g_bit_r[n] = b;
g_val_r[n] = v;
end
gold_done = 1;
endtask
// ------------------------------------------------------------ engine
logic cmd_valid = 1'b0, cmd_ready;
logic [3:0] cmd_op = '0;
logic [63:0] cmd_base = '0, cmd_offset = '0;
logic [31:0] cmd_len = '0;
logic [23:0] cmd_src = '0;
logic [2:0] cmd_fn = '0;
logic cmd_dry = 1'b0;
logic res_valid, res_ready = 1'b0, res_error, res_bit;
logic [55:0] res_value;
logic host_valid = 1'b0, host_ready, host_write = 1'b0, host_rvalid;
logic [$clog2(WORDS)-1:0] host_word = '0;
logic [63:0] host_wdata = '0, host_rdata;
logic idle;
bitacc_engine #(.LANES(LANES), .WORDS_PER_LANE(WPL), .WITH_MATCH(WITH_MATCH), .MATCH_LANES(MLANES)) dut (.*);
logic e_err_r [MAXOPS];
logic e_bit_r [MAXOPS];
logic [55:0] e_val_r [MAXOPS];
integer e_results = 0;
bit eng_done = 0;
// result sink with random backpressure
always @(posedge clk) begin
if (!reset && res_valid && res_ready) begin
e_err_r[e_results] <= res_error;
e_bit_r[e_results] <= res_bit;
e_val_r[e_results] <= res_value;
e_results <= e_results + 1;
end
end
// Random backpressure, plus a 200-cycle stall every 2000 cycles that fills
// the reorder buffer and must hold the engine at its in-flight limit.
integer sink_cyc = 0;
always @(negedge clk) begin
sink_cyc <= sink_cyc + 1;
res_ready <= ((sink_cyc % 2000) >= 200) && ((rnd32() % 4) != 0);
end
task automatic host_write_word(input integer w, input logic [63:0] v);
@(negedge clk);
host_valid = 1'b1; host_write = 1'b1; host_word = w; host_wdata = v;
while (!host_ready) @(negedge clk);
@(negedge clk); host_valid = 1'b0; host_write = 1'b0;
endtask
task automatic host_read_word(input integer w, output logic [63:0] v);
@(negedge clk);
host_valid = 1'b1; host_write = 1'b0; host_word = w;
while (!host_ready) @(negedge clk);
@(negedge clk); host_valid = 1'b0;
check(host_rvalid === 1'b1, "host_rvalid one cycle after a read handshake");
v = host_rdata;
endtask
task automatic run_engine();
integer n = 0;
while (n < ops) begin
@(negedge clk);
if (rnd(20) == 0) cmd_valid = 1'b0; // input gap
else begin
// cmd_ready does not depend on cmd_valid and only changes on a rising
// edge, so its value now is the one the engine samples at the next edge.
cmd_valid = 1'b1; cmd_op = s_op[n]; cmd_base = s_base[n]; cmd_offset = s_off[n];
cmd_len = s_len[n]; cmd_src = s_src[n]; cmd_fn = s_fn[n]; cmd_dry = s_dry[n];
if (cmd_ready) n++;
end
end
@(negedge clk); cmd_valid = 1'b0;
while (e_results < ops) @(negedge clk);
eng_done = 1;
endtask
// ------------------------------------------------------------ coverage, watchdog
// A completion is out of order when an older operation (between the reorder
// head and its own slot) has not completed yet. Shadow completion flags per
// reorder slot: set when a result is written, cleared when it is emitted.
integer ooo = 0, idle_cycles = 0, last_results = 0, rob_full_cycles = 0;
integer range_waits = 0, single_waits = 0; // barrier: range behind lanes, single behind range
bit older_pending;
bit cdone [32];
initial for (int i = 0; i < 32; i++) cdone[i] = 0;
// Sampled mid-cycle: the engine's registers are stable and hold exactly the
// values the next rising edge consumes, independent of simulator ordering.
always @(negedge clk) begin
if (!reset) begin
if (dut.inflight == 32) rob_full_cycles <= rob_full_cycles + 1;
if (dut.d_valid && !dut.d_err && dut.d_range && dut.ops_out != '0) range_waits <= range_waits + 1;
if (dut.d_valid && !dut.d_err && !dut.d_range && dut.r_busy) single_waits <= single_waits + 1;
for (int i = 0; i < LANES; i++)
if (dut.l_done[i]) begin
older_pending = 0;
for (int t = dut.head; (t % 32) != dut.l_seq[i]; t++)
if (!cdone[t % 32]) older_pending = 1;
if (older_pending) ooo = ooo + 1;
end
for (int i = 0; i < LANES; i++) if (dut.l_done[i]) cdone[dut.l_seq[i]] = 1;
if (dut.d_take && dut.d_err) cdone[dut.d_seq] = 1;
if (dut.r_done) cdone[dut.r_seq] = 1;
if (res_valid && res_ready) cdone[dut.head] = 0;
if (e_results != last_results || dut.idle) idle_cycles <= 0;
else idle_cycles <= idle_cycles + 1;
last_results <= e_results;
if (idle_cycles > 20000) $fatal(1, "engine stalled: no result for 20000 cycles");
if (dut.inflight > 32) $fatal(1, "more operations in flight than reorder slots");
end
end
// ------------------------------------------------------------ main
logic [63:0] v;
integer kind; // per-operation stimulus class
integer pend_i = 0; // next entry of a pending MATCH sequence
integer n_hot = 0, n_any = 0, n_past = 0, n_wrap = 0, n_undef = 0, n_range = 0;
logic [63:0] ra;
// Bit-store-wide operations at fixed points of the stream.
task automatic gen_whole(input int n, input int which);
s_base[n] = 0; s_off[n] = 0; s_src[n] = 0; s_fn[n] = 3'd3; s_dry[n] = 1'b0;
case (which)
0: begin s_op[n] = R_COUNT; s_len[n] = WORDS * 64; end
1: begin s_op[n] = R_FLIPR; s_len[n] = WORDS * 64; end
default: begin s_op[n] = R_BULK; s_len[n] = WORDS / 2; s_off[n] = WORDS / 2; end // upper ^= lower
endcase
endtask
initial begin
if (!$value$plusargs("ops=%d", ops)) ops = 5000;
if (!$value$plusargs("seed=%d", seed)) seed = 1;
seed0 = seed;
if (ops > MAXOPS) ops = MAXOPS;
for (int w = 0; w < WORDS; w++) begin
init[w] = {rnd32(), rnd32()};
gmem[w] = init[w];
end
for (int n = 0; n < ops; n++) begin
// Blocks of 24: 16 back-to-back operations on 4 words of lane 0 (its queue
// builds up), then 8 mixed ones, many of which overtake them in other lanes.
kind = ((n % 24) < 16) ? 0 : 40 + rnd(60);
s_op[n] = (rnd(12) == 0) ? 4'(5 + rnd(3)) : 4'(rnd(5));
s_len[n] = '0; s_src[n] = '0; s_fn[n] = '0; s_dry[n] = 1'b0;
if (s_op[n] > 4'd4) n_undef++;
if (n == ops / 4 || n == ops / 2 || n == 3 * ops / 4) begin
n_range++;
gen_whole(n, n == ops / 4 ? 0 : n == ops / 2 ? 1 : 2);
end else if (pend_i < rs_q_n || (kind >= 40 && rnd(4) == 0)) begin // range operation
n_range++;
if (pend_i < rs_q_n) begin // rest of a MATCH sequence
s_op[n] = rs_q_op[pend_i]; ra = rs_q_a[pend_i]; s_len[n] = rs_q_len[pend_i];
s_src[n] = rs_q_src[pend_i]; s_fn[n] = rs_q_fn[pend_i]; pend_i++;
end else if (rnd(4) == 0) begin
gen_match_seq();
s_op[n] = rs_q_op[0]; ra = rs_q_a[0]; s_len[n] = rs_q_len[0];
s_src[n] = rs_q_src[0]; s_fn[n] = rs_q_fn[0]; pend_i = 1;
end else
gen_range(s_op[n], ra, s_len[n], s_src[n], s_fn[n], s_dry[n]);
if (rnd(2) == 0) begin s_base[n] = 0; s_off[n] = ra; end
else begin s_base[n] = ra >> 3; s_off[n] = ra & 64'd7; end
end else if (kind < 40) begin // hot spot: 4 words, all in lane 0
n_hot++;
s_base[n] = rnd(4) * LANES * 8; s_off[n] = rnd(64);
end else if (kind < 80) begin // anywhere in memory
n_any++;
s_base[n] = rnd(WORDS * 8); s_off[n] = rnd(64 * 4);
if (rnd(2) == 0) s_base[n] = (rnd(WPL) * LANES + 1 + rnd(LANES - 1)) * 8; // not lane 0
end else if (kind < 95) begin // just past the end
n_past++;
s_base[n] = WORDS * 8 + rnd(16); s_off[n] = rnd(64);
end else begin // arbitrary 64-bit values (wraps)
n_wrap++;
s_base[n] = {rnd32(), rnd32()}; s_off[n] = {rnd32(), rnd32()};
end
end
$display("stimulus: hot=%0d anywhere=%0d past_end=%0d wrap=%0d undefined_op=%0d range=%0d",
n_hot, n_any, n_past, n_wrap, n_undef, n_range);
check(n_hot > ops / 3 && n_any > ops / 20 && n_past > ops / 80 && n_wrap > ops / 400
&& n_undef > ops / 40 && n_range > ops / 40, "stimulus covers every operation class");
repeat (3) @(negedge clk);
reset = 1'b0;
for (int w = 0; w < WORDS; w++) host_write_word(w, init[w]);
for (int w = 0; w < WORDS; w++) begin
host_read_word(w, v);
check(v === init[w], $sformatf("host read back word %0d", w));
end
fork
run_gold();
run_engine();
join
for (int n = 0; n < ops; n++) begin
check(e_err_r[n] === g_err_r[n],
$sformatf("op %0d (%0d, base %h, off %h, len %0d, src %0d, fn %0d): error %b, reference %b",
n, s_op[n], s_base[n], s_off[n], s_len[n], s_src[n], s_fn[n], e_err_r[n], g_err_r[n]));
if (!g_err_r[n])
check(e_bit_r[n] === g_bit_r[n] && e_val_r[n] === g_val_r[n],
$sformatf("op %0d (%0d, base %h, off %h, len %0d, src %0d, fn %0d): bit %b value %0d, reference %b %0d",
n, s_op[n], s_base[n], s_off[n], s_len[n], s_src[n], s_fn[n],
e_bit_r[n], e_val_r[n], g_bit_r[n], g_val_r[n]));
else
check(e_bit_r[n] === 1'b0 && e_val_r[n] === '0, $sformatf("op %0d: error result has bit 0, value 0", n));
range_cover(s_op[n], (s_base[n] << 3) + s_off[n], s_len[n], s_src[n], s_fn[n], s_dry[n],
g_err_r[n], g_bit_r[n]);
end
range_cover_check();
check(range_waits > 0, "a range operation waited for busy lanes");
check(single_waits > 0, "an operation waited for a running range operation");
check(idle, "engine idle after the stream");
check(ooo > ops / 50, $sformatf("out-of-order completions exercised (%0d)", ooo));
check(rob_full_cycles > 0, "reorder buffer filled (in-flight limit exercised)");
for (int w = 0; w < WORDS; w++) begin
host_read_word(w, v);
check(v === gmem[w], $sformatf("final word %0d: %h, reference %h", w, v, gmem[w]));
end
$display("checks=%0d fails=%0d lanes=%0d words=%0d ops=%0d seed=%0d out_of_order=%0d rob_full=%0d range_waits=%0d single_waits=%0d reference_ops=%0d",
checks, fails, LANES, WORDS, ops, seed0, ooo, rob_full_cycles, range_waits, single_waits, gold_ops_issued);
if (fails != 0) $fatal(1, "FAIL");
$display("PASS: engine matches the reference core");
$finish;
end
initial begin
repeat (20) #1_000_000_000;
$fatal(1, "global timeout");
end
endmodule