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.

systemverilog
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
endclass

Review findings

  1. 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().

  2. 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).

  3. 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).

  4. 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.

  5. 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.

  6. NOTE — no transaction logging at any verbosity; this driver is invisible to the debug instrumentation layers.

Corrected core

systemverilog
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")
endtask

Note 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.