Part 2 · OOP for Verification · Intermediate
Callbacks & Hooks
A callback base class with a queue in the driver, pre_drive/post_drive hooks, and error injection without editing the driver.
Extending a component you are not allowed to edit
A verified, signed-off driver should never be edited for one test's needs — yet tests legitimately need to corrupt a transaction before it hits the pins, or count things after each transfer. Inheritance is the wrong tool here: a subclass per test combination explodes, and a factory override swaps the whole class when you only want to add ten lines of behavior. The callback pattern inverts the relationship: the driver exposes fixed hook points (pre_drive, post_drive) and iterates a queue of user-supplied callback objects at each one. The driver's code is closed to modification but open to extension — the open/closed principle in its most practical form.
CALLBACK FLOW THROUGH THE DRIVER
test: drv.add_cb(err_cb); drv.add_cb(cov_cb); (install before run)
driver.run() loop: cbs queue: [ err_cb, cov_cb ]
mb.get(t)
│
▼
foreach cbs[i]: cbs[i].pre_drive(this, t) ◄── err_cb corrupts t here
│
▼
drive pins with t ← frozen, verified code
│
▼
foreach cbs[i]: cbs[i].post_drive(this, t) ◄── cov_cb samples t here
│
└── repeat
No callbacks installed → both loops run zero iterations → driver behaves
exactly as shipped. Callbacks are pure extension, never modification.The pattern: hook base class + queue + iteration
// ---- the hook contract: empty virtual methods, never pure ----
class driver_cb;
virtual task pre_drive (driver drv, bus_txn t); endtask
virtual task post_drive(driver drv, bus_txn t); endtask
endclass
class driver;
mailbox #(bus_txn) mb;
virtual bus_if vif;
driver_cb cbs[$]; // the callback queue
function new(mailbox #(bus_txn) mb, virtual bus_if vif);
this.mb = mb; this.vif = vif;
endfunction
function void add_cb(driver_cb cb);
cbs.push_back(cb);
endfunction
task run();
bus_txn t;
forever begin
mb.get(t);
foreach (cbs[i]) cbs[i].pre_drive(this, t); // hook 1
drive_pins(t); // frozen core
foreach (cbs[i]) cbs[i].post_drive(this, t); // hook 2
end
endtask
protected task drive_pins(bus_txn t);
@(posedge vif.clk);
vif.valid <= 1; vif.addr <= t.addr; vif.data <= t.data;
@(posedge vif.clk);
vif.valid <= 0;
endtask
endclassTwo deliberate choices: the hook methods are virtual with empty bodies , not pure virtual — so a callback overriding only pre_drive need not mention post_drive. And the hooks receive the driver handle plus the transaction, so a callback can both inspect component state and mutate the in-flight transaction. Hooks are tasks, not functions, so a callback may legally consume time (a delay-injection callback, for instance).
Error injection and coverage — without touching the driver
// corrupt ~12% of transactions before they reach the pins
class err_inject_cb extends driver_cb;
int unsigned corrupted;
virtual task pre_drive(driver drv, bus_txn t);
if ($urandom_range(7) == 0) begin
t.data ^= 32'h0000_0001; // flip parity-relevant bit
corrupted++;
$display("[%0t] CB: corrupted txn -> %s", $time, t.convert2string());
end
endtask
endclass
// sample coverage on what was ACTUALLY driven (post-corruption)
class cov_cb extends driver_cb;
covergroup cg with function sample(bus_txn t);
coverpoint t.dir;
coverpoint t.len { bins one = {1}; bins burst = {[2:8]}; }
endgroup
function new(); cg = new(); endfunction
virtual task post_drive(driver drv, bus_txn t);
cg.sample(t);
endtask
endclass
// the error test: stock env + two installed callbacks
module test_err;
initial begin
// ... construct mb, vif, drv as usual ...
err_inject_cb ecb = new();
cov_cb ccb = new();
drv.add_cb(ecb); // order matters: inject first,
drv.add_cb(ccb); // then sample the corrupted txn
// ... run ...
end
endmoduleCallbacks vs factory override — when to use which
Factory override — replace WHAT is created (a different transaction or driver type), set once before build.
Callback — add behavior AT a point in time inside an existing component, installable and stackable per test.
They compose: a factory-substituted err_txn carries the error fields; a callback decides per-transfer when to activate them.
UVM mapping: driver_cb → uvm_callback, the cbs[$] loop → the uvm_do_callbacks macro, add_cb → uvm_callbacks#(T,CB)::add.
Interview angle
"Inject errors without modifying the driver" — the canonical callback question; sketch hook class, queue, and the two foreach loops.
"Why empty virtual instead of pure virtual hooks?" — callbacks override only the hooks they care about.
"When callback vs subclass driver?" — subclassing forks verified code per test; callbacks stack independent behaviors on one driver.
Key takeaways
Callbacks invert extension: the component publishes hook points; tests supply behavior objects.
Pattern = empty-virtual hook base class + cbs[$] queue + foreach at each hook point.
Installation order is execution order — injection before coverage sampling, deliberately.
This is uvm_callback / uvm_do_callbacks with the macros removed.
Common pitfalls
Pure virtual hooks — every callback is forced to implement every hook, killing the pattern's ergonomics.
Forgetting to install the callback in the test — silently runs clean; assert the install or log it.
A pre_drive callback mutating the txn that a scoreboard ALSO holds by handle — corrupt expected data; copy before queueing to the scoreboard.
Hook iteration with a callback that blocks forever — stalls the driver loop; time-consuming callbacks need a contract.