How UVM Callback works?
In short: A UVM callback lets you change what a component (like a driver or monitor) does at chosen points, without touching that component’s source code. You define an empty hook method inside the component, register it, then plug in your own class later to inject errors, add checks, or collect coverage. It is the tool of choice when a base component is locked, such as a third-party VIP.
A callback is a small piece of code you attach to a component so it runs at a fixed spot during that component’s work. The base component stays exactly as written. You just supply the extra behaviour from outside. In UVM this is built on the uvm_callback base class and a set of macros that wire everything together.
A simple way to picture it
Think of a delivery driver who follows a fixed route every day. The delivery company does not want anyone rewriting the driver’s route. Instead, they leave a note slot on the dashboard. Before each stop, the driver checks the slot and does whatever the note says: skip a house, ring twice, or leave the parcel with a neighbour. The driver code never changes. You change behaviour only by dropping in a different note. A UVM callback is that note slot: the component reaches a fixed point, checks for attached callbacks, and runs whatever you plugged in.
When callbacks are the right choice
Callbacks shine when you need small, targeted changes to a component you should not or cannot edit. Common reasons include:
- You are using an encrypted third-party VIP and only get published hook points such as “just after get_next_item” or “just before driving the bus”.
- You want to inject an error into one test without breaking the clean behaviour every other test relies on.
- You want a monitor to also feed a scoreboard or gather coverage, added per test rather than baked in.
- The change is rare or minor, so building a whole new component through the factory would be more work than it is worth.
The building blocks of a UVM callback
A working callback needs four parts. Keep them in mind as you read the example below.
| Part | What it does |
|---|---|
| Callback class | Extends uvm_callback and declares one or more virtual hook methods (empty by default). |
`uvm_register_cb | Placed inside the component. Tells UVM which callback type is allowed on this component. |
`uvm_do_callbacks | Placed at the hook point in the component. Runs every attached callback in order. |
uvm_callbacks#(T,CB)::add | Called from a test to attach your callback object to a specific component instance. |
A worked example: injecting an error from a test
This driver exposes one hook, inject_err. By default it does nothing, so normal tests drive clean traffic. One test attaches a callback that flips a bit, so only that test sees corrupted data. The base driver is never edited.
`include "uvm_macros.svh"
import uvm_pkg::*;
// 1) The callback class with an empty hook
typedef class spi_driver;
class spi_driver_cb extends uvm_callback;
`uvm_object_utils(spi_driver_cb)
function new(string name = "spi_driver_cb");
super.new(name);
endfunction
// Hook methods must be virtual so a derived callback can override them
virtual task pre_send(spi_driver drv, spi_item tr);
endtask
endclass// 2) The driver registers the callback and calls the hook
class spi_driver extends uvm_driver #(spi_item);
`uvm_component_utils(spi_driver)
`uvm_register_cb(spi_driver, spi_driver_cb) // allow this callback type
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
// Hook point: run every attached callback before we drive
`uvm_do_callbacks(spi_driver, spi_driver_cb, pre_send(this, req))
`uvm_info("SPI_DRV", $sformatf("driving addr=%0h data=%0h",
req.addr, req.data), UVM_LOW)
seq_item_port.item_done();
end
endtask
endclass// 3) A concrete callback that corrupts one bit of data
class spi_bitflip_cb extends spi_driver_cb;
`uvm_object_utils(spi_bitflip_cb)
function new(string name = "spi_bitflip_cb");
super.new(name);
endfunction
task pre_send(spi_driver drv, spi_item tr);
tr.data[0] = ~tr.data[0]; // flip bit 0 on the way out
`uvm_info("BITFLIP_CB", "injected single-bit error", UVM_LOW)
endtask
endclass// 4) The test attaches the callback to one driver instance
class err_inject_test extends uvm_test;
`uvm_component_utils(err_inject_test)
spi_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 = spi_env::type_id::create("env", this);
endfunction
function void end_of_elaboration_phase(uvm_phase phase);
spi_bitflip_cb cb = spi_bitflip_cb::type_id::create("cb");
// attach cb to this specific driver only
uvm_callbacks#(spi_driver, spi_driver_cb)::add(env.agt.drv, cb);
endfunction
endclassRunning the test prints a driving line for each item. When the bit-flip callback is attached, its message appears just before each drive and the data value shown has bit 0 inverted. A run of the same test with no callback attached would omit the injected lines and print the data exactly as randomized:
# Sample log (spi_env with bitflip callback attached)
UVM_INFO @ 0: reporter [RNTST] Running test err_inject_test...
UVM_INFO bitflip_cb.sv [BITFLIP_CB] injected single-bit error
UVM_INFO spi_driver.sv [SPI_DRV] driving addr=1a data=c5
UVM_INFO bitflip_cb.sv [BITFLIP_CB] injected single-bit error
UVM_INFO spi_driver.sv [SPI_DRV] driving addr=2f data=90
UVM_INFO bitflip_cb.sv [BITFLIP_CB] injected single-bit error
UVM_INFO spi_driver.sv [SPI_DRV] driving addr=07 data=3b
# Note: each data value has bit 0 flipped versus what the sequence randomized.
# With no callback attached, the BITFLIP_CB lines disappear and data matches the sequence.Because the hook lives inside the driver and the behaviour lives in a separate callback class, the same driver runs clean in every other test. Only err_inject_test sees the flipped bit. To turn it off again mid-test you can call uvm_callbacks#(spi_driver, spi_driver_cb)::delete(env.agt.drv, cb).
Expected output, in plain words
When you run err_inject_test, the driver prints a “driving” line for each item. On every item the bit-flip callback also prints its “injected single-bit error” line just before, and the data value shown in the driver log has bit 0 inverted compared with what the sequence randomized. Run a plain test with no callback attached and the “injected” lines disappear while the data matches the sequence exactly. Since I do not run a simulator here, treat this as the behaviour the code is written to produce; run it in your own tool or on EDA Playground to confirm the exact logs.
Common mistakes to avoid
- Forgetting
`uvm_register_cb. Without it,`uvm_do_callbackshas nothing registered and your hook silently never fires. - Non-virtual hook methods. If the hook in the base callback is not
virtual, your derived override is ignored. - Adding the callback too late. Attach it in
build_phaseorend_of_elaboration_phaseso it is in place beforerun_phasestarts using it. - Attaching to the wrong instance.
addtakes a specific component handle; pass the exact driver you mean, or use the type-wide add form if you truly want every instance. - Doing heavy work in a hook. Keep callbacks light. Large behaviour changes usually belong in a factory override, not a callback.
Keep learning
Callbacks are one of two reuse tools in UVM. See how they compare with the other in Callbacks vs Factory, and read more on the concept of the UVM factory. To see where the driver and monitor sit, review the UVM testbench architecture. For interview prep, browse our interview questions and the full UVM guides.
Frequently asked questions
What is a callback in UVM?
A callback is a mechanism that lets you run your own code at a fixed point inside a component, such as a driver or monitor, without editing that component. You define a hook method in a class based on uvm_callback, register it in the component, and attach your version from a test.
When should I use a callback instead of the factory?
Use a callback for small, targeted tweaks like error injection or extra coverage, especially on a locked or third-party component. Use the factory when you need to replace a component with a different version that changes behaviour more broadly.
Why is my callback method never called?
The two most common causes are a missing `uvm_register_cb in the component, or a hook method that is not declared virtual so your override is skipped. Also check that you attached the callback before run_phase started.
Can I attach a callback to only one component instance?
Yes. uvm_callbacks#(T,CB)::add takes a specific component handle, so you can target a single driver or monitor. There is also a type-wide form that applies to every instance of that component type.
How do I remove a callback during a test?
Call uvm_callbacks#(T,CB)::delete with the same component handle and callback object you used to add it. After deletion the hook runs its default empty behaviour again.
Do callbacks work on monitors as well as drivers?
Yes. Any component can register a callback type and place hook points. Monitors often use callbacks to pass observed transactions to a scoreboard or to sample coverage per test.


