Download pcie/tb/tb_engine.sv from Snapkitty/bit-string-accelerator: direct link, hf CLI and curl.
- Browser
- Download file 15.1 kB
-
https://huggingface.co/Snapkitty/bit-string-accelerator/resolve/main/pcie/tb/tb_engine.sv
- Command line
-
hf download hf://Snapkitty/bit-string-accelerator/pcie/tb/tb_engine.sv
-
curl -L -o tb_engine.sv https://huggingface.co/Snapkitty/bit-string-accelerator/resolve/main/pcie/tb/tb_engine.sv
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) | |
| module tb_engine; | |
| 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; | |
| 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 | |