Part 8 · Senior & Interview Prep · Intermediate
Reviewing TB Components
Driver edge/region discipline, monitors that snoop the driver, scoreboard drain handling, blocking calls in run loops, fork hygiene — with a worked driver review.
The component checklist
Components are where timing bugs and silent-checking bugs live. The review lens: every component has exactly one source of truth it is allowed to observe, and one timing discipline it must obey — most component defects are violations of one or the other.
Driver timing: all pin drives through a clocking block (or nonblocking with explicit discipline) — a blocking drive on the active clock edge is a tool-dependent race waiting for a version upgrade.
Driver reset behavior: what does it do mid-transaction on reset assert? A driver with no reset branch keeps wiggling pins through reset — undefined DUT abuse.
Monitor source of truth: monitors observe PINS only. A monitor that reads the driver's transaction (or worse, is handed it) verifies the testbench against itself — checking dies, regression stays green.
Monitor protocol completeness: does it handle every legal timing (0-wait responses, back-to-back, aborts), or only what the current RTL emits?
Scoreboard drain: end-of-test must verify the expected queue is EMPTY and the compare count is NONZERO; a scoreboard without both checks can pass while comparing nothing.
Blocking calls in run loops: a get() that can starve, with no timeout/watchdog, converts a dropped transaction into an undiagnosable hang.
Fork hygiene: every fork in a loop body needs an answer to 'who joins this, and what kills it on reset?' — unmanaged fork...join_none in a loop leaks threads that outlive their transaction.
Worked review: a flawed driver
As submitted. Find the issues before reading the findings — focus on edges, regions, reset, and forks.
class bus_driver extends uvm_driver #(bus_txn);
virtual bus_if vif;
`uvm_component_utils(bus_driver)
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
fork
drive_one(req);
join_none
seq_item_port.item_done();
end
endtask
task drive_one(bus_txn t);
@(posedge vif.clk);
vif.valid = 1; // blocking drives
vif.addr = t.addr;
vif.write = t.write;
while (vif.ready !== 1)
@(posedge vif.clk);
vif.valid = 0;
endtask
endclassReview findings
BLOCK — fork...join_none around drive_one with immediate item_done(): the driver reports completion before driving anything, transactions overlap on the same pins (last writer wins, earlier txns corrupted), and the sequencer's response handshake is meaningless. Unless pipelining is a deliberate, documented protocol feature with per-txn channels, drive must complete before item_done().
BLOCK — blocking assignments to vif.* on the active clock edge: whether the DUT samples old or new values this edge is scheduler-dependent — the classic TB race. Drive through the interface's clocking block (vif.cb.valid <= 1).
BLOCK — no reset handling: on mid-burst reset the driver continues the while loop and holds valid through reset. run_phase needs a reset branch (fork drive vs reset-monitor, kill and re-init on assert).
BLOCK — while (vif.ready !== 1) sampled raw, not via the clocking block — ready can be read in the wrong region relative to the DUT's NBA update; same race family as the drives.
NOTE — no timeout on the ready wait: a dead DUT makes this an unattributed hang; a bounded wait with a fatal names the culprit instantly.
NOTE — no transaction logging at any verbosity; this driver is invisible to the debug instrumentation layers.
Corrected core
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
fork begin : drive_or_reset
fork
drive_one(req);
@(negedge vif.rst_n); // reset wins the race
join_any
disable fork;
end join
if (!vif.rst_n) reset_pins();
seq_item_port.item_done(); // AFTER the drive completes
end
endtask
task drive_one(bus_txn t);
vif.cb.valid <= 1'b1; // clocking-block drives: race-free
vif.cb.addr <= t.addr;
vif.cb.write <= t.write;
repeat (MAX_READY_WAIT) begin
@(vif.cb);
if (vif.cb.ready) begin
vif.cb.valid <= 1'b0;
return;
end
end
`uvm_fatal("DRVTMO", "ready not seen within MAX_READY_WAIT")
endtaskNote the isolation pattern: the inner fork ... join_any; disable fork; is wrapped in an outer fork...join so the disable cannot kill sibling threads elsewhere in run_phase — a fork-hygiene point reviewers should demand whenever they see disable fork.
Key takeaways
item_done() after the drive completes — completion reporting before pin activity breaks the sequencer contract.
All pin access (drive AND sample) through the clocking block; raw vif reads/writes on the clock edge are races.
Every driver needs a reset branch that aborts the in-flight transaction and re-initializes the pins.
Every disable fork needs an isolating outer fork...join; every wait needs a bounded timeout that names itself.
Common pitfalls
Approving a monitor that subscribes to the driver 'temporarily until pins are ready' — temporary becomes permanent and checking is circular.
Missing that fork...join_none in a get_next_item loop overlaps transactions on shared pins.
Reviewing drive timing by eye against the current RTL's behavior instead of the protocol's legal range.
Letting unbounded waits through review — each one is a future undiagnosable hang.