Skip to content

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

UVM callbacks three-actor architecture
UVM callbacks three-actor architecture

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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
## ── 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ═══ 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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

SystemVerilog — missing register_cb macro and the fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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.

uvm_register_cb records the legal type pair; add checks it and either appends to the pool or warns with CBUNREG; uvm_do_callbacks iterates the pool in order, skipping disabled callbacks`uvm_register_cb(T, CB)records the legal type pairuvm_callbacks#(T,CB)::add()is this pair registered?Component's callbackpoolordered list, per instanceUVM_WARNING CBUNREGadd() refuses; pool stays empty`uvm_do_callbacks in thedriveriterates the pool inregistration ordercallback_mode(0)registered but skippedpair knownregistered → appendNOT registered →refuseiterateskipped duringiteration12
Figure 2 — what add() checks, and where an unregistered pair stops. The uvm_register_cb macro records that a callback type is legal for an object type. When a test calls uvm_callbacks::add(), UVM first tests that pair against the registry: if it is registered the callback is appended to that component's pool, and if it is not, add() refuses and issues a UVM_WARNING with the ID CBUNREG. At run time the component's uvm_do_callbacks macro iterates whatever is in the pool, in registration order, skipping any callback whose callback_mode is 0. An unregistered pair therefore produces an empty pool and a silent-looking run — the evidence exists, but as one warning during build rather than as an error at the hook.
1

An error-injection callback was registered on every run and injected nothing

CALLBACK-NEVER-FIRED
Symptom

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

Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endfunction
Diagnostic Evidence

The evidence was in the log the whole time, eleven lines into a forty-thousand line run:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 added

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`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
Root Cause

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

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class apb_driver extends uvm_driver #(apb_txn);
  `uvm_component_utils(apb_driver)
  `uvm_register_cb(apb_driver, apb_drv_cb)     // <-- the missing line
  ...
endclass

Verifying 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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")
endfunction

That 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class cbunreg_is_fatal extends uvm_report_catcher;
  function action_e catch();
    if (get_id() == "CBUNREG") set_severity(UVM_ERROR);
    return THROW;
  endfunction
endclass

The 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 — complete callback demo, compile and run
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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_callback and uvm_callbacks#(T, CB). The typed callback pool, add() / delete(), the UVM_APPEND and UVM_PREPEND ordering arguments, and callback_mode() for enabling and disabling an already-registered callback.
  • IEEE 1800.2-2020 — callback macros. `uvm_register_cb for registering a type pair, `uvm_do_callbacks for iterating every enabled callback in registration order, and `uvm_do_callbacks_exit_on for terminating the iteration on a returned value.
  • IEEE 1800.2-2020 — reporting. The UVM_WARNING issued with ID CBUNREG when add() is called for an unregistered type pair, and uvm_report_catcher for promoting it to an error.
  • IEEE 1800.2-2020 — phasing. The build_phase / end_of_elaboration_phase / run_phase ordering 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

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

Continue learning