Part 6 · Testbench Architecture · Intermediate
End-of-Test Checks & Drain
Quiescence checks, drain-time pattern, timeout watchdog, final PASS/FAIL banner, and the zero-compare sanity trap.
Quiescence: proving the TB is actually done
A test is not finished when stimulus stops — it is finished when every consequence of that stimulus has been observed and checked. That condition is quiescence : all expected transactions consumed, all mailboxes and queues empty, no in-flight activity in the DUT. Ending the simulation earlier silently discards pending compares — including, possibly, the failing one.
END-OF-TEST SEQUENCE
stimulus done ──► drain window ──► quiescence checks ──► report
│ │
│ ├─ expected_q empty?
in-flight txns │ ├─ expected[id] array empty?
flush through ────┘ ├─ all mailboxes num() == 0?
the DUT ├─ compare count > 0 ? ◄── sanity!
└─ mismatch count == 0 ?
│
all yes ────────────┴──── any no
│ │
▼ ▼
PASS banner FAIL banner
(leftovers listed)
watchdog (parallel, whole test): #MAX_TIME → $fatal("TB hung")class env;
// ... generator, driver, monitors, model, scoreboard handles ...
task run_test();
fork : tb_threads
begin // main flow
gen.run_all_stimulus(); // returns when last txn sent
drain(); // wait for in-flight to flush
scb.final_check();
report();
end
watchdog(); // global hang protection
join_any
disable tb_threads;
endtask
// Drain: wait until nothing has moved for DRAIN_IDLE 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 → restart window
end else idle++;
end
endtask
task watchdog();
#1ms;
$fatal(1, "[ENV] WATCHDOG: test exceeded 1ms — TB or DUT hung");
endtask
endclassActivity-based drain (restart the idle window on every observed txn) beats a fixed #delay — it adapts to back-pressure and deep pipelines.
The watchdog runs for the whole test in a parallel thread; a hung handshake otherwise stalls the regression slot forever.
join_any plus disable kills the watchdog on normal completion and kills the main flow on timeout.
Final checks and the report banner
class scoreboard;
bus_txn expected_q[$];
mailbox #(bus_txn) mbx_act;
int unsigned match_count, mismatch_count;
int unsigned errors;
function void final_check();
// 1. leftovers: predicted but never observed
if (expected_q.size() != 0) begin
errors++;
$error("[SCB] %0d expected txns never observed:", expected_q.size());
foreach (expected_q[i])
$display(" leftover[%0d]: %s", i, expected_q[i].convert2string());
end
// 2. unconsumed actuals stuck in the mailbox
if (mbx_act.num() != 0) begin
errors++;
$error("[SCB] %0d actual txns never compared (mailbox not drained)",
mbx_act.num());
end
// 3. THE ZERO-COMPARE TRAP: empty scoreboard must not mean PASS
if (match_count + mismatch_count == 0) begin
errors++;
$error("[SCB] ZERO comparisons performed — test exercised nothing");
end
if (mismatch_count != 0) errors++;
endfunction
function void report();
$display("=======================================================");
$display(" matches=%0d mismatches=%0d leftovers=%0d",
match_count, mismatch_count, expected_q.size());
if (errors == 0)
$display(" *** TEST PASSED ***");
else
$display(" *** TEST FAILED (%0d errors) ***", errors);
$display("=======================================================");
endfunction
endclassThe empty-scoreboard trap
The most dangerous regression result is a PASS that checked nothing. A broken generator, a monitor watching the wrong interface, or a config typo that disabled the driver all produce the same outcome: zero transactions, zero compares, zero mismatches — and a naive if (mismatch_count == 0) PASS banner. The zero-compare check converts that silent hole into a loud failure. Stronger variants: require a per-test minimum compare count , or have the test specify how many transactions it intends to send and check the scoreboard against that number.
Interview angle
Two staples: "How do you know when a test is done?" (quiescence — not when stimulus ends; describe drain plus leftover checks plus watchdog) and "How could a test pass while verifying nothing?" (zero-compare trap — name it and give the counter-check). These separate candidates who have shipped a TB from those who have only read about them.
Key takeaways
End of test = quiescence proven, not stimulus finished — drain, then check leftovers.
Use activity-based drain windows, not fixed delays — they adapt to back-pressure.
Always run a watchdog in parallel; a hung handshake must kill the test, not the regression slot.
PASS requires compare_count > 0 — an empty scoreboard is a failure, not a success.
Common pitfalls
Ending at "stimulus sent" — in-flight transactions are never checked, late mismatches vanish.
Fixed #10us drain delay — passes at low back-pressure, truncates checking at high back-pressure.
PASS banner gated only on mismatch_count == 0 — the zero-compare hole ships a broken test as green.
No leftover dump — "3 expected txns never observed" without printing them forces a full re-run.