UVM
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. This is the property that distinguishes callbacks from factory overrides, where the last registered override wins and the others are simply replaced.
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
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 and Debugging
Bug 1 — Forgetting `uvm_register_cb in the Component
// ❌ 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.§10b — The Callback That Never Fired
Every other callback bug announces itself. This one does not: the registration code is correct, the callback class is correct, the hook is correct, and nothing runs.
An error-injection callback was registered on every run and injected nothing
CALLBACK-NEVER-FIREDAn error-injection test reported a clean pass on every run. That was the bug: the test existed to corrupt one transaction in fifty and confirm the scoreboard caught it, so a clean pass meant either the DUT tolerated corruption or the corruption never happened.
Everything about the setup looked right. The callback class compiled, the test
constructed it, uvm_callbacks#(apb_driver, apb_drv_cb)::add(...) executed —
confirmed with a uvm_info immediately after — and the driver contained a
`uvm_do_callbacks at the intended hook. No errors, no fatals, and the run
completed normally.
// The callback class - fine.
class apb_drv_cb extends uvm_callback;
`uvm_object_utils(apb_drv_cb)
virtual function void pre_drive(apb_driver drv, apb_txn req);
if (req.id % 50 == 0) req.data ^= 32'hFFFF_FFFF; // inject
endfunction
endclass
// The driver - fine.
class apb_driver extends uvm_driver #(apb_txn);
`uvm_component_utils(apb_driver)
// ✗ MISSING: `uvm_register_cb(apb_driver, apb_drv_cb)
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
`uvm_do_callbacks(apb_driver, apb_drv_cb, pre_drive(this, req))
drive(req);
seq_item_port.item_done();
end
endtask
endclass
// The test - fine.
function void err_test::build_phase(uvm_phase phase);
super.build_phase(phase);
cb = apb_drv_cb::type_id::create("cb");
uvm_callbacks#(apb_driver, apb_drv_cb)::add(env.apb_agent.drv, cb);
`uvm_info("TEST", "callback added", UVM_LOW) // this printed
endfunctionThe evidence was in the log the whole time, eleven lines into a forty-thousand line run:
UVM_WARNING @ 0: reporter [CBUNREG] Callback cb cannot be registered with object
uvm_test_top.env.apb_agent.drv because the callback type
apb_drv_cb is not registered with the object type apb_driver
UVM_INFO @ 0: uvm_test_top [TEST] callback addedThe two lines are adjacent and say opposite things. The uvm_info reports that
the test's add() call executed, which is true; the warning reports that the
call did not attach anything, which is also true. Reading only the one the test
printed is what made the bug invisible.
Confirming took one query — the pool was empty:
`uvm_info("DBG", $sformatf("callbacks attached: %0d",
uvm_callbacks#(apb_driver, apb_drv_cb)::get_all(cbs, env.apb_agent.drv)),
UVM_LOW)
// -> callbacks attached: 0`uvm_register_cb(T, CB) is what tells UVM that a callback type is legal for
an object type. uvm_callbacks#(T,CB)::add() checks that registration before it
will attach anything, and on a miss it refuses and warns rather than attaching
and failing later.
So the component's pool stayed empty, `uvm_do_callbacks iterated zero
entries, and every transaction was driven unmodified. Nothing in the test's own
output could reveal this, because from the test's point of view the add() call
returned normally.
The severity comes from what the test is for. An error-injection test that injects nothing does not fail — it passes, and its passing is taken as evidence that the checker works. The scoreboard, the coverage on injected-error scenarios, and the whole negative-testing story were all reporting green on a stimulus that never happened. A test that cannot fail is worth less than no test, because it occupies the slot where a real one would have gone.
class apb_driver extends uvm_driver #(apb_txn);
`uvm_component_utils(apb_driver)
`uvm_register_cb(apb_driver, apb_drv_cb) // <-- the missing line
...
endclassVerifying the fix means checking that injection happened, not that the run passed. The test should count its own injections and fail if the count is zero:
function void err_test::check_phase(uvm_phase phase);
super.check_phase(phase);
if (cb.n_injected == 0)
`uvm_error("TEST", "error-injection test injected nothing - callback never fired")
endfunctionThat single check converts this entire failure class from a silent pass into an error, and it applies to any callback whose purpose is to do something rather than to observe.
The complementary guard is at the framework level. Promote the warning to an error so no run can start with an unattached callback:
class cbunreg_is_fatal extends uvm_report_catcher;
function action_e catch();
if (get_id() == "CBUNREG") set_severity(UVM_ERROR);
return THROW;
endfunction
endclassThe general habit is worth more than either fix: a test that can only pass is
not a test. Any negative test — error injection, protocol violation, timeout
provocation — should assert in check_phase that the thing it exists to provoke
actually occurred. See report catchers for the
promotion mechanism and the UVM factory for the
alternative extension model this one is often confused with.
§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
Factory overrides replace an entire component with a different class; callbacks add behaviour at specific points inside an existing component without replacing it.
That difference decides which one you reach for. If you need a fundamentally different driver — a different protocol, a different architecture — override the type and supply your own implementation. If you need the existing, verified driver plus a small project-specific behaviour at a known point, a callback plugs into pre_drive() and post_drive() while the driver's code stays exactly as shipped.
The reason this matters for third-party VIP is that overriding means owning. An override replaces the vendor's implementation with yours, so every vendor fix has to be re-merged into your copy. A callback leaves the vendor's class untouched and survives a VIP upgrade, provided the hook points survive.
Callbacks also compose in a way overrides do not: three callbacks on the same driver all run, in registration order, at every hook. Three overrides of the same type do not — the last one registered wins.
`uvm_register_cb(comp_type, cb_type) registers the pair — it tells UVM that this callback type is legal for that object type, which is what uvm_callbacks#(comp, cb)::add() checks before it will attach anything.
Omit it and add() refuses. The callback object is constructed, your add() call executes, and the callback is never attached, so `uvm_do_callbacks iterates an empty list and nothing fires.
The detail worth knowing precisely, because it is the whole diagnosis: this is not silent. UVM issues a UVM_WARNING with the ID CBUNREG at the add() call, saying the callback cannot be registered with that object type. What makes the bug hard is not the absence of evidence but its position — a single warning early in build_phase, in a log that usually contains thousands of UVM_INFO lines, on a run that then completes normally with no errors.
So the diagnostic is a grep, not a waveform: search the log for CBUNREG before doing anything else. The fix is one macro at the top of the component class, and the durable guard is a report catcher that promotes CBUNREG to an error.
Either works, and the constraint that actually matters is that the registration must complete before the hook it targets is first reached — which in practice means before run_phase starts.
build_phase is the conventional choice because the component being targeted must exist for add() to attach to it, and build is where the hierarchy comes into being. If you add in the test's build_phase, call super.build_phase() first so the environment and its agents have been constructed; adding before that means env.apb_agent.drv is a null handle.
start_of_simulation_phase and end_of_elaboration_phase are both safe alternatives and are sometimes preferable, because the whole hierarchy is guaranteed built by then and you can look components up by path rather than by handle.
What does not work is adding during run_phase. Callbacks are consulted when the hook executes, so a callback added at time 500 has no effect on the two hundred transactions that were driven before it. That is a legitimate technique for deliberately switching behaviour mid-test, and a bug when it happens by accident.
It runs, and it sees the flag — because `uvm_do_callbacks iterates every enabled callback in registration order regardless of what the earlier ones did.
That is the important part: a callback setting a status field does not by itself stop the iteration or the driver. The field is just data on a shared transaction or on the callback's argument, so the second callback observes whatever the first wrote, and the driver is responsible for checking the flag after the iteration completes and acting on it.
If you want the iteration itself to stop, that is a different macro: `uvm_do_callbacks_exit_on(T, CB, METHOD, VAL) halts as soon as a callback returns the specified value. Use it when the callbacks are alternatives rather than layers — the first one that handles the transaction ends the chain.
The practical consequence is ordering sensitivity. Layered callbacks that each modify the transaction produce a different result depending on registration order, so order becomes part of the test's specification rather than an accident. Where order matters, register in one place and comment why.
Put the hook in the driver unconditionally and the policy in the callback. The driver calls `uvm_do_callbacks(apb_driver, apb_drv_cb, pre_drive(this, req)) on every transaction; with no callbacks registered the list is empty and the call costs a list traversal of zero entries. The clean test therefore needs no configuration at all — it simply never registers anything.
The error-injection test registers one callback whose pre_drive decides per transaction: match on an address range, a transaction id, a counter, or a randomised rate, and corrupt only the matching ones. That keeps the selection logic in the test where it belongs, and keeps the driver free of test-specific conditionals — which is the whole point, because a driver full of if (inject_errors) branches is a driver that every test has to reason about.
Two refinements are worth having. Give the callback a callback_mode(0) / callback_mode(1) switch so a long test can enable injection for a window rather than registering and removing. And have the callback log every injection with the transaction id, because a scoreboard mismatch caused by deliberate injection and one caused by a real bug look identical unless the injector says which is which.
Four causes, in the order worth checking.
Grep the log for CBUNREG first. A missing `uvm_register_cb makes add() refuse to attach, and UVM warns exactly once at that point. This is the most common cause and the cheapest to rule out.
Confirm the target instance. add() takes a component handle, and attaching to env.agent0.drv does nothing for transactions driven by env.agent1.drv — see agent architecture for where those handles live. Print get_full_name() on the handle you passed and compare it against print_topology().
Check the timing. A callback added during run_phase has no effect on hooks already executed. If registration happens in a phase that runs after the driver started, every transaction before that point ran without it.
Check callback_mode. A callback disabled with callback_mode(0) stays registered and is skipped during iteration, so it appears in the pool and never runs.
If all four are clean, the remaining possibility is that the hook is not on the path being exercised — the driver has a `uvm_do_callbacks in one branch and the transactions are taking another. Driver debugging covers isolating which path a transaction actually took. Confirm by putting a uvm_info beside the hook itself rather than inside the callback.
Where This Is Specified
- IEEE 1800.2-2020 (UVM) —
uvm_callbackanduvm_callbacks#(T, CB). The typed callback pool,add()/delete(), theUVM_APPENDandUVM_PREPENDordering arguments, andcallback_mode()for enabling and disabling an already-registered callback. - IEEE 1800.2-2020 — callback macros.
`uvm_register_cbfor registering a type pair,`uvm_do_callbacksfor iterating every enabled callback in registration order, and`uvm_do_callbacks_exit_onfor terminating the iteration on a returned value. - IEEE 1800.2-2020 — reporting. The
UVM_WARNINGissued with IDCBUNREGwhenadd()is called for an unregistered type pair, anduvm_report_catcherfor promoting it to an error. - IEEE 1800.2-2020 — phasing. The
build_phase/end_of_elaboration_phase/run_phaseordering that determines when a registration is early enough to affect a hook. - Accellera UVM User Guide — callbacks. The non-invasive extension model and the convention of placing hooks in VIP that tests may extend without modifying the component.
§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 |