Part 10 · Advanced Topics · Intermediate
Registration Per Instance
Manage callback registration scope with add/delete and iterator utilities such as get_first/get_next, including practical ordering patterns.
Instance-scoped callback attachment
Callbacks are usually attached to specific component instances, not globally to every component of a class. This gives precise control: one driver in one agent can run error injection callbacks while other agents remain clean.
The common API is uvm_callbacks#(host_t, cb_t)::add(host_inst, cb_obj) and ::delete(host_inst, cb_obj). Registration can occur in build/end_of_elaboration/start_of_simulation depending on when host handles are available and policy needs to be activated.
function void my_test::end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
delay_cb dcb = delay_cb::type_id::create("dcb");
corrupt_cb ccb = corrupt_cb::type_id::create("ccb");
uvm_callbacks#(eth_driver, eth_driver_cb)::add(env.tx_agt0.drv, dcb);
uvm_callbacks#(eth_driver, eth_driver_cb)::add(env.tx_agt0.drv, ccb);
endfunction[TEST][ADV] instance targeting
env.tx_agt0.drv <-- add delay_cb, corrupt_cb
env.tx_agt1.drv <-- no callbacks
env.rx_agt.drv <-- add audit_cb only
Same class, different runtime behavior per instanceAttach callbacks to the smallest scope needed for scenario precision.
Keep callback ownership in test or env policy layer, not inside host constructor.
Track callback handles if you plan to remove them later.
Delete and lifecycle control
Dynamic tests may enable callbacks for part of a run then remove them. Deleting callbacks cleanly avoids policy leakage between phases or sequences.
task burst_error_window_seq::body();
corrupt_cb ccb = corrupt_cb::type_id::create("ccb");
uvm_callbacks#(axi_driver, axi_driver_cb)::add(p_sequencer.drv, ccb);
// Inject errors for first window
repeat (20) send_random_item();
// Stop injection
uvm_callbacks#(axi_driver, axi_driver_cb)::delete(p_sequencer.drv, ccb);
// Continue clean traffic
repeat (50) send_random_item();
endtask[DRV] callback lifecycle window
t0 add corrupt_cb
t1..t20 callbacks active -> faults injected
t21 delete corrupt_cb
t22..end base behavior only
Controlled activation avoids whole-test contaminationDelete using the same callback object handle that was added.
Pair add/delete calls in one control flow where possible.
For long tests, log activation windows for reproducibility.
Iterating callbacks with get_first/get_next
Beyond macro dispatch, UVM exposes iterator-style access to registered callbacks for inspection and advanced host logic. get_first and get_next allow the host or debug utility to walk callback lists in their current order.
This is useful for diagnostics (print active callback chain), selective invocation patterns, or checking if a specific policy callback is currently attached.
function void axi_driver::dump_callbacks();
axi_driver_cb cb;
string chain = "";
cb = uvm_callbacks#(axi_driver, axi_driver_cb)::get_first(this);
while (cb != null) begin
chain = {chain, cb.get_name(), " -> "};
cb = uvm_callbacks#(axi_driver, axi_driver_cb)::get_next(this);
end
`uvm_info("CB_DUMP", $sformatf("active callbacks: %s", chain), UVM_LOW)
endfunction[UVM][ADV] iterator view
callback list on drv instance:
[0] delay_cb
[1] corrupt_cb
[2] audit_cb
get_first(this) -> delay_cb
get_next(this) -> corrupt_cb
get_next(this) -> audit_cb
get_next(this) -> nullIterator order reflects current registration chain for that host instance.
Use iterator APIs mostly for visibility and meta-control, not routine dispatch.
Keep iterator-based logic side-effect free unless strongly justified.
Ordering strategies and walkthrough
Order can be treated as policy layering: first normalize transaction, then inject disturbance, then audit. A predictable template reduces accidental behavior shifts across tests.
[TEST][ADV] recommended callback order template
1) sanitize/normalize callbacks
- ensure defaults, clip ranges
2) scenario perturbation callbacks
- delay, corruption, protocol pressure
3) observability callbacks
- audit logs, counters, custom coverage taps[DRV] ordering walkthrough
tr enters host
▼
normalize_cb.pre_txn
- fills missing sideband defaults
▼
corrupt_cb.pre_txn
- flips selected fields for target scenario
▼
drive
▼
audit_cb.post_txn
- records final transmitted formfunction void register_policy_chain(axi_driver drv);
normalize_cb ncb = normalize_cb::type_id::create("ncb");
corrupt_cb ccb = corrupt_cb::type_id::create("ccb");
audit_cb acb = audit_cb::type_id::create("acb");
uvm_callbacks#(axi_driver, axi_driver_cb)::add(drv, ncb);
uvm_callbacks#(axi_driver, axi_driver_cb)::add(drv, ccb);
uvm_callbacks#(axi_driver, axi_driver_cb)::add(drv, acb);
endfunctionKey takeaways
Callback registration is typically per-instance for precise scenario control.
Use add/delete to open and close behavior windows during tests.
get_first/get_next provide callback chain introspection and debug visibility.
Adopt explicit ordering templates to keep stacked callbacks predictable.
Common pitfalls
Adding callback objects and losing handles - cannot reliably delete later.
Registering in scattered places - effective order becomes accidental.
Assuming class-wide behavior when only one instance has callbacks attached.
Iterator use for normal dispatch - reimplements macro path unnecessarily.