Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
    • Callbacks, Command Line, Seeds and Versions
      • Callbacks Done Right
      • The Command Line in Full
      • Random Stability and Seeds
      • UVM 1.2 to IEEE 1800.2: The Delta Table
      • Performance at Scale
    • Synchronisation and Reporting
    • Testbenches at Scale
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Callbacks, Command Line, Seeds and Versions
  1. Topics
  2. UVM Verification
  3. Advanced UVM in Practice
  4. Callbacks, Command Line, Seeds and Versions
  5. Callbacks Done Right

Part 10 · Advanced Topics · Intermediate

Callbacks Done Right

uvm_callback classes, uvm_register_cb and uvm_do_callbacks, ordering, typewide versus instance callbacks, callback_mode, cost and debug, and when a factory override or a virtual hook is the better tool.

On this page

    The interviewer has your CV open at "designed fault injection for a DMA VIP". "Your driver has callbacks. A test adds a truncation callback and a beat counter. In which order do they run? What does the counter see? What does the callback site cost when nobody adds anything? And how would you know if a fault callback never fired?" Four questions, and each one has a precise answer you can show in a log. This lesson builds the callback machinery on the DMA bench and answers all four.

    Why a hook needs three owners

    A callback is an object you attach to a running component so that, at a fixed point in its code, the component calls a method on your object. The component's author decides where the hook is and what it may change; a test decides what happens there and when. That split is the whole value: the VIP stays one file that nobody forks, and every project plugs its own behaviour into the same sockets.

    Think of a socket built into a workbench. The bench's maker chose where the socket goes and what voltage it carries (the hook's signature). A virtual hook with a factory override is replacing the whole bench with one that has a different tool bolted on. A callback is plugging a power strip into the socket: you can plug in two tools, in an order, and unplug one halfway through the job. Three pieces make it work, and each has a different owner: the callback class (VIP author), the registration and call site (VIP author), and the add/delete (test writer).

    What one callback site does at run time

    Flowchart of a callback site: pre_drive calls the uvm_do_callbacks macro, which creates an iterator over the object's queue (typewide callbacks first, then instance callbacks in append or prepend order). For each callback, if callback_mode is on its pre_beat method runs, otherwise it is skipped; then the next callback, then the beat is returned.
    The macro expands to a loop. It builds an iterator over this object's callback queue, calls the method on every enabled callback in queue order, and each callback sees what the previous one did to the beat.

    Two details in that picture explain most surprises. First, the instance queues live in one global pool keyed by the object, shared by every uvm_callbacks#(T, CB) specialisation; typewide callbacks are kept per host type T. Second, the macro calls the method directly on each callback, so every callback must cope with what an earlier one left behind, including a beat set to null.

    Build it on the DMA's stream driver

    The callback class and the host

    The hook's signature is the contract. This one receives the driver (so a callback can read its config), the beat index within the packet, and the beat by inout, so a callback can replace it with a modified copy or drop it with null. The host registers the pair with uvm_register_cb and calls the list with uvm_do_callbacks inside the bench's existing pre_drive hook. In a real VIP both lines go into axis_driver itself; the course bench is never edited, so the scratch run puts them in a subclass that one type override swaps in.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // snippet: scratch run alongside the DMA bench
    typedef class axis_cb_driver;
    
    class axis_beat_cb extends uvm_callback;
      `uvm_object_utils(axis_beat_cb)
      function new(string name = "axis_beat_cb");
        super.new(name);
      endfunction
      // Called once per beat before it is driven: replace beat, or set it to null to drop it.
      virtual function void pre_beat(axis_cb_driver drv, int unsigned idx, inout axis_item beat);
      endfunction
    endclass
    
    class axis_cb_driver extends axis_driver;
      `uvm_component_utils(axis_cb_driver)
      `uvm_register_cb(axis_cb_driver, axis_beat_cb)
      int unsigned beat_no;
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
      function axis_item pre_drive(axis_item it);
        axis_item beat = it;
        `uvm_do_callbacks(axis_cb_driver, axis_beat_cb, pre_beat(this, beat_no, beat))
        beat_no = it.last ? 0 : beat_no + 1;
        return beat;
      endfunction
    endclass

    Decide function or task per hook when you design it: overrides must match, so a function hook can never wait. Timing faults (extra idle cycles) need a task hook where waiting is legal.

    A fault and an observer

    The truncation callback is the bench's axis_truncating_driver moved into a callback, clone included: the sequence's own item is never edited. The counter is an observer that records how many beats it saw and how many were still alive when it ran.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // snippet: scratch run alongside the DMA bench
    class axis_trunc_cb extends axis_beat_cb;
      `uvm_object_utils(axis_trunc_cb)
      int unsigned cut = 3;
      function new(string name = "axis_trunc_cb");
        super.new(name);
      endfunction
      function void pre_beat(axis_cb_driver drv, int unsigned idx, inout axis_item beat);
        axis_item b;
        if (beat == null || idx < cut) return;      // already dropped, or before the cut
        if (idx > cut) begin beat = null; return; end
        $cast(b, beat.clone());                     // never edit the sequence's item
        b.last = 1'b1;
        beat = b;
        `uvm_info_context("TRUNC_CB", $sformatf("%s: TLAST forced on beat %0d", get_name(), idx), UVM_LOW, drv)
      endfunction
    endclass
    
    class axis_count_cb extends axis_beat_cb;
      `uvm_object_utils(axis_count_cb)
      int unsigned seen, driven;
      function new(string name = "axis_count_cb");
        super.new(name);
      endfunction
      function void pre_beat(axis_cb_driver drv, int unsigned idx, inout axis_item beat);
        seen++;
        if (beat != null) driven++;
      endfunction
    endclass

    uvm_info_context with the driver puts the driver's path on the line. A plain uvm_info inside a callback reports as reporter, because a callback is an object, not a component.

    The test: attach after the driver exists

    A callback attaches to an object, so the object must exist: add it in connect_phase or end_of_elaboration_phase, never in the test's build_phase, where the agent's children have not been built yet. display() prints the queue, and is the first thing to call when a callback seems not to fire.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet: scratch run alongside the DMA bench (drv is env.in_agt.drv cast to axis_cb_driver)
    function void end_of_elaboration_phase(uvm_phase phase);
      trunc = axis_trunc_cb::type_id::create("trunc");
      cnt   = axis_count_cb::type_id::create("cnt");
      uvm_callbacks#(axis_cb_driver, axis_beat_cb)::add(drv, trunc);              // UVM_APPEND
      uvm_callbacks#(axis_cb_driver, axis_beat_cb)::add(drv, cnt);                // runs after trunc
      // uvm_callbacks#(axis_cb_driver, axis_beat_cb)::add(drv, cnt, UVM_PREPEND); // runs before trunc
      uvm_callbacks#(axis_cb_driver, axis_beat_cb)::display(drv);
    endfunction
    // During run_phase: void'(trunc.callback_mode(0)) switches it off,
    // uvm_callbacks#(axis_cb_driver, axis_beat_cb)::delete(drv, trunc) removes it.

    Call

    What it does

    add(obj, cb, UVM_APPEND)

    Attach to one instance, at the back (the default) or the front with UVM_PREPEND.

    add(null, cb)

    Typewide: every instance of T, including ones created later. A null handle by accident means all instances.

    add_by_name("*.in_agt.drv", cb, root)

    Attach to every component under root whose name matches the pattern.

    delete(obj, cb) / delete_by_name(...)

    Detach. Legal at run time, between callback sites.

    cb.callback_mode(0 or 1)

    Switch one callback off or on without removing it; returns the previous mode.

    display(obj)

    Print what is attached where, with the on/off state.

    uvm_do_obj_callbacks(T, CB, OBJ, M)

    Call the list of another object, for example an item's callbacks from inside its driver.

    uvm_do_callbacks_exit_on(T, CB, M, VAL)

    Stop at the first callback whose bit-returning method returns VAL, and return it: a veto.

    uvm_set_super_type(T, ST)

    Let a derived host inherit typewide callbacks registered on its base type.

    Watch it run: order is a design decision

    The scratch run first appends the counter after the truncation callback. The display table shows the queue, and the counter's numbers show it ran second:

    Diagram
    UVM_INFO uvm_callback.svh(511) @ 0: reporter [UVM/CB/DISPLAY] Registered callbacks for instance uvm_test_top.env.in_agt.drv of axis_cb_driver
    trunc   uvm_test_top.env.in_agt.drv on   ON
    cnt     uvm_test_top.env.in_agt.drv on   ON
    UVM_INFO adv_tb.sv(53) @ 275000: uvm_test_top.env.in_agt.drv [TRUNC_CB] trunc: TLAST forced on beat 3
    UVM_INFO adv_tb.sv(129) @ 385000: uvm_test_top [TRUNC] DMA reported ERR for the short packet, as expected
    UVM_INFO adv_tb.sv(130) @ 385000: uvm_test_top [CB] count_cb (after trunc): saw 8 beats, 4 driven
    UVM_INFO dma_tb.sv(411) @ 485000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
    • 275000, 385000, 485000 are the same times as the bench's dma_truncate_test, which does the same cut with a factory override. The behaviour moved; it did not change.

    • saw 8, 4 driven: the counter ran after the cut, so beats 4 to 7 reached it as null. With UVM_PREPEND the same run prints saw 8 beats, 8 driven: the counter now runs before the cut.

    • In a second scratch test the callback is switched off, on, then deleted across three 6-word jobs: STATUS reads 00000002 (DONE), 00000004 (ERR), 00000002, and the scoreboard ends at 16 words matched (6 + 4 + 6). One test turned a fault on and off, which no override can do.

    Callback, factory override or virtual hook?

    The bench already has the other two mechanisms. axis_driver declares a virtual pre_drive hook that does nothing; axis_truncating_driver overrides it, and dma_truncate_test swaps it in by type override:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // snippet: from the course DMA bench
    // Fault injection without touching the sequence: cuts every packet after beat
    // `cut` by setting TLAST there and dropping the rest of the packet.
    class axis_truncating_driver extends axis_driver;
      `uvm_component_utils(axis_truncating_driver)
      int unsigned cut = 3;           // index of the beat that gets TLAST
      int unsigned beat_no;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function axis_item pre_drive(axis_item it);
        axis_item b;
        int unsigned i = beat_no;
        beat_no = it.last ? 0 : beat_no + 1;    // count beats per packet
        if (i > cut) return null;                // after the cut: drop the beat
        if (i < cut) return it;
        $cast(b, it.clone());                    // never edit the sequence's own item
        b.last = 1'b1;
        `uvm_info("TRUNC", $sformatf("TLAST forced on beat %0d", i), UVM_MEDIUM)
        return b;
      endfunction
    endclass

    Question

    Virtual hook + override

    Callback

    Who must prepare the code

    A virtual method in the component

    A registered callback class and a call site

    When the choice is made

    Before the component is created

    Any time after it exists

    Two behaviours at once

    Needs a class that has both

    Two callbacks, run in queue order

    Scope

    All instances (type) or one path (instance)

    One object, or typewide with null

    Turn off mid-test

    No

    callback_mode(0) or delete

    Can change the driver's state or flow

    Yes: it is the driver

    Only through what the hook passes in

    Use an override when a whole component changes for a whole test (the bench's reset-aware drivers), a callback when small behaviours must combine, target one instance or switch mid-test, and a plain virtual hook inside your own code. Error injection specifically, and the scoreboard side of an expected fault, is in Callbacks versus factory overrides and Checking expected faults.

    What a callback site costs, and how to debug one

    The macro creates a uvm_callback_iter object and walks the queue every time it runs, even when the queue is empty. Scratch measurements of one million calls of an empty site took 0.15 to 0.18 s on Verilator across runs (about 0.15 µs per call, on an Apple M4 Pro; the performance lesson table has the 0.18 s run). That is nothing once per transaction and real money once per clock in a 200-lane monitor. Put sites at transaction boundaries, not in per-cycle loops.

    • Count firing. Every fault callback counts how often it fired, and the test checks the count; a fault test that checks only for "no errors" passes when the fault never happens.

    • Print the queue. uvm_callbacks#(T, CB)::display() shows typewide entries as (*) and each instance's queue.

    • Trace the library. Compile with +define+UVM_CB_TRACE_ON and every add, delete and execution is reported (the macro bodies include uvm_cb_trace_noobj calls that are empty otherwise).

    • Report catchers are callbacks too. uvm_report_catcher extends uvm_callback; so do uvm_reg_cbs and uvm_objection_callback. The same add, order and display rules apply. Catchers are covered in Synchronisation and reporting.

    Note

    In IEEE 1800.2: the callback API (uvm_callback, uvm_callbacks#(T,CB), uvm_register_cb, uvm_do_callbacks, uvm_set_super_type) is the same as in UVM 1.2. One related rename: uvm_event::add_callback() is a 1.2 method; the standard attaches event callbacks with uvm_callbacks#(...)::add, and the 2020.3 library keeps add_callback only for compatibility.

    What breaks: two fault tests that tested nothing

    The fault that never fired. A test attached the truncation callback typewide, but named the base class as the host: uvm_callbacks#(axis_driver, axis_beat_cb)::add(null, trunc). The driver that calls the list is axis_cb_driver, and typewide queues are kept per host type. In the scratch run, the only trace was one warning, and the job finished cleanly:

    Diagram
    UVM_WARNING @ 0: reporter [CBUNREG] Callback trunc cannot be registered with object (*) because callback type axis_trunc_cb is not registered with object type uvm_object
    UVM_INFO adv_tb.sv(228) @ 605000: uvm_test_top [CB] STATUS=00000002
    UVM_INFO dma_tb.sv(411) @ 705000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched

    STATUS=2 is DONE: the packet went through whole. The same wrong type with an instance add behaves differently, which makes the warning hard to read: it prints the same CBUNREG warning, yet the callback still fires (STATUS=4, ERR), because instance queues are keyed by the object, not by T. Treat CBUNREG as an error (uvm_root::get().set_report_severity_id_override(UVM_WARNING, "CBUNREG", UVM_ERROR) in the base test) and make every fault test check its firing count.

    The corruption nobody saw. A second callback flipped bit 0 of beat 2's data, to prove the scoreboard catches corrupted words. The scratch run reported beat 2: data cbccc8de -> cbccc8df and then 8 words matched, 0 mismatched. The scoreboard was right. The input monitor watches the pins, so it saw the corrupted word, and the scoreboard predicted the DMA's output from it. A fault injected before the observation point is just different stimulus. To test the DUT, inject faults the DUT must detect (the early TLAST gives STATUS.ERR); to test the checker, corrupt what the checker compares, or have the DUT path produce the wrong word, as the bench's DMA_BUG_WORD6 build does.

    Interview

    "Your VIP's driver has callbacks. A test adds two. In which order do they run, and what does the site cost when nobody adds any?"

    A weak answer: "They run in the order they were added, and callbacks are basically free." Half right, and it misses typewide entries, UVM_PREPEND, and the iterator built on every call.

    A strong answer: "Each object has a queue: typewide callbacks first, then instance callbacks, appended by default or put in front with UVM_PREPEND. uvm_do_callbacks builds an iterator and calls the method on every enabled callback, so each sees the previous one's result; a dropped beat arrives as null. An empty site still builds the iterator, about 0.15 µs per call in our Verilator measurement, so I put sites on transaction boundaries. I attach after build, check the queue with display(), promote CBUNREG to an error, and every fault callback counts how often it fired."

    Follow-ups: How do you inject a fault on one of two identical drivers? (an instance add on that driver's handle, or add_by_name with its path) and Why can't you add the callback in the test's build_phase? (the driver does not exist yet; a null handle would silently become typewide).

    Check yourself A subsystem has two axis_cb_driver instances. A test intends to truncate packets on dma1 only, and writes uvm_callbacks#(axis_cb_driver, axis_beat_cb)::add(drv1, trunc) in its build_phase, right after super.build_phase(). What happens?

    drv1 cannot have been assigned yet: the test's build_phase creates the env, but the agents and drivers are built later, top-down, so the handle is still null. add with a null object is a typewide add, so the callback lands on both drivers and both DMAs see short packets. Move the add to connect_phase or end_of_elaboration_phase, when the handle is real.

    Check yourself A callback must be able to veto a beat: if any callback says "drop", the remaining callbacks should not run. Which macro do you use, and what does the hook look like?

    Give the hook a bit return value, for example virtual function bit allow(axis_cb_driver drv, axis_item it), and call it with uvm_do_callbacks_exit_on(axis_cb_driver, axis_beat_cb, allow(this, it), 0) as the last statement of a function returning bit. The macro returns 0 at the first callback that returns 0, and 1 if none does.

    Check yourself A colleague proposes making axis_count_cb the first callback with UVM_PREPEND "so the count is right". When is that the wrong choice?

    It depends on what the count means. Prepended, it counts the beats the sequence produced (8 of 8). Appended, it counts the beats actually driven (4 of 8). A coverage or firing check usually wants what reached the pins, so it should run last. Write down which one you mean, because the order is part of the result.

    Next: a callback is chosen in code; most run-to-run variation comes from the command line. The command line in full shows every built-in plusarg, and the typos that the library silently ignores.

    Related topics

    • Callbacks versus factory overrides

    • Driver injection hooks

    • Register field callbacks

    • Synchronisation and reporting

    Previous

    169 / 280

    Next: The Command Line in Full

    Up next

    Up nextThe Command Line in Full

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Random Stability and Seeds
    • UVM 1.2 to IEEE 1800.2: The Delta Table
    • Performance at Scale