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
    • Agent Architecture
    • Reusable VIP: Active, Passive and Protocol Agents
    • Error and Fault Injection
      • Sequence-Directed Faults
      • Driver Injection Hooks
      • Callbacks versus Factory Overrides
      • Checking Expected Faults
    • Agent Patterns: Slaves, Reset, Interrupts and Pipelines
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Error and Fault Injection
  1. Topics
  2. UVM Verification
  3. UVM Agents & Protocol VIP
  4. Error and Fault Injection
  5. Callbacks versus Factory Overrides

Part 6 · Agents & Protocol IP · Intermediate

Callbacks versus Factory Overrides

Why factory overrides do not compose, and how a callback adds a removable, stackable fault to one driver instance: uvm_register_cb, uvm_do_callbacks and add/delete done right.

On this page

    dma_reset_test overrides axis_driver with axis_driver_ra. dma_truncate_test overrides it with axis_truncating_driver. Now the test plan asks for a short packet and a reset in the middle of the next job, in one test. The factory has one slot for axis_driver, and it holds one class. You could write axis_truncating_driver_ra. Next month someone wants corrupted data with reset, then truncation with corruption, and the class list grows as the product of every feature. Is there a way to add fault behaviour without choosing one class?

    The idea: replace the worker, or hand them an extra step

    A factory override replaces the worker: the bench builds a different class in that slot, chosen once, at build time. A callback keeps the worker and hands them an extra step on their checklist: an object attached to one component instance, called at a fixed point, which can be added, stacked with others and removed while the test runs. Overrides are the right tool when a whole component behaves differently. Callbacks are the right tool for small behaviours that must combine.

    Factory override

    Callback

    What changes

    The class that is built

    Extra objects called by one instance

    When it is decided

    Before the component is created (build)

    Any time after it exists, add or delete

    Combining two behaviours

    Needs a class that has both

    Add two callbacks; they run in order

    Scope

    Every instance (type) or one path (instance)

    One instance, or all instances with a null object

    What the VIP must provide

    Virtual methods to override

    A registered callback class and a call site

    The picture

    Two panels: on the left, axis_driver with two derived classes of which a test may choose one; on the right, one callback-capable driver whose pre_drive calls a list of callback objects, truncate_cb then corrupt_cb.
    An override chooses one derived class per slot. Callbacks attach several small behaviours to one driver instance, called in order.

    Build it: the three pieces of a callback

    A callback needs three pieces, and missing any one of them fails quietly. The VIP author writes the first two once; tests write the third.

    1. The callback class: the hook's signature

    The callback base class extends uvm_callback and declares the hook as a virtual method that does nothing. It receives the driver (so a callback can read its configuration) and the beat by inout, so each callback in a list sees the result of the one before. Setting the beat to null drops it.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet: scratch run alongside the DMA bench
    typedef class axis_cb_driver;
    class axis_driver_cb extends uvm_callback;
      `uvm_object_utils(axis_driver_cb)
      function new(string name = "axis_driver_cb");
        super.new(name);
      endfunction
      // beat: the beat to drive; set it to null to drop it.
      virtual function void pre_drive(axis_cb_driver drv, inout axis_item beat);
      endfunction
    endclass

    2. The driver: register the pair, call the list

    uvm_register_cb(T, CB) tells UVM that objects of type T accept callbacks of type CB. uvm_do_callbacks(T, CB, METHOD) walks every callback attached to this and calls the method on each. In a VIP, both lines go into axis_driver itself, inside its pre_drive. The course bench is not edited, so the scratch run puts them in a small subclass and swaps it in once by override:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // snippet: scratch run alongside the DMA bench
    class axis_cb_driver extends axis_driver;
      `uvm_component_utils(axis_cb_driver)
      `uvm_register_cb(axis_cb_driver, axis_driver_cb)
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
      virtual function axis_item pre_drive(axis_item it);
        axis_item beat = it;
        `uvm_do_callbacks(axis_cb_driver, axis_driver_cb, pre_drive(this, beat))
        return beat;                             // the item, a modified copy, or null
      endfunction
    endclass

    3. The test: add, and later delete

    The truncation becomes a callback with the same logic as axis_truncating_driver, including the clone. The test adds it after the driver exists, so in connect_phase (or later), and can delete it at run time, which no override can do:

    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
    30
    31
    // snippet: scratch run alongside the DMA bench
    class truncate_cb extends axis_driver_cb;
      `uvm_object_utils(truncate_cb)
      int unsigned cut = 3;
      int unsigned beat_no;
      function new(string name = "truncate_cb");
        super.new(name);
      endfunction
      virtual function void pre_drive(axis_cb_driver drv, inout axis_item beat);
        axis_item b;
        int unsigned i = beat_no;
        if (beat == null) return;                // an earlier callback already dropped it
        beat_no = beat.last ? 0 : beat_no + 1;
        if (i > cut) begin beat = null; return; end
        if (i < cut) return;
        if (!$cast(b, beat.clone())) `uvm_fatal("CLONE", "axis_item clone failed")
        b.last = 1'b1;
        beat = b;
        `uvm_info("TRUNC_CB", $sformatf("TLAST forced on beat %0d", i), UVM_MEDIUM)
      endfunction
    endclass
    
    // In dma_cb_truncate_test (d is env.in_agt.drv, cast to axis_cb_driver):
    function void connect_phase(uvm_phase phase);
      super.connect_phase(phase);
      tcb = truncate_cb::type_id::create("tcb");
      if (!$cast(d, env.in_agt.drv)) `uvm_fatal("CAST", "in_agt.drv is not an axis_cb_driver")
      uvm_callbacks#(axis_cb_driver, axis_driver_cb)::add(d, tcb);
    endfunction
    // In run_phase, after job 1 (LEN=8, cut) and before job 2 (LEN=4, clean):
    //   uvm_callbacks#(axis_cb_driver, axis_driver_cb)::delete(d, tcb);

    Callbacks run in the order they were added (UVM_APPEND, the default); pass UVM_PREPEND as a third argument to put one at the front. Passing null as the object adds a callback to every instance of the type. uvm_callbacks#(T, CB)::display() prints what is attached where, which is the first thing to run when a callback seems not to fire.

    Note

    The callback API (uvm_callback, uvm_callbacks#(T,CB)::add/delete, uvm_register_cb, uvm_do_callbacks) is the same in UVM 1.2 and IEEE 1800.2.

    Watch it run

    A scratch run of dma_cb_truncate_test: job 1 with the callback attached, then the callback deleted and a clean job 2:

    Diagram
    UVM_INFO scratch_tb.sv(92) @ 275000: reporter [TRUNC_CB] TLAST forced on beat 3
    UVM_INFO scratch_tb.sv(133) @ 385000: uvm_test_top [CB] job 1 STATUS=00000004
    UVM_INFO scratch_tb.sv(137) @ 735000: uvm_test_top [CB] job 2 (callback removed) STATUS=00000002
    UVM_INFO dma_tb.sv(411) @ 835000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
    UVM_ERROR :    0
    • The cut happens at the same beat and time as with the override (275 ns): the behaviour moved, not changed.

    • job 1 STATUS=4 (ERR), then job 2 STATUS=2 (DONE) after the delete: one test turned the fault on and off, which an override cannot do.

    • The callback reports as reporter because a callback is an object, not a component. Pass the driver to uvm_info_context if you want the driver's path on the line.

    What breaks: the callback that was never registered

    A team wrote a callback for their stream driver and added it type-wide in the test, uvm_callbacks#(axis_driver, axis_driver_cb)::add(null, cb), so every driver of that type would get it. The driver class that actually registered and called the callbacks was a subclass. The fault test passed. Weeks later someone noticed the fault had never been injected: the test checked only that no mismatches occurred, which is easy when nothing goes wrong. The only trace was one warning among hundreds:

    Diagram
    UVM_WARNING @ 0: reporter [CBUNREG] Callback tcb cannot be registered with object uvm_test_top.env.in_agt.drv because callback type truncate_cb is not registered with object type axis_driver

    That line is real, from a scratch run that makes the same mistake. The T in uvm_callbacks#(T, CB) must be the type that has uvm_register_cb(T, CB), and the add is checked against that registration, not against the object's actual class. The trap is specific to the type-wide form: a type-wide add under the wrong T lands in a queue the subclass never reads, so the fault is never injected. An instance add under the wrong T prints the same CBUNREG warning, but the callback still fires, because instance queues are kept per object in one global pool (verified in a scratch run: STATUS=4, the fault happened). The warning means "check this" either way. Three habits prevent it: promote CBUNREG to an error in the base test (uvm_root::get().set_report_severity_id_override(UVM_WARNING, "CBUNREG", UVM_ERROR), since uvm_root is the reporter that issues it), have every fault callback count how many times it fired, and make every fault test check that the fault happened on the pins, not only that nothing mismatched.

    Interview

    "Callbacks or factory overrides for error injection: which do you use, and why?"

    A weak answer: "Callbacks, because they are more flexible." Flexible how, and at what cost? Callbacks only work where the VIP author put a call site.

    A strong answer: "Overrides when a whole component behaves differently for a whole test, such as a reset-aware driver or a different scoreboard: one line in build_phase, before the component is created. Callbacks when small behaviours must combine or change during the test: the VIP registers a callback class with uvm_register_cb and calls it with uvm_do_callbacks at its hook points, and the test adds callbacks to one instance after build and can delete them later. Overrides do not compose, since one slot holds one class, so truncation plus reset-awareness would need a combined class; callbacks stack in order. The traps are a CBUNREG warning when the add uses the wrong type, which I promote to an error, and override order, which must be before super.build_phase."

    Follow-ups to expect: What if the VIP has no callback hooks? (override a virtual method, or ask the vendor for a hook) and Do callbacks cost performance? (a small iteration per call; noticeable only at very high call rates, and never a reason to skip one fault).

    Check yourself A bench has two DMAs side by side, each with an active, callback-capable input driver. A test must cut packets short on one of them only. What does it do?

    Add the callback to that one driver instance, for example env.dma0.in_agt.drv, after build. The other driver has no callback attached and stays clean. With overrides it would need an instance override on that path instead; with callbacks the choice is made per instance at run time.

    Check yourself A test adds truncate_cb and then corrupt_cb (which flips bit 0 of each beat's data) to the same driver. What does a dropped beat look like to corrupt_cb?

    It receives beat == null, because truncate_cb ran first and dropped the beat, and it must return without touching it. Callbacks in a list see each other's results, so every callback has to handle a null beat. Reversing the order with UVM_PREPEND would corrupt beats that are then dropped anyway.

    Check yourself Why must the test add the callback in connect_phase or later, not in build_phase before super.build_phase?

    The add needs the driver object, and the driver does not exist until the agent's build_phase has run, which happens after the test's build_phase returns. An override is registered before creation because it changes what gets created; a callback is attached to something that already exists.

    Next: whatever injects the fault, the bench must know a fault is coming. Checking expected faults makes the scoreboard predict it from what the monitors saw.

    Related topics

    • Callback types and hooks

    • Overriding items and sequences

    • Reset on the fly

    • Checking expected faults

    Previous

    109 / 280

    Next: Checking Expected Faults

    Up next

    Up nextChecking Expected Faults

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Sequence-Directed Faults