In short: UVM callbacks let you register small hook objects that a driver or monitor invokes at fixed points, so you can inject errors, delays or coverage sampling without editing or subclassing the base component.
Why callbacks exist
Every verification team eventually hits the same wall. The driver in your agent works, it is reused across ten tests, and then one test needs to corrupt a CRC, another needs to add random back-pressure, and a third needs to log every transaction to a side file. You could copy the driver and edit it, but now you maintain two drivers. You could use a factory override to swap in a subclass, but that gives you one replacement per type, and combining two behaviors means writing a third subclass.
Callbacks solve this differently. The base component calls a hook at a defined point, and whatever callback objects are registered at that moment run in order. Behaviors become small, independent, stackable objects. If you are new to the component hierarchy, start with what UVM is and the UVM testbench architecture.
Key fact: A factory override replaces a component with one alternative. Callbacks add several behaviors to the same component at once, and can be added or removed at runtime.
The four pieces of the callback mechanism
The mechanism is built from four classes and macros in the UVM base library (IEEE 1800.2):
- Callback class: extends
uvm_callbackand declares virtual hook methods with empty or default bodies. - Registration:
`uvm_register_cb(my_driver, my_driver_cb)inside the component tells UVM which callback type the component accepts. - Call site:
`uvm_do_callbacks(my_driver, my_driver_cb, hook(this, item))loops over every registered callback and invokes the hook. - Attachment:
uvm_callbacks#(my_driver, my_driver_cb)::add(drv, cb)binds a callback object to one instance, or to all instances of a type.
A worked example: error injection into a driver
Suppose a bus driver sends a packet with a parity bit. We want some tests to flip parity on roughly 2 percent of packets. First, the callback base class and the driver hook point:
class bus_driver_cb extends uvm_callback;
`uvm_object_utils(bus_driver_cb)
function new(string name = "bus_driver_cb"); super.new(name); endfunction
virtual task pre_drive(bus_driver drv, bus_item item); endtask
virtual task post_drive(bus_driver drv, bus_item item); endtask
endclass
class bus_driver extends uvm_driver #(bus_item);
`uvm_component_utils(bus_driver)
`uvm_register_cb(bus_driver, bus_driver_cb)
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
`uvm_do_callbacks(bus_driver, bus_driver_cb, pre_drive(this, req))
drive_to_dut(req);
`uvm_do_callbacks(bus_driver, bus_driver_cb, post_drive(this, req))
seq_item_port.item_done();
end
endtask
endclass
Now the error injector, written once and reusable on any test:
class parity_err_cb extends bus_driver_cb;
`uvm_object_utils(parity_err_cb)
int unsigned pct = 2;
virtual task pre_drive(bus_driver drv, bus_item item);
if ($urandom_range(99) < pct) begin
item.parity = ~item.parity;
`uvm_info("ERRCB", "parity flipped", UVM_MEDIUM)
end
endtask
endclass
// in the test, after build:
parity_err_cb cb = new("cb");
uvm_callbacks#(bus_driver, bus_driver_cb)::add(env.agent.drv, cb);
Key fact: If the corrupted item is also sent to a scoreboard, the checker must know about the injected error. Otherwise the UVM scoreboard will report false mismatches.
Callbacks on monitors
Monitors are the other classic target. A monitor should stay passive and reusable, yet you often want to attach protocol checkers, functional coverage or a debug dumper per project. Place a hook such as item_collected(this, item) immediately before ap.write(item). A coverage callback samples a covergroup, a protocol-check callback asserts ordering rules, and neither requires touching the monitor. Transactions still flow to subscribers through TLM analysis ports as usual; the callback just sees the item first.
Callbacks versus factory overrides versus other options
| Technique | Best for | Weakness |
|---|---|---|
| Factory override | Swapping an entire component or sequence item type | One replacement per type; combining behaviors needs new subclasses |
| Callbacks | Adding stackable, optional behavior at known points | Hook points must be planned in the base component |
| Config DB knobs | Simple on/off or numeric switches | Logic still lives inside the component |
| Virtual interface tricks | Signal-level glitches | Bypasses transaction abstraction |
A good rule used in many Synopsys VCS, Cadence Xcelium and Siemens Questa projects: use factory overrides when the identity of the component changes, and callbacks when the behavior around an unchanged component changes.
Animated view: a callback altering a transaction
Pitfalls interviewers like to probe
- Forgetting the registration macro. Without
`uvm_register_cbthe add() call produces a warning and the hook never fires. - Hooks in the wrong place. A hook placed after
item_done()cannot alter what was driven. Decide hook points at design time. - Blocking hooks. A task hook that waits can stall the driver. This is useful for delay injection but dangerous if forgotten.
- Type versus instance scope. Adding to a type affects every instance, including those in other agents. Pass an instance handle to limit scope.
- Debuggability. Use verbosity levels inside callbacks so injected faults are traceable in the log.
A typical regression runs 500 to 2,000 seeds, so keep callbacks lightweight; a callback that does heavy string formatting on every item can double simulation time on a high-throughput bus.
Key fact: Callbacks are inherited by derived component types, so a callback registered on bus_driver also reaches subclasses of it.
Where to go next
Practice by adding a delay callback and a coverage callback to the same driver and confirming they stack. Our structured CourseTron courses include guided labs on this, and if you want instructor-led placement-focused training, see ChipXpert's VLSI training programs.
Ready to master UVM hands-on? Explore the verification track in our CourseTron courses and build testbenches the way production teams do.
FAQ
What is the difference between a UVM callback and a factory override?
A factory override replaces one type with another at creation time. A callback keeps the original component and lets you attach any number of extra behaviors at predefined hook points.
Can I use callbacks on sequences?
Yes, uvm_sequence_base supports callbacks, but most teams use them on drivers, monitors and scoreboards, where hook points are naturally defined.
Do callbacks work on multiple instances at once?
Yes. Adding a callback with a null instance handle attaches it to every instance of the type, while passing a specific handle limits it to that one instance.
Are callbacks still recommended in UVM 1.2 and IEEE 1800.2?
Yes. They remain part of the standard. Many teams also use configuration objects and analysis components, but callbacks stay the cleanest option for optional, stackable behavior.