File size: 15,146 Bytes
0ba037b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 | // 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
|