Not every field participates in every compare. Timestamps differ by construction, debug ids are bench bookkeeping, and some payloads are don't-care in specific modes. Encode that as a field mask.
The next listing is a fragment that uses bus_txn from the scoreboard lesson, so this course's checks do not run it. The mask is chosen by the scoreboard per the spec, never hard-coded in the transaction.
// not-run: fragment: uses bus_txn from the scoreboard-architecture listing, no top module
class scoreboard;
mailbox #(bus_txn) mbx_act;
bus_txn expected_q[$];
int unsigned match_count, diff_count;
int unsigned max_diffs = 10; // stop the cascade
bit stop;
function void check_pair(bus_txn exp, bus_txn act, int unsigned mask);
string desc;
if (exp.do_compare(act, mask, desc)) begin
match_count++;
return;
end
diff_count++;
// First reports carry everything: both transactions, fields, index, time.
// The format is one string literal inside $sformatf: Icarus 13 accepts a
// concatenation as a format, but Verilator 5.048 prints it as a number.
$error("%s", $sformatf(
"[SCB] DIFFERENCE #%0d at compare index %0d, time %0t\n fields :%s\n expect : %s\n actual : %s",
diff_count, match_count + diff_count, $time,
desc, exp.convert2string(), act.convert2string()));
if (diff_count >= max_diffs)
$fatal(1, "[SCB] difference threshold (%0d) reached", max_diffs);
endfunction
endclass
// Scoreboard call site: payload is don't-care on an error response
// mask = (exp.resp != 0) ? (bus_txn::F_ALL & ~bus_txn::F_DATA)
// : bus_txn::F_ALL;Compare rule | Reason |
|---|---|
Use | An X in DUT output must fail, not silently match. |
Never compare timestamps or debug ids | Metadata differs by construction and always false-alarms. |
Name every differing field with both values | The report then writes itself. |
Cap reports with a threshold | The first difference is signal; the cascade is noise. |
At the first one only. Check stream alignment first via monitor counts, recompute that case against the spec by hand, and open waveforms at its recorded time.
One dropped transaction misaligns every later FIFO compare. Triage by what differs, not by how many reports arrived.
First report shows | Likely cause | First check |
|---|---|---|
All fields differ, queue out of step | Dropped or duplicated transaction upstream | Monitor counts on both sides. |
Data only, address and length match | Datapath bug or model formula bug | Recompute from the spec by hand. |
X or Z in actual fields | Uninitialized RTL or sampling race | Reset coverage and clocking-block use. |
A test ends at quiescence, not at the last stimulus. In-flight transactions must flush through the DUT before the verdict.
The next listing is a fragment that uses classes from earlier listings, so this course's checks do not run it. The env stores the generator, driver, monitor and clock handles the end of test depends on.
// not-run: fragment: uses generator, driver, bus_monitor, scoreboard and bus_if from earlier listings, no top module
class env;
virtual bus_if.MON vif; // clock the drain waits on
generator gen; // done_flag set after the last put
driver drv; // n_done counts completed handshakes
bus_monitor mon_out;
scoreboard scb;
task run_test();
fork : tb_threads
begin
// 1. stimulus exhausted: every item generated AND driven
wait (gen.done_flag && drv.n_done == gen.n);
drain(); // 2. wait for in-flight activity to flush
mon_out.stop = 1; // 3. loops exit at their next iteration
scb.stop = 1;
scb.final_check(); // 4. leftovers, undrained mailboxes, zero-compare
scb.report();
end
watchdog(); // global hang protection, whole test
join_any
disable tb_threads;
endtask
// Drain: wait until nothing has moved for 100 cycles
task drain();
int idle = 0;
int unsigned last_seen = mon_out.txn_count;
while (idle < 100) begin
@(posedge vif.clk);
if (mon_out.txn_count != last_seen) begin
last_seen = mon_out.txn_count;
idle = 0; // activity restarts the window
end else idle++;
end
endtask
task watchdog();
#1ms;
$fatal(1, "[ENV] WATCHDOG: test exceeded 1ms");
endtask
endclass
// Inside scoreboard: final_check fails leftovers, undrained actuals,
// a zero-compare run, and any recorded difference before report() prints.The zero-compare guard is the most valuable line: a run with no compares at all must fail, never pass. Stronger benches also require a per-test minimum compare count.
No. Transactions are still in flight. Ending now discards pending compares, possibly the failing one. Wait for the drain window, then check leftovers.
A compare function and a verdict function, plus a testbench that runs them on a masked compare, a first difference, a leftover, an X, and an empty run.
module tb_compare_eot;
timeunit 1ns; timeprecision 1ps;
reg [31:0] exp_data [0:3];
reg [31:0] act_data [0:3];
reg [1:0] exp_resp [0:3];
bit has_actual [0:3];
int errors = 0;
int match_count = 0, diffs = 0, leftovers = 0;
int first_diff = -1;
// Compare engine: payload is don't-care on an error response, and
// !== makes an X in the actual data a difference.
function automatic bit pair_differs(input [31:0] e, input [31:0] a,
input [1:0] resp);
return (resp == 2'd0) && (e !== a);
endfunction
// Verdict: something compared, no difference, nothing left over.
function automatic bit verdict_pass(input int nmatch, input int ndiff,
input int left);
return (nmatch + ndiff > 0) && (ndiff == 0) && (left == 0);
endfunction
initial begin
exp_data[0] = 32'hAAAA; exp_resp[0] = 2'd0;
act_data[0] = 32'hAAAA; has_actual[0] = 1; // identical
exp_data[1] = 32'hBBBB; exp_resp[1] = 2'd2;
act_data[1] = 32'hBEEF; has_actual[1] = 1; // error response: masked
exp_data[2] = 32'hCCCC; exp_resp[2] = 2'd0;
act_data[2] = 32'hCCD0; has_actual[2] = 1; // real data difference
exp_data[3] = 32'hDDDD; exp_resp[3] = 2'd0;
has_actual[3] = 0; // dropped: leftover
#1;
for (int i = 0; i < 4; i++) begin
if (!has_actual[i]) leftovers++;
else if (pair_differs(exp_data[i], act_data[i], exp_resp[i])) begin
diffs++;
if (first_diff == -1) first_diff = i;
end
else match_count++;
end
if (match_count !== 2) errors++;
if (diffs !== 1) errors++;
if (first_diff !== 2) errors++;
if (leftovers !== 1) errors++;
if (!pair_differs(32'hCCCC, 32'hxxxx_xxxx, 2'd0)) errors++; // X must differ
if (verdict_pass(match_count, diffs, leftovers)) errors++; // this run fails
if (verdict_pass(0, 0, 0)) errors++; // empty run fails
if (!verdict_pass(3, 0, 0)) errors++; // clean run passes
if (errors == 0) $display("PASS: masked compare matched 2, first difference at index 2, 1 leftover; X and empty run both fail the verdict");
else $fatal(1, "FAIL: %0d checks wrong (hit=%0d diffs=%0d first=%0d leftovers=%0d)",
errors, match_count, diffs, first_diff, leftovers);
$finish;
end
endmodule