Part 6 · Testbench Architecture · Intermediate
End-of-Test: The Objection Pattern by Hand
From naive #delay endings through transaction-count endings to a full raise/drain objection-counter class with watchdog.
Three generations of end-of-test
Every bench has to answer "when do we stop?" and the answer evolves the same way on every project. Generation one is a #1ms; $finish; — it truncates slow runs and wastes time on fast ones, and every stimulus change means re-tuning a magic number. Generation two ends when a transaction count is reached — better, but it hard-codes one component's view: it cannot express "the interrupt handler is still busy" or "agent B has follow-up traffic." Generation three lets any component hold the test open while it has outstanding work: the objection pattern, which is exactly what UVM's uvm_objection formalizes.
END-OF-TEST MECHANISMS, WORST TO BEST
GEN 1: #delay #1ms; $finish;
└─ too short → truncated checks; too long → wasted cycles
breaks on every stimulus or back-pressure change
GEN 2: txn count wait (scb.compared == N); $finish;
└─ one component's view only; N must be known up front;
reactive/interrupt traffic has no fixed N
GEN 3: objections any component: raise() while busy
(UVM-style) drop() when idle
└─ test ends when count == 0 (after drain time)
every component votes; no magic numbers
Always paired with: watchdog timeout (the safety net under all three)An objection counter class
class objection;
local int unsigned count;
local string holders[$]; // who raised — for hang debug
local time drain_time = 100ns;
function void raise(string who);
count++;
holders.push_back(who);
endfunction
function void drop(string who);
if (count == 0)
$fatal(1, "[OBJ] drop('%s') with zero outstanding objections", who);
count--;
foreach (holders[i]) if (holders[i] == who) begin
holders.delete(i);
break;
end
endfunction
function void set_drain_time(time t);
drain_time = t;
endfunction
// Blocks until count==0 AND stays 0 through a full drain window.
task wait_for_done();
forever begin
wait (count == 0);
#(drain_time);
if (count == 0) return; // nobody re-raised during drain → done
end
endtask
function void dump(string prefix = "[OBJ]");
$display("%s outstanding=%0d", prefix, count);
foreach (holders[i]) $display("%s held by: %s", prefix, holders[i]);
endfunction
endclassWhy the drain re-check loop matters
The subtle part is wait_for_done(): after the count hits zero it waits a drain time and then re-checks. This handles the gap pattern — generator drops its objection, and only some cycles later does the monitor see a response and raise its own. Without the re-check loop the test ends inside that gap. UVM's objection drain-time exists for exactly this reason.
Wiring it into the bench
class generator;
objection obj;
task run();
obj.raise("generator");
repeat (n_items) begin
bus_txn t = new();
void'(t.randomize());
mbx.put(t);
end
obj.drop("generator"); // accepted ≠ driven: driver holds its own
endtask
endclass
class driver;
objection obj;
task run();
forever begin
bus_txn t;
mbx.get(t);
obj.raise("driver"); // busy only while actually driving
drive_one(t);
obj.drop("driver");
end
endtask
endclass
class env;
objection obj = new();
task main_phase();
fork : main_threads
drv.run(); mon_in.run(); mon_out.run(); mdl.run(); scb.run();
join_none
gen.run();
fork : eot
obj.wait_for_done(); // normal end
begin // watchdog
#1ms;
obj.dump("[WATCHDOG]"); // who is hanging?
$fatal(1, "[ENV] timeout — objections never drained");
end
join_any
disable eot;
disable main_threads;
endtask
endclassDesign notes
raise/drop take a name string — when the watchdog fires, dump() tells you which component hung instead of leaving you to bisect.
The driver raises per item, not for the whole test — a forever loop holding one permanent objection would never let the count reach zero.
drop() below zero is a $fatal: an unbalanced raise/drop pair is a TB bug worth failing loudly on.
Watchdog and wait_for_done race under fork...join_any — whichever fires first decides, the other is disabled.
Interview angle
This is one of the highest-yield interview builds: "Implement UVM-style objections in plain SystemVerilog." Hit the four marks — counter with raise/drop, drain-time re-check loop, named holders for debug, watchdog alongside — and explain the gap problem the drain time solves. Candidates who only say "count up, count down, wait for zero" miss the re-raise window and that is the detail interviewers probe.
Key takeaways
End-of-test evolves: #delay → txn count → objections; objections let every component vote.
After count reaches zero, wait a drain time and re-check — late consumers may re-raise.
Track holder names so a watchdog dump names the hung component instantly.
The watchdog stays even with objections — a stuck objection is just a politer hang.
Common pitfalls
Per-test #delay endings — every back-pressure change silently truncates or wastes simulation.
Ending the instant count hits zero — the raise gap between producer and consumer ends the test early.
A forever-loop component holding one permanent objection — the count never reaches zero.
Unbalanced raise/drop pairs left unchecked — the count drifts and end-of-test becomes nondeterministic.