Skip to content

UVM Callbacks

Three-actor model, callback classes, pool registration, uvm_do_callbacks, ordering, error injection.

UVM Fundamentals · Module 15

What Callbacks Solve

Factory override (Module 5) lets you replace an entire component with a different class. Callbacks let you add behaviour at specific points inside a component without replacing it. The component stays exactly as written. The callback is a plug-in object that runs at the component's invitation.

  • Factory Override vs Callback — Override: replaces the whole driver — your subclass must reimplement everything. Callback: inserts code at a named hook point — the driver runs normally, your callback adds behaviour.
  • Zero Coupling — The driver never imports or references any callback class. It only calls `uvm_do_callbacks at hook points. Callback classes live entirely in test packages.
  • Multiple Simultaneous — Register three callbacks on the same driver: one injects errors, one adds delays, one logs timing. All three execute in registration order at every hook point.
  • Instance-Level Control — Register a callback on one specific driver instance by component handle or by full path. Other instances of the same driver class are unaffected.

The Three Actors — Pool, Object, Component

apb_driver (Component)Contains hook points:pre_drive() hookpost_drive() hookInside run_phase:uvm_do_callbacks( apb_driver, apb_drv_cb, pre_drive(this, req))uvm_callbacks Pooluvm_callbacks #(apb_driver, apb_drv_cb)Registry of (component → callback list):drv_0 → [error_cb, delay_cb]drv_1 → [log_cb](global) → [timing_cb]Added by test via uvm_callbacks::add()Callback ObjectsExtend apb_drv_cb:error_inject_cbpre_drive: corrupt 1 bit in 10delay_inject_cbpre_drive: add random delaytiming_log_cbpost_drive: record timestampscallsinvokes Figure 1 — The three-actor callback model. The component calls uvm_do_callbacks at hook points. The pool maintains the callback list per component. The callback objects implement the actual hook behaviour. None of the three knows the others' types directly.

Step 1 — Defining the Callback Base Class

The callback base class extends uvm_callback and declares virtual methods for each hook point. These virtual methods have empty default implementations — so registering a callback object that only overrides one hook leaves all other hooks as no-ops.

SystemVerilog — defining the callback base class with hook points
// ── apb_driver_cb.sv — callback base class ────────────────────────────
class apb_drv_cb extends uvm_callback;
 
// Each virtual method defines one hook point in the driver.
// Default implementations do nothing — override only what you need.
 
// Called BEFORE driving signals onto the bus
virtual task pre_drive(apb_driver drv, apb_seq_item req);
// Default: nothing
endtask
 
// Called AFTER driving signals and receiving response
virtual task post_drive(apb_driver drv, apb_seq_item req);
// Default: nothing
endtask
 
// Called when an error response is detected from the DUT
virtual function void on_error(apb_driver drv, apb_seq_item req);
// Default: nothing
endfunction
 
function new(string name = "apb_drv_cb");
super.new(name);
endfunction
 
endclass
 
// ── Concrete callback #1: Error injection ─────────────────────────────
class error_inject_cb extends apb_drv_cb;
int error_rate = 10;   // inject error on every 10th transaction
int count      = 0;
 
function new(string name = "error_inject_cb");
super.new(name);
endfunction
 
// Override only pre_drive — post_drive and on_error stay as no-ops
virtual task pre_drive(apb_driver drv, apb_seq_item req);
count++;
if ((count % error_rate) == 0) begin
req.data = ~req.data;   // corrupt the data payload
`uvm_info("ERR_CB",
$sformatf("Injecting error on txn #%0d — data corrupted", count),
UVM_LOW)
end
endtask
endclass
 
// ── Concrete callback #2: Timing delay injection ───────────────────────
class delay_inject_cb extends apb_drv_cb;
rand int delay_ns;
constraint c_delay { delay_ns inside {[5:50]}; }
 
function new(string name = "delay_inject_cb");
super.new(name);
endfunction
 
virtual task pre_drive(apb_driver drv, apb_seq_item req);
void'(this.randomize());
#(delay_ns * 1ns);
`uvm_info("DLY_CB",
$sformatf("Added %0dns delay before driving", delay_ns),
UVM_MEDIUM)
endtask
endclass
 
// ── Concrete callback #3: Post-drive timing log ───────────────────────
class timing_log_cb extends apb_drv_cb;
time drive_start_time;
 
function new(string name = "timing_log_cb");
super.new(name);
endfunction
 
virtual task pre_drive(apb_driver drv, apb_seq_item req);
drive_start_time = $time;
endtask
 
virtual task post_drive(apb_driver drv, apb_seq_item req);
`uvm_info("LOG_CB",
$sformatf("Transaction took %0t ns  addr=0x%0h",
$time - drive_start_time, req.addr),
UVM_HIGH)
endtask
endclass

Step 2 — Registering the Callback Pool in the Component

The component must declare its association with the callback type using ``uvm_register_cb`. This macro registers the (component type, callback type) pair with the global callback infrastructure, enabling type-safe registration and invocation.

SystemVerilog — registering callback type in the driver component
class apb_driver extends uvm_driver #(apb_seq_item);
`uvm_component_utils(apb_driver)
 
// ── CRITICAL: register the callback type ──────────────────────────
// Must appear inside the class body, after `uvm_component_utils
// Format: `uvm_register_cb(THIS_COMPONENT_TYPE, CALLBACK_BASE_TYPE)
`uvm_register_cb(apb_driver, apb_drv_cb)
 
virtual apb_if vif;
 
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("CFG", "vif not found")
endfunction
 
// ── run_phase with callback invocations ────────────────────────────
task run_phase(uvm_phase phase);
apb_seq_item req;
forever begin
seq_item_port.get_next_item(req);
 
// ── Hook: pre_drive ───────────────────────────────────────
// `uvm_do_callbacks(COMP, CB, METHOD_CALL)
// Iterates all registered callbacks and calls pre_drive() on each
`uvm_do_callbacks(apb_driver, apb_drv_cb, pre_drive(this, req))
 
// Normal driver logic — drive the DUT
@(posedge vif.clk);
vif.paddr   = req.addr;
vif.pwdata  = req.data;
vif.pwrite  = req.write;
vif.psel    = 1;
vif.penable = 1;
@(posedge vif.clk iff vif.pready);
 
// Check for error response
if (vif.pslverr) begin
`uvm_do_callbacks(apb_driver, apb_drv_cb, on_error(this, req))
end
 
vif.psel    = 0;
vif.penable = 0;
 
// ── Hook: post_drive ──────────────────────────────────────
`uvm_do_callbacks(apb_driver, apb_drv_cb, post_drive(this, req))
 
seq_item_port.item_done();
end
endtask
 
endclass

Step 3 — Controlling Execution with Return Values

Callback methods can return a uvm_callback_iter status to control whether subsequent callbacks in the chain execute. For function callbacks, returning UVM_CB_RETURN stops the chain.

SystemVerilog — callback with status, uvm_do_callbacks_exit_on, enable/disable
// ── Callback that can abort the chain ─────────────────────────────────
class gating_cb extends apb_drv_cb;
bit block_drive = 0;   // set to 1 to suppress all further callbacks
 
function new(string name = "gating_cb");
super.new(name);
endfunction
 
virtual task pre_drive(apb_driver drv, apb_seq_item req);
if (block_drive) begin
`uvm_info("GATE", "Drive blocked by gating_cb", UVM_LOW)
// To abort remaining callbacks in chain for this hook:
this.callback_mode(0);   // disable this callback temporarily
end
endtask
endclass
 
// ── `uvm_do_callbacks_exit_on: stop chain on first non-zero return ────
// Useful when the FIRST matching callback should handle an event exclusively
`uvm_do_callbacks_exit_on(apb_driver, apb_drv_cb, on_error(this, req), 1)
// Stops after the first on_error() callback that returns 1 (handled)
 
// ── Enable / disable individual callbacks ─────────────────────────────
error_inject_cb err_cb = new("err_cb");
uvm_callbacks #(apb_driver, apb_drv_cb)::add(drv, err_cb);
 
// Disable temporarily (callback stays registered but skipped during iteration)
err_cb.callback_mode(0);   // 0 = disabled
err_cb.callback_mode(1);   // 1 = re-enabled
 
// Check enabled state:
bit en = err_cb.is_enabled();

Step 4 — Adding Callbacks in Tests

Callbacks are registered in tests — never inside the VIP. This keeps the VIP code clean and test-specific behaviour isolated. The test registers callbacks in build_phase (before components are built) or in start_of_simulation_phase.

SystemVerilog — all ways to register, order, and remove callbacks
// ── Method 1: Add to a specific component instance (handle-based) ────
class error_inject_test extends base_test;
`uvm_component_utils(error_inject_test)
 
function void start_of_simulation_phase(uvm_phase phase);
error_inject_cb err_cb;
delay_inject_cb dly_cb;
timing_log_cb   log_cb;
super.start_of_simulation_phase(phase);
 
err_cb = new("err_cb");
dly_cb = new("dly_cb");
log_cb = new("log_cb");
 
// Register on the SPECIFIC driver in the env (handle-based)
// Callbacks execute in the order they are added
uvm_callbacks#(apb_driver,apb_drv_cb)::add(env.apb_agent.drv, err_cb);
uvm_callbacks#(apb_driver,apb_drv_cb)::add(env.apb_agent.drv, dly_cb);
uvm_callbacks#(apb_driver,apb_drv_cb)::add(env.apb_agent.drv, log_cb);
// Order: err_cb first, then dly_cb, then log_cb on pre_drive()
endfunction
endclass
 
// ── Method 2: Add globally — applies to ALL instances of apb_driver ──
timing_log_cb global_log = new("global_log");
uvm_callbacks#(apb_driver,apb_drv_cb)::add(null, global_log);
// null = global registration — every apb_driver in the sim gets this callback
 
// ── Method 3: Add by full path string (no handle needed) ──────────────
error_inject_cb err = new("err");
uvm_callbacks#(apb_driver,apb_drv_cb)::add_by_name(
"uvm_test_top.env.apb_agent.drv", err,
uvm_root::get());
// Useful when you don't have a handle to the component at registration time
 
// ── Insert at specific position in the chain ─────────────────────────
// add(comp, cb, UVM_APPEND)   — add at end (default)
// add(comp, cb, UVM_PREPEND)  — add at beginning (runs first)
error_inject_cb first_cb = new("first_cb");
uvm_callbacks#(apb_driver,apb_drv_cb)::add(
env.apb_agent.drv, first_cb, UVM_PREPEND);
 
// ── Remove a callback ─────────────────────────────────────────────────
uvm_callbacks#(apb_driver,apb_drv_cb)::delete(env.apb_agent.drv, err_cb);
 
// ── Query registered callbacks ────────────────────────────────────────
int n = uvm_callbacks#(apb_driver,apb_drv_cb)::size(env.apb_agent.drv);
`uvm_info("CB", $sformatf("%0d callbacks on drv", n), UVM_LOW)

Ready-to-Run Simulator Example

A complete, self-contained demonstration: an APB-style driver with two hook points, two simultaneous callbacks (error injection + timing log), and a test that verifies callback behaviour. Copy to callbacks_demo.sv and run with the commands below. Ready to Run — Questa / VCS / Xcelium

SystemVerilog — callbacks_demo.sv (complete, copy and run)
// callbacks_demo.sv — complete ready-to-run UVM callback demonstration
// Compile:
//   Questa : vlog -sv callbacks_demo.sv
//            vsim -c callbacks_demo_top -do "run -all; quit"
//   VCS    : vcs -sverilog -ntb_opts uvm callbacks_demo.sv && ./simv
//   Xcelium: xrun -sv -uvm callbacks_demo.sv
 
`include "uvm_macros.svh"
import uvm_pkg::*;
 
// ═══════════════════════════════════════════════════════════════════════
//  TRANSACTION
// ═══════════════════════════════════════════════════════════════════════
class pkt extends uvm_sequence_item;
`uvm_object_utils(pkt)
rand bit [7:0] data;
rand bit [3:0] id;
function new(string name="pkt"); super.new(name); endfunction
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  CALLBACK BASE CLASS
// ═══════════════════════════════════════════════════════════════════════
class demo_drv_cb extends uvm_callback;
virtual task pre_drive(uvm_component drv, pkt req);  endtask
virtual task post_drive(uvm_component drv, pkt req); endtask
function new(string name="demo_drv_cb"); super.new(name); endfunction
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  CONCRETE CALLBACK 1: Error Injection (corrupts every 3rd transaction)
// ═══════════════════════════════════════════════════════════════════════
class err_cb extends demo_drv_cb;
int cnt = 0;
function new(string name="err_cb"); super.new(name); endfunction
 
virtual task pre_drive(uvm_component drv, pkt req);
cnt++;
if (cnt % 3 == 0) begin
automatic bit [7:0] orig = req.data;
req.data ^= 8'hFF;   // flip all bits
`uvm_info("ERR_CB",
$sformatf("txn #%0d: data 0x%0h → 0x%0h (ERROR INJECTED)",
cnt, orig, req.data), UVM_NONE)
end
endtask
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  CONCRETE CALLBACK 2: Timing Logger
// ═══════════════════════════════════════════════════════════════════════
class log_cb extends demo_drv_cb;
time t0;
function new(string name="log_cb"); super.new(name); endfunction
 
virtual task pre_drive(uvm_component drv, pkt req);
t0 = $time;
endtask
 
virtual task post_drive(uvm_component drv, pkt req);
`uvm_info("LOG_CB",
$sformatf("id=%0d data=0x%0h  drive_time=%0t",
req.id, req.data, $time - t0), UVM_LOW)
endtask
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  DRIVER — with callback hook points
// ═══════════════════════════════════════════════════════════════════════
class demo_driver extends uvm_driver#(pkt);
`uvm_component_utils(demo_driver)
`uvm_register_cb(demo_driver, demo_drv_cb)   // register callback type
 
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
 
task run_phase(uvm_phase phase);
pkt req;
forever begin
seq_item_port.get_next_item(req);
 
// ── PRE-DRIVE hook ────────────────────────────────────────
`uvm_do_callbacks(demo_driver, demo_drv_cb, pre_drive(this, req))
 
// Simulated drive: just consume some time
#20;
`uvm_info("DRV",
$sformatf("DRIVING  id=%0d  data=0x%0h", req.id, req.data),
UVM_MEDIUM)
 
// ── POST-DRIVE hook ───────────────────────────────────────
`uvm_do_callbacks(demo_driver, demo_drv_cb, post_drive(this, req))
 
seq_item_port.item_done();
end
endtask
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  SEQUENCE — generates 6 packets
// ═══════════════════════════════════════════════════════════════════════
class pkt_seq extends uvm_sequence#(pkt);
`uvm_object_utils(pkt_seq)
function new(string name="pkt_seq"); super.new(name); endfunction
 
task body();
pkt p;
repeat(6) begin
p = pkt::type_id::create("p");
start_item(p);
void'(p.randomize() with { id == 6 - $cast(int,p); });
p.id   = $urandom_range(0,15);
p.data = $urandom_range(0,255);
finish_item(p);
end
endtask
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  ENVIRONMENT
// ═══════════════════════════════════════════════════════════════════════
class demo_env extends uvm_env;
`uvm_component_utils(demo_env)
demo_driver           drv;
uvm_sequencer#(pkt)  seqr;
 
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv  = demo_driver::type_id::create("drv",  this);
seqr = uvm_sequencer#(pkt)::type_id::create("seqr", this);
endfunction
 
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
drv.seq_item_port.connect(seqr.seq_item_export);
endfunction
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  TEST — registers TWO callbacks on the driver
// ═══════════════════════════════════════════════════════════════════════
class cb_demo_test extends uvm_test;
`uvm_component_utils(cb_demo_test)
demo_env env;
 
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = demo_env::type_id::create("env", this);
endfunction
 
function void start_of_simulation_phase(uvm_phase phase);
err_cb ec; log_cb lc;
super.start_of_simulation_phase(phase);
ec = new("ec");
lc = new("lc");
// Register err_cb first (runs before log_cb in pre_drive)
uvm_callbacks#(demo_driver,demo_drv_cb)::add(env.drv, ec);
uvm_callbacks#(demo_driver,demo_drv_cb)::add(env.drv, lc);
`uvm_info("TEST",
"Registered: error-inject (every 3rd txn) + timing-log callbacks",
UVM_NONE)
endfunction
 
task run_phase(uvm_phase phase);
pkt_seq seq = pkt_seq::type_id::create("seq");
phase.raise_objection(this);
seq.start(env.seqr);
phase.drop_objection(this);
endtask
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  TOP MODULE
// ═══════════════════════════════════════════════════════════════════════
module callbacks_demo_top;
initial run_test("cb_demo_test");
endmodule
 
// ═══════════════════════════════════════════════════════════════════════
//  EXPECTED OUTPUT (txn #3 and #6 are error-injected):
//
//  TEST: Registered: error-inject (every 3rd txn) + timing-log callbacks
//  DRV:  DRIVING  id=?  data=0x??   (txn 1 — normal)
//  LOG_CB: id=? data=0x??  drive_time=20
//  DRV:  DRIVING  id=?  data=0x??   (txn 2 — normal)
//  LOG_CB: id=? data=0x??  drive_time=20
//  ERR_CB: txn #3: data 0x?? → 0x?? (ERROR INJECTED)
//  DRV:  DRIVING  id=?  data=0x??   (txn 3 — corrupted by err_cb)
//  LOG_CB: id=? data=0x??  drive_time=20  ← logs corrupted data
//  ... (txns 4,5 normal; txn 6 also corrupted)
//  UVM_INFO @ 0: UVM_ERROR :   0   UVM_FATAL :   0
// ═══════════════════════════════════════════════════════════════════════
Shell — compile and run on all major simulators
## ── Questa ────────────────────────────────────────────────────────────
vlog -sv -timescale 1ns/1ps callbacks_demo.sv
vsim -c callbacks_demo_top +UVM_VERBOSITY=UVM_MEDIUM \
-do "run -all; quit -f"
 
## ── VCS ───────────────────────────────────────────────────────────────
vcs -sverilog -ntb_opts uvm -timescale=1ns/1ps callbacks_demo.sv -o simv
./simv +UVM_TESTNAME=cb_demo_test +UVM_VERBOSITY=UVM_MEDIUM
 
## ── Xcelium ───────────────────────────────────────────────────────────
xrun -sv -uvm -timescale 1ns/1ps callbacks_demo.sv \
-input "run; exit" \
+UVM_TESTNAME=cb_demo_test +UVM_VERBOSITY=UVM_MEDIUM
 
## ── To see full log output including timing:
##    Add +UVM_VERBOSITY=UVM_HIGH  to any of the above

Quick Reference — The Four-Step Pattern

  • Define the callback base class Extend uvm_callback. Declare one virtual method per hook point with empty default implementations. Users only override the methods they care about. class my_drv_cb extends uvm_callback; virtual task pre_drive(my_driver drv, my_txn req); endtask endclass
  • Register the callback type in the component Inside the component class body, after uvm_component_utils`, add the registration macro. One macro associates the component type with the callback base type. uvm_register_cb(my_driver, my_drv_cb)`
  • Call uvm_do_callbacks at each hook point At each hook location in the component's tasks/functions, invoke all registered callbacks. They execute in registration order. ``uvm_do_callbacks(my_driver, my_drv_cb, pre_drive(this, req))
  • Register callback objects in the test In the test's start_of_simulation_phase, create callback objects and register them on specific components or globally. The VIP source is never edited. uvm_callbacks#(my_driver,my_drv_cb)::add(env.drv, new_cb)
APIEffect
``uvm_register_cb(COMP, CB)`Inside component class — registers the (comp, cb) type pair with the framework
``uvm_do_callbacks(COMP, CB, METHOD)`At hook point — calls METHOD on all registered callbacks in order
``uvm_do_callbacks_exit_on(COMP,CB,METHOD,VAL)`At hook point — stops chain when METHOD returns VAL
uvm_callbacks#(C,CB)::add(comp, cb)Register a callback on a specific component instance (or null for global)
uvm_callbacks#(C,CB)::add(null, cb)Register globally — all instances of C get this callback
uvm_callbacks#(C,CB)::add(comp, cb, UVM_PREPEND)Insert at beginning of chain (runs before others)
uvm_callbacks#(C,CB)::delete(comp, cb)Remove a specific callback from a component
cb.callback_mode(0/1)Disable (0) or re-enable (1) a registered callback without removing it
uvm_callbacks#(C,CB)::size(comp)Returns number of callbacks registered on a component

§9 — Code Examples

Example 1 — Beginner: Error Injection Callback for an APB Driver

The most common real-world callback use case. A production driver that is shared across projects gets error injection capability without a single line of driver source being changed.

SystemVerilog — complete error injection callback: 4 steps
// ═══ STEP 1: Define callback base class ════════════════════════════════
class apb_drv_cb extends uvm_callback;
`uvm_object_utils(apb_drv_cb)
function new(string name="apb_drv_cb"); super.new(name); endfunction
 
// Virtual method — override in derived classes to add behaviour
// Return 0 = continue normal driver flow; 1 = skip it
virtual task pre_drive(apb_driver drv, apb_seq_item req,
ref bit skip);
skip = 0;   // base: do nothing, don't skip
endtask
 
virtual task post_drive(apb_driver drv, apb_seq_item req);
endtask
endclass
 
// ═══ STEP 2: Register callback pool in driver ═══════════════════════════
class apb_driver extends uvm_driver#(apb_seq_item);
`uvm_component_utils(apb_driver)
`uvm_register_cb(apb_driver, apb_drv_cb)  // ← link pool to this component
 
virtual apb_if vif;
 
task run_phase(uvm_phase phase);
apb_seq_item req;
bit skip;
forever begin
seq_item_port.get_next_item(req);
 
// ═══ STEP 3: Fire pre_drive hook ═══════════════════════════
skip = 0;
`uvm_do_callbacks(apb_driver, apb_drv_cb,
pre_drive(this, req, skip))
 
if (!skip) begin
// Normal driving — callbacks can bypass this
@(posedge vif.clk);
vif.paddr  <= req.addr;
vif.pwdata <= req.data;
vif.pwrite <= req.write;
vif.psel   <= 1;
vif.penable <= 1;
@(posedge vif.clk iff vif.pready);
vif.psel   <= 0;
vif.penable <= 0;
end
 
`uvm_do_callbacks(apb_driver, apb_drv_cb,
post_drive(this, req))
 
seq_item_port.item_done();
end
endtask
endclass
 
// ═══ STEP 4 (Test): Error injection callback ════════════════════════════
class apb_err_inject_cb extends apb_drv_cb;
`uvm_object_utils(apb_err_inject_cb)
int inject_on_txn = 3;    // inject error on transaction #3
int txn_count    = 0;
 
virtual task pre_drive(apb_driver drv, apb_seq_item req, ref bit skip);
txn_count++;
if (txn_count == inject_on_txn) begin
`uvm_info("CB", $sformatf(
"Injecting PSLVERR on txn #%0d addr=0x%0h",
txn_count, req.addr), UVM_LOW)
req.inject_slverr = 1;  // driver reads this flag and asserts PSLVERR
end
endtask
endclass
 
// ── Registering in the test ────────────────────────────────────────────
class error_test extends base_test;
function void start_of_simulation_phase(uvm_phase phase);
apb_err_inject_cb cb = apb_err_inject_cb::type_id::create("cb");
cb.inject_on_txn = 3;
// Add to ALL apb_driver instances in the hierarchy
uvm_callbacks#(apb_driver, apb_drv_cb)::add(null, cb);
endfunction
endclass

Example 2 — Intermediate: Timing Jitter Callback

A second project needs random inter-transaction delay to stress timing paths. Same driver, different callback, different test — no VIP modifications.

SystemVerilog — timing jitter callback: post_drive delay
// ── Timing jitter callback — adds random delay between transactions ────
class apb_jitter_cb extends apb_drv_cb;
`uvm_object_utils(apb_jitter_cb)
 
int min_delay_ns = 10;
int max_delay_ns = 100;
 
virtual task post_drive(apb_driver drv, apb_seq_item req);
int jitter = $urandom_range(min_delay_ns, max_delay_ns);
`uvm_info("CB_JITTER", $sformatf(
"Post-drive jitter: %0dns", jitter), UVM_HIGH)
#(jitter);   // insert delay AFTER driving — pre_drive not used
endtask
endclass
 
// ── Timing test: add jitter callback to specific agent only ───────────
class timing_stress_test extends base_test;
function void start_of_simulation_phase(uvm_phase phase);
apb_jitter_cb cb = apb_jitter_cb::type_id::create("cb");
cb.min_delay_ns = 5;
cb.max_delay_ns = 50;
// Add only to the master agent's driver — not slave
uvm_callbacks#(apb_driver, apb_drv_cb)::add(
env.master_agent.drv, cb);  // specific component, not null
endfunction
endclass
 
// ── Two callbacks active simultaneously (error test + jitter) ─────────
class combined_test extends base_test;
function void start_of_simulation_phase(uvm_phase phase);
apb_err_inject_cb err_cb  = apb_err_inject_cb::type_id::create("err_cb");
apb_jitter_cb     jit_cb  = apb_jitter_cb::type_id::create("jit_cb");
// Both run: pre_drive (err) then post_drive (jit) — in add() order
uvm_callbacks#(apb_driver, apb_drv_cb)::add(null, err_cb);
uvm_callbacks#(apb_driver, apb_drv_cb)::add(null, jit_cb);
endfunction
endclass

Example 3 — Verification: Transaction Logger Callback

Project 9 needs every transaction logged to a protocol database. The logger callback runs post_drive, reads the completed transaction fields, and writes to an external file — all without any driver modification.

SystemVerilog — transaction logger callback with file output
class apb_txn_logger_cb extends apb_drv_cb;
`uvm_object_utils(apb_txn_logger_cb)
int log_fd;
int txn_count = 0;
 
function new(string name="apb_txn_logger_cb");
super.new(name);
log_fd = $fopen("apb_protocol_log.csv", "w");
$fwrite(log_fd, "txn_id,time_ns,addr,data,write,status\n");
endfunction
 
virtual task post_drive(apb_driver drv, apb_seq_item req);
txn_count++;
$fwrite(log_fd, "%0d,%0t,0x%0h,0x%0h,%0b,%s\n",
txn_count, $time, req.addr, req.data,
req.write, req.got_error ? "ERR" : "OK")
`uvm_info("CB_LOG", $sformatf("Logged txn #%0d", txn_count), UVM_HIGH)
endtask
 
virtual function void report();
$fclose(log_fd);
`uvm_info("CB_LOG", $sformatf("Protocol log closed: %0d entries",
txn_count), UVM_NONE)
endfunction
endclass
 
// ── Logger test ───────────────────────────────────────────────────────
class logging_test extends base_test;
apb_txn_logger_cb logger_cb;
 
function void start_of_simulation_phase(uvm_phase phase);
logger_cb = apb_txn_logger_cb::type_id::create("logger_cb");
uvm_callbacks#(apb_driver, apb_drv_cb)::add(null, logger_cb);
endfunction
 
function void report_phase(uvm_phase phase);
super.report_phase(phase);
logger_cb.report();   // flush and close the log file
endfunction
endclass

Example 4 — Tricky: Callback Ordering and the Skip Flag

Two callbacks registered — one checks a condition and sets skip=1. When does the second callback run? When does the skip flag take effect? Order and flag semantics matter more than engineers realise.

SystemVerilog — callback ordering: who runs when skip is set
// ── Two callbacks, both registered ───────────────────────────────────
class gate_cb extends apb_drv_cb;
`uvm_object_utils(gate_cb)
virtual task pre_drive(apb_driver drv, apb_seq_item req, ref bit skip);
if (req.addr[31:28] == 4'hF) begin
`uvm_info("GATE", "Reserved address — skipping drive", UVM_LOW)
skip = 1;   // signal that normal driving should be bypassed
end
endtask
endclass
 
class counter_cb extends apb_drv_cb;
`uvm_object_utils(counter_cb)
int count = 0;
virtual task pre_drive(apb_driver drv, apb_seq_item req, ref bit skip);
count++;
`uvm_info("COUNT", $sformatf("txn #%0d", count), UVM_HIGH)
// Does NOT check skip — runs regardless of what gate_cb set
endtask
endclass
 
// ── Registration order determines execution order ─────────────────────
// uvm_callbacks::add(null, gate_cb)    ← registered first
// uvm_callbacks::add(null, counter_cb) ← registered second
 
// When `uvm_do_callbacks fires:
// 1. gate_cb.pre_drive() runs → may set skip=1
// 2. counter_cb.pre_drive() runs → ALWAYS runs, skip is ignored by this CB
// 3. Driver checks skip → if 1, bypasses normal drive logic
//
// CRITICAL: `uvm_do_callbacks does NOT stop after one CB sets skip.
// It calls ALL registered callbacks, in registration order.
// The skip flag is just a ref argument — each CB reads and writes it.
// A CB registered AFTER gate_cb can CLEAR skip if it wants to.
// Design your callbacks to cooperate through skip — don't assume exclusivity.
 
// ── To stop at first skip: implement manually ─────────────────────────
// Instead of `uvm_do_callbacks, iterate manually:
task drive_with_early_exit(apb_seq_item req);
apb_drv_cb cb;
bit skip = 0;
uvm_queue#(apb_drv_cb) q =
uvm_callbacks#(apb_driver, apb_drv_cb)::get(this);
foreach (q[i]) begin
q[i].pre_drive(this, req, skip);
if (skip) break;  // stop at first skip — remaining CBs don't run
end
endtask

§10 — Bugs & Debugging

Bug 1 — Forgetting `uvm_register_cb in the Component

⚠️ Callback Added But Never Executes — Silent Non-Invocation

The test creates and adds a callback object. The driver has uvm_do_callbacks` calls in its run_phase. But the callback never fires. No error. The root cause: the driver class is missing uvm_register_cb(apb_driver, apb_drv_cb)`. Without this macro, the callback pool is not linked to the component, and add() has nowhere to store the callback handle.

SystemVerilog — missing register_cb macro and the fix
// ❌ WRONG — missing `uvm_register_cb ─────────────────────────────────
class apb_driver extends uvm_driver#(apb_seq_item);
`uvm_component_utils(apb_driver)
// `uvm_register_cb(apb_driver, apb_drv_cb) ← MISSING!
 
task run_phase(uvm_phase phase);
`uvm_do_callbacks(apb_driver, apb_drv_cb, pre_drive(this, req, skip))
// Never actually calls anything — pool not registered
endtask
endclass
 
// In the test:
// uvm_callbacks::add(null, cb); ← cb is added to... nowhere useful
// Simulation runs, no CB fires, no error, test looks like it passed.
// Debug: enable +UVM_CB_TRACE and look for the component+callback pair
 
// ✓ CORRECT — register_cb macro present ──────────────────────────────
class apb_driver extends uvm_driver#(apb_seq_item);
`uvm_component_utils(apb_driver)
`uvm_register_cb(apb_driver, apb_drv_cb)  // ← essential
 
task run_phase(uvm_phase phase);
`uvm_do_callbacks(apb_driver, apb_drv_cb, pre_drive(this, req, skip))
endtask
endclass
 
// Debug command: +UVM_CB_TRACE prints every callback invocation:
// vsim +UVM_CB_TRACE work.tb_top
// Output: UVM_CB: apb_driver::apb_drv_cb callback 'pre_drive' called

Bug 2 — Adding Callback After Simulation Starts (Too Late)

SystemVerilog — late callback registration and the correct phase
// ❌ WRONG — registering callback in run_phase ────────────────────────
task run_phase(uvm_phase phase);
phase.raise_objection(this);
// Driver already processing transactions...
#50;
// Adding callback at T=50 — misses all transactions before T=50
uvm_callbacks#(apb_driver, apb_drv_cb)::add(null, cb); // ← WRONG place
start_seq.start(seqr);
phase.drop_objection(this);
endtask
 
// ✓ CORRECT — register in start_of_simulation_phase ──────────────────
function void start_of_simulation_phase(uvm_phase phase);
super.start_of_simulation_phase(phase);
// start_of_simulation runs BEFORE run_phase — all transactions covered
uvm_callbacks#(apb_driver, apb_drv_cb)::add(null, cb);
endfunction
 
// start_of_simulation_phase is the canonical location for callback registration:
// - All components fully constructed (build_phase complete)
// - run_phase not yet started — no transactions missed
// - Callback is active for the entire simulation duration

Bug 3 — Callback Class Not Registered in Factory

SystemVerilog — missing factory registration on callback class
// ❌ WRONG — no factory registration on callback class ────────────────
class apb_err_inject_cb extends apb_drv_cb;
// `uvm_object_utils(apb_err_inject_cb)  ← MISSING!
virtual task pre_drive(apb_driver drv, apb_seq_item req, ref bit skip);
skip = 1;
endtask
endclass
 
// In test:
// apb_err_inject_cb cb = apb_err_inject_cb::type_id::create("cb");
// → null handle! 'type_id' not defined without `uvm_object_utils
 
// ✓ CORRECT — both base and derived need factory registration ─────────
class apb_drv_cb extends uvm_callback;
`uvm_object_utils(apb_drv_cb)           // ← base class registered
virtual task pre_drive(apb_driver drv, apb_seq_item req, ref bit skip);
skip = 0;
endtask
endclass
 
class apb_err_inject_cb extends apb_drv_cb;
`uvm_object_utils(apb_err_inject_cb)   // ← derived class registered
virtual task pre_drive(apb_driver drv, apb_seq_item req, ref bit skip);
skip = 1;
endtask
endclass
 
// Now type_id::create() works correctly for both base and derived.

§11 — Ready-to-Run: Full Callback Demo

A complete, self-contained callback demo. A producer component has two hook points (pre and post). A logger callback and an injector callback are both active. Run it and observe the callback invocation order and the skip mechanism. Ready to Run — Questa / VCS / Xcelium

cb_demo.sv — complete callback demo, compile and run
// cb_demo.sv
// Compile: vlog -sv cb_demo.sv
// Run:     vsim -c work.tb_cb_top +UVM_TESTNAME=cb_demo_test -do "run -all; quit"
 
`include "uvm_macros.svh"
import uvm_pkg::*;
 
// ── Transaction ────────────────────────────────────────────────────────
class demo_txn extends uvm_sequence_item;
`uvm_object_utils(demo_txn)
int id;
bit force_error;
function new(string n="demo_txn"); super.new(n); endfunction
endclass
 
// ── Forward declaration ────────────────────────────────────────────────
typedef class demo_producer;
 
// ── Callback base class ────────────────────────────────────────────────
class demo_cb extends uvm_callback;
`uvm_object_utils(demo_cb)
function new(string n="demo_cb"); super.new(n); endfunction
virtual task pre_process(demo_producer comp, demo_txn txn, ref bit skip);
skip = 0;
endtask
virtual task post_process(demo_producer comp, demo_txn txn);
endtask
endclass
 
// ── Producer component with callback hooks ─────────────────────────────
class demo_producer extends uvm_component;
`uvm_component_utils(demo_producer)
`uvm_register_cb(demo_producer, demo_cb)
 
function new(string n, uvm_component p); super.new(n,p); endfunction
 
task run_phase(uvm_phase phase);
demo_txn txn;
bit      skip;
phase.raise_objection(this);
for (int i=0; i<5; i++) begin
txn    = demo_txn::type_id::create($sformatf("t%0d",i));
txn.id = i;
txn.force_error = (i == 2);  // txn #2 will trigger skip
 
`uvm_info("PROD", $sformatf("Processing txn[%0d]", i), UVM_LOW)
 
skip = 0;
`uvm_do_callbacks(demo_producer, demo_cb,
pre_process(this, txn, skip))
 
if (!skip) begin
`uvm_info("PROD", $sformatf("Normal processing txn[%0d]", i), UVM_LOW)
#10;
end else
`uvm_info("PROD", $sformatf("SKIPPED txn[%0d] by callback", i), UVM_LOW)
 
`uvm_do_callbacks(demo_producer, demo_cb,
post_process(this, txn))
end
phase.drop_objection(this);
endtask
endclass
 
// ── Callback 1: Logger — prints every txn ─────────────────────────────
class logger_cb extends demo_cb;
`uvm_object_utils(logger_cb)
virtual task pre_process(demo_producer comp, demo_txn txn, ref bit skip);
`uvm_info("LOGGER", $sformatf("[PRE]  txn[%0d] force_error=%0b",
txn.id, txn.force_error), UVM_LOW)
endtask
virtual task post_process(demo_producer comp, demo_txn txn);
`uvm_info("LOGGER", $sformatf("[POST] txn[%0d] done", txn.id), UVM_LOW)
endtask
endclass
 
// ── Callback 2: Injector — sets skip on error transactions ────────────
class injector_cb extends demo_cb;
`uvm_object_utils(injector_cb)
virtual task pre_process(demo_producer comp, demo_txn txn, ref bit skip);
if (txn.force_error) begin
`uvm_info("INJECT", $sformatf("Injecting skip on txn[%0d]",
txn.id), UVM_LOW)
skip = 1;
end
endtask
endclass
 
// ── Test: installs both callbacks ──────────────────────────────────────
class cb_demo_test extends uvm_test;
`uvm_component_utils(cb_demo_test)
demo_producer prod;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
prod = demo_producer::type_id::create("prod", this);
endfunction
function void start_of_simulation_phase(uvm_phase phase);
super.start_of_simulation_phase(phase);
uvm_callbacks#(demo_producer,demo_cb)::add(null,
logger_cb::type_id::create("logger"));   // registered first
uvm_callbacks#(demo_producer,demo_cb)::add(null,
injector_cb::type_id::create("injector")); // registered second
endfunction
endclass
 
module tb_cb_top;
initial run_test();
endmodule
 
// Expected output (txn 2 is skipped):
// PROD:   Processing txn[0]
// LOGGER: [PRE]  txn[0] force_error=0
// INJECT: (no action — force_error=0)
// PROD:   Normal processing txn[0]
// LOGGER: [POST] txn[0] done
//
// PROD:   Processing txn[2]
// LOGGER: [PRE]  txn[2] force_error=1   ← logger runs BEFORE injector
// INJECT: Injecting skip on txn[2]       ← injector sets skip=1
//                                          logger already ran, can't stop it
// PROD:   SKIPPED txn[2] by callback     ← driver sees skip=1
// LOGGER: [POST] txn[2] done             ← post_process still runs on all CBs

§12 — Interview Questions

Beginner Level

Intermediate Level

Senior / Architect Level

§13 — Best Practices

RulePracticeWhy It Matters
BP-1Always register callbacks in start_of_simulation_phaseGuarantees active from the first transaction; build_phase is too early (hierarchy incomplete), run_phase is too late (misses early transactions)
BP-2Include both uvm_component_utils` and uvm_register_cb` in every component that uses callbacksMissing `uvm_register_cb causes silent non-invocation — the most common callback bug
BP-3Register both base and derived callback classes with ``uvm_object_utils`type_id::create() requires factory registration; missing it causes null handles or compilation failures
BP-4Use add(specific_component, cb) rather than add(null, cb) when targeting one instancenull applies to ALL instances of the component type — can inadvertently affect slave agents when only master is intended
BP-5Keep callback hook methods as virtual task even if they don't consume timeTasks can call both functions and other tasks; declaring as function restricts future extension without breaking the API
BP-6Document every hook point in the VIP with a comment explaining what the callback can and cannot doWithout documentation, users don't know what state is safe to modify at each hook — leading to subtle simulation errors
BP-7Enable +UVM_CB_TRACE during debug to see every callback invocationInstantly confirms whether callbacks are firing, in what order, and for which component instance
BP-8Use callbacks for error injection, delay injection, and logging — never for functional changes to the VIP behaviourCallbacks that change fundamental VIP behaviour make the VIP unpredictable for other projects; keep the base component deterministic

§14 — Summary

ActorRoleKey Macro / MethodTypical Location
Callback Base ClassDefines virtual hook methods; extends uvm_callback``uvm_object_utils`VIP package — alongside the component
Component (host)Registers the callback pool; fires hook pointsuvm_register_cb` + uvm_do_callbacks`VIP driver, monitor, or sequencer
Callback Object (extension)Overrides virtual hooks; implements test-specific behaviour``uvm_object_utils` + override virtual methodsTest package — per-project extension
RegistrationConnects callback object to the target componentuvm_callbacks#(comp,cb)::add(comp, obj)Test's start_of_simulation_phase