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.

diagram
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

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

Two 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

systemverilog
// 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
endmodule

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