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.
// ── 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
endclassStep 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.
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
endclassStep 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.
// ── 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.
// ── 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
// 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
// ═══════════════════════════════════════════════════════════════════════## ── 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 aboveQuick 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)
| API | Effect |
|---|---|
| ``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.
// ═══ 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
endclassExample 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.
// ── 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
endclassExample 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.
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
endclassExample 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.
// ── 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.
// ❌ 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' calledBug 2 — Adding Callback After Simulation Starts (Too Late)
// ❌ 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 durationBug 3 — Callback Class Not Registered in Factory
// ❌ 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
// 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
| Rule | Practice | Why It Matters |
|---|---|---|
| BP-1 | Always register callbacks in start_of_simulation_phase | Guarantees active from the first transaction; build_phase is too early (hierarchy incomplete), run_phase is too late (misses early transactions) |
| BP-2 | Include both uvm_component_utils` and uvm_register_cb` in every component that uses callbacks | Missing `uvm_register_cb causes silent non-invocation — the most common callback bug |
| BP-3 | Register 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-4 | Use add(specific_component, cb) rather than add(null, cb) when targeting one instance | null applies to ALL instances of the component type — can inadvertently affect slave agents when only master is intended |
| BP-5 | Keep callback hook methods as virtual task even if they don't consume time | Tasks can call both functions and other tasks; declaring as function restricts future extension without breaking the API |
| BP-6 | Document every hook point in the VIP with a comment explaining what the callback can and cannot do | Without documentation, users don't know what state is safe to modify at each hook — leading to subtle simulation errors |
| BP-7 | Enable +UVM_CB_TRACE during debug to see every callback invocation | Instantly confirms whether callbacks are firing, in what order, and for which component instance |
| BP-8 | Use callbacks for error injection, delay injection, and logging — never for functional changes to the VIP behaviour | Callbacks that change fundamental VIP behaviour make the VIP unpredictable for other projects; keep the base component deterministic |
§14 — Summary
| Actor | Role | Key Macro / Method | Typical Location |
|---|---|---|---|
| Callback Base Class | Defines virtual hook methods; extends uvm_callback | ``uvm_object_utils` | VIP package — alongside the component |
| Component (host) | Registers the callback pool; fires hook points | uvm_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 methods | Test package — per-project extension |
| Registration | Connects callback object to the target component | uvm_callbacks#(comp,cb)::add(comp, obj) | Test's start_of_simulation_phase |