Part 10 · Advanced Topics · Intermediate
Callback Type and Hooks
Core callback architecture: uvm_callback base class, uvm_register_cb binding, and virtual hook methods in host components.
Why callback types exist
A callback object encapsulates behavior that can be attached to another component at runtime. UVM models this with uvm_callback as the common base type. You derive your callback class from it, define virtual hook functions, and let host components invoke those hooks at specific points.
This approach decouples extension logic from component inheritance. Without callbacks, teams often create a chain of driver subclasses just to add tiny behavior changes. With callbacks, the host class remains single-source and the variation lives in pluggable callback objects.
[UVM][ADV] callback type relationship
uvm_callback
▲
│
├── my_driver_cb
│ ├─ virtual pre_drive(...)
│ └─ virtual post_drive(...)
│
└── my_monitor_cb
├─ virtual pre_sample(...)
└─ virtual post_sample(...)
Host component chooses when hooks run[TEST] attaches callbacks
│
▼
[UVM] host component
│ calls hook points
▼
[ADV] callback object methods
│
▼
[DRV] behavior modification at controlled timesCallback classes model optional behavior, not mandatory base protocol flow.
One host component can expose several hook points across its lifecycle.
Different tests can attach different callback sets without recompiling host logic.
Binding host and callback types
The macro `uvm_register_cb(host_type, cb_type) declares that a specific callback class is legal for a specific host class. This gives type-safe registration and allows UVM callback utilities to manage pools per host instance.
Registration is not just bookkeeping. It forms the contract that says: this callback type is designed for this host type, and this host type knows when to call the callback hooks.
class spi_driver_cb extends uvm_callback;
virtual function void pre_drive(spi_driver drv, spi_item tr);
endfunction
virtual function void post_drive(spi_driver drv, spi_item tr);
endfunction
endclass
class spi_driver extends uvm_driver #(spi_item);
`uvm_component_utils(spi_driver)
`uvm_register_cb(spi_driver, spi_driver_cb)
// ...
endclass[UVM][ADV] type contract
Host type: spi_driver
Callback type: spi_driver_cb
Contract says:
spi_driver accepts spi_driver_cb objects
spi_driver invokes cb.pre_drive/post_drive at defined points
registration errors surface early if wrong cb type is addedDefine callback type first, then register it on the host component.
Keep hook signatures explicit: include host handle and transaction where useful.
Treat registration macros as part of API surface, not local implementation detail.
Virtual hook methods and execution points
Hook methods should map to meaningful lifecycle events. For a driver, common hook points are before drive, before pin update, after drive completion, and on error. For a monitor, hooks often wrap sampling and decode boundaries.
The host component calls hooks via callback macros, not direct object method calls. This ensures all registered callbacks run consistently, preserving ordering and framework semantics.
class i2c_driver_cb extends uvm_callback;
virtual function void pre_txn(i2c_driver drv, ref i2c_item tr);
endfunction
virtual task pre_drive_bits(i2c_driver drv, i2c_item tr);
endtask
virtual function void post_txn(i2c_driver drv, i2c_item tr);
endfunction
endclasstask i2c_driver::drive_item(i2c_item tr);
`uvm_do_callbacks(i2c_driver, i2c_driver_cb, pre_txn(this, tr))
`uvm_do_callbacks(i2c_driver, i2c_driver_cb, pre_drive_bits(this, tr))
drive_on_bus(tr);
`uvm_do_callbacks(i2c_driver, i2c_driver_cb, post_txn(this, tr))
endtask[DRV] drive_item flow with hooks
start drive_item(tr)
├─ [ADV] pre_txn callbacks
├─ [ADV] pre_drive_bits callbacks
├─ base driver drives legal protocol
└─ [ADV] post_txn callbacks
doneUse ref arguments only when mutation is intentionally supported.
Separate function hooks (fast) from task hooks (timing/delay capable).
Document which hooks may alter transaction fields and which are observation-only.
Walkthrough: adding a checksum-adjust callback
Assume a base UART driver emits packets with checksum precomputed by sequence code. A test needs to occasionally tweak payload and recompute checksum before drive. Instead of a custom driver subclass, a callback can modify the item in pre_txn.
class uart_checksum_cb extends uart_driver_cb;
`uvm_object_utils(uart_checksum_cb)
virtual function void pre_txn(uart_driver drv, ref uart_item tr);
if (tr.inject_noise) begin
tr.payload ^= 8'h04;
tr.checksum = calc_checksum(tr.payload, tr.header);
end
endfunction
endclassclass uart_noise_test extends uvm_test;
// ...
function void end_of_elaboration_phase(uvm_phase phase);
uart_checksum_cb cb = uart_checksum_cb::type_id::create("cb");
uvm_callbacks#(uart_driver, uart_driver_cb)::add(env.uart_agt.drv, cb);
endfunction
endclass[TEST] callback attachment walkthrough
test builds env
▼
creates uart_checksum_cb
▼
[UVM][ADV] add callback to env.uart_agt.drv
▼
[DRV] each drive_item calls pre_txn
▼
callback mutates selected items before protocol driveKey takeaways
Define callback classes from uvm_callback and expose meaningful virtual hooks.
Bind callback and host with uvm_register_cb to establish type-safe extension.
Host owns execution points; callbacks provide optional behavior at those points.
Callback-based extension avoids driver/monitor subclass explosion.
Common pitfalls
No clear hook contract - callbacks accidentally violate host assumptions.
Direct callback invocation loop written manually - bypasses UVM callback semantics.
Mutating transactions in observation-only hooks - introduces hidden side effects.
Too many tiny hooks with unclear purpose - API becomes difficult to use.