UVM Callbacks: Inject Behavior Without Editing Code
A UVM callback is a hook: a predefined point inside a verification component where you can slip in extra behavior from the outside. Instead of forking a driver or monitor every time one test needs it to behave a little differently, you register a small callback object and the component calls into it at the right moment. This keeps a reusable component closed for modification but open for extension, which is exactly what you want when the same agent is shared across dozens of tests.
Everything in the class examples below is standard UVM code. It is syntax-only on this page and will not compile on a plain Verilog simulator; to run it you need a UVM-capable tool such as industry-standard simulation tools, VCS, or industry-standard simulators, or a free EDA Playground session with a UVM target selected. The one runnable program at the end is deliberately plain SystemVerilog, so it executes on an ordinary simulator and lets you watch the idea in action.
Why a callback instead of editing the component?
Imagine a shared bus_driver used by an entire team. One test needs it to occasionally corrupt a byte so the DUT’s parity checker gets exercised. You could drop an if (inject_error) branch straight into the driver, but now every test pays for that code, the driver grows a tangle of test-specific switches, and the next project that reuses the agent inherits all of it. Callbacks invert the dependency: the driver advertises a hook, and the error-injection logic lives in the test where it belongs.
UVM also gives you the factory and the configuration database for customization. Reach for a uvm callback specifically when you want to augment an operation at a fixed moment in time — before driving an item, after a monitor collects one, on a reset, at end of test — rather than replace a whole class or pass a single value. Typical hook points include pre/post drive, pre/post transaction recording, scoreboard comparison, and reporting.
The three moving parts
uvm_callback— the base class you extend to declare your hook methods.uvm_callbacks#(T,CB)— the type-parameterized pool that tracks which callback objects are attached to which component typeTand callback typeCB, and providesadd()anddelete().- Two macros —
`uvm_register_cbassociates a component with its callback class, and`uvm_do_callbacksfires the registered callbacks at the hook.
Step 1 — Define the callback class
Extend uvm_callback and declare one virtual method per hook. The default body is empty, so a component with no callbacks attached simply does nothing extra.
class bus_driver_cb extends uvm_callback;
`uvm_object_utils(bus_driver_cb)
function new(string name = "bus_driver_cb");
super.new(name);
endfunction
// Hook method: virtual, and does nothing by default.
virtual function void pre_drive(bus_driver drv, bus_item item);
endfunction
endclassStep 2 — Make the component callback-ready
`uvm_register_cb(T, CB) tells UVM that objects of type bus_driver accept callbacks of type bus_driver_cb. At the hook point, `uvm_do_callbacks(T, CB, method_call) walks every attached callback — type-wide ones first, then per-instance — and invokes the method you name.
class bus_driver extends uvm_driver #(bus_item);
`uvm_component_utils(bus_driver)
`uvm_register_cb(bus_driver, bus_driver_cb) // tie the type to its callback
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
bus_item item;
forever begin
seq_item_port.get_next_item(item);
// Fire every registered callback at this hook point.
`uvm_do_callbacks(bus_driver, bus_driver_cb, pre_drive(this, item))
drive(item);
seq_item_port.item_done();
end
endtask
task drive(bus_item item);
// normal pin wiggling on the interface ...
endtask
endclassNotice the driver’s run_phase never mentions error injection, retries, or extra logging. It only says “at this point, run whatever callbacks are registered.” That single macro line is the entire extension surface the rest of the environment is allowed to use.
Step 3 — Write an error-injection callback
Now the interesting part: a callback that corrupts the item on its way to the pins. It extends bus_driver_cb and overrides pre_drive.
class inject_lsb_err_cb extends bus_driver_cb;
`uvm_object_utils(inject_lsb_err_cb)
function new(string name = "inject_lsb_err_cb");
super.new(name);
endfunction
virtual function void pre_drive(bus_driver drv, bus_item item);
item.data ^= 8'h01; // flip LSB: a one-bit data error
`uvm_info("CB", $sformatf("corrupted item, data = 0x%0h", item.data), UVM_LOW)
endfunction
endclassBecause item is a class handle, mutating item.data inside the callback changes the very object the driver is about to send — no return value or extra plumbing is required.
Step 4 — Register it with uvm_callbacks#(T,CB)
Create the callback and attach it, usually in the test’s build or end-of-elaboration phase. The driver source is untouched; only the test knows error injection exists at all.
class err_injection_test extends uvm_test;
`uvm_component_utils(err_injection_test)
my_env env;
// ... build_phase constructs env ...
function void end_of_elaboration_phase(uvm_phase phase);
inject_lsb_err_cb cb;
cb = inject_lsb_err_cb::type_id::create("cb");
// Attach to ONE driver instance; its source never changes.
uvm_callbacks #(bus_driver, bus_driver_cb)::add(env.agent.drv, cb);
endfunction
endclassPass a specific component handle to target a single instance, or pass null to apply the callback type-wide to every matching component. You can detach it later with delete().
// Apply to EVERY bus_driver in the testbench (null handle = type-wide): uvm_callbacks #(bus_driver, bus_driver_cb)::add(null, cb); // Remove a previously added callback: uvm_callbacks #(bus_driver, bus_driver_cb)::delete(env.agent.drv, cb);
How `uvm_do_callbacks iterates
When the hook fires, UVM runs each registered callback in registration order. For a void method, every callback runs in turn. When you need a callback to be able to veto or short-circuit the operation, use `uvm_do_callbacks_exit_on(T, CB, method, VAL), which stops iterating as soon as a callback’s return value matches VAL — handy for an “abort this transaction” style hook. This ordered, type-safe iteration is the main reason to prefer a callback pool over hand-rolled hooks.
| Piece | Role |
|---|---|
uvm_callback | Base class you extend to declare hook methods |
`uvm_register_cb(T,CB) | Declares that component type T accepts callback type CB |
`uvm_do_callbacks(T,CB,call) | Fires all registered callbacks at a hook |
uvm_callbacks#(T,CB)::add | Attaches a callback (one instance, or type-wide with null) |
uvm_callbacks#(T,CB)::delete | Detaches a previously added callback |
The same idea without any UVM library
Strip away the class machinery and a callback is just “a component that calls out to replaceable behavior at a fixed point.” The program below shows that in plain SystemVerilog you can run on any simulator. A driver module drives whatever a shaper hook hands it; the shaper defaults to a clean pass-through. The testbench “registers a callback” by arming corruption at the hook — and the driver’s source is never edited. Watch the bus value diverge from the item only after the hook is armed.
// A fixed, reusable component. We never edit its source to inject errors.
module driver(input clk, input [7:0] item, output reg [7:0] bus);
always @(posedge clk) begin
bus <= item;
$display("[%0t] driver drove bus = 0x%02h", $time, item);
end
endmodule
// The "hook". By default it is a pass-through, so data is clean.
// External code flips err_mask to shape the item, without touching driver.
module shaper(input [7:0] original, output [7:0] shaped);
reg [7:0] err_mask;
initial err_mask = 8'h00;
assign shaped = original ^ err_mask;
endmodule
// Top-level testbench (listed last).
module tb;
reg clk = 1'b0;
reg [7:0] item;
wire [7:0] shaped, bus;
shaper u_shaper(.original(item), .shaped(shaped));
driver u_driver(.clk(clk), .item(shaped), .bus(bus));
always #5 clk = ~clk;
initial begin
item = 8'hA5;
@(posedge clk); #1;
$display("clean run : item=0x%02h bus=0x%02h (callback inactive)", item, bus);
// "Register" an error-injection callback at the hook -- driver is untouched.
u_shaper.err_mask = 8'hFF;
@(posedge clk); #1;
$display("all-bit flip: item=0x%02h bus=0x%02h", item, bus);
u_shaper.err_mask = 8'h01;
@(posedge clk); #1;
$display("1-bit error : item=0x%02h bus=0x%02h", item, bus);
$display("done");
$finish;
end
endmodule[5] driver drove bus = 0xa5 clean run : item=0xa5 bus=0xa5 (callback inactive) [15] driver drove bus = 0x5a all-bit flip: item=0xa5 bus=0x5a [25] driver drove bus = 0xa4 1-bit error : item=0xa5 bus=0xa4 done /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp8fz8ukvu/tb.v:43: $finish called at 26 (1s)
In real UVM you would never poke a sub-module’s register like this; uvm_callback and uvm_callbacks#(T,CB) give you the same “modify from the outside” power with proper type safety, ordered iteration, and per-instance control. But the shape of the idea is identical: a fixed component, a declared hook, and behavior supplied from elsewhere. Once the pattern clicks, callbacks become the cleanest way to bolt error injection, protocol variations, or extra checks onto components you are not allowed to touch.
Quick check
What problem do UVM callbacks solve?
They let you add behavior at fixed points inside a reusable component without editing or subclassing it, so a shared agent stays clean while individual tests supply their own extra behavior such as error injection.
What is the difference between `uvm_register_cb and `uvm_do_callbacks?
`uvm_register_cb(T,CB) is a one-time declaration that associates component type T with callback class CB. `uvm_do_callbacks(T,CB,call) runs at the hook during simulation and actually invokes every registered callback’s method.
How do you attach a callback to one driver instance versus all of them?
uvm_callbacks#(T,CB)::add(handle, cb) attaches it to the specific component passed as handle. Passing null as the handle makes the callback type-wide, applying to every component of type T.
Why can an error-injection callback change the transaction with no return value?
The transaction item is passed to the callback as a class handle, so mutating its fields inside the callback changes the exact same object the driver is about to drive onto the interface.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses