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.

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

§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

Beginner Level

Intermediate Level

Senior / Architect Level

§13 — Best Practices

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

§14 — Summary

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