Skip to main content

Callbacks, Events & Pools

The UVM Factory covered one way to customize behavior without editing existing code: override which class gets built. Callbacks offer a different kind of extension point — hooking into an existing, unmodified instance's behavior at specific points, without subclassing anything at all. Separately, this page covers uvm_event/uvm_event_pool — a synchronization mechanism distinct from both TLM (earlier in this section) and objections (from Phases & Objections).

Callbacks: hooking into a component without subclassing it​

class fifo_driver_callback extends uvm_callback;
`uvm_object_utils(fifo_driver_callback)

function new(string name = "fifo_driver_callback");
super.new(name);
endfunction

// Empty placeholders — real callback subclasses override what they need
virtual task pre_drive(fifo_driver drv, fifo_txn txn); endtask
virtual task post_drive(fifo_driver drv, fifo_txn txn); endtask
endclass

`uvm_register_cb(fifo_driver, fifo_driver_callback)
  • `uvm_register_cb(fifo_driver, fifo_driver_callback), invoked once outside any class, registers fifo_driver_callback as a valid callback type for fifo_driver — a prerequisite before any instance of it can actually be attached.
  • The base callback class's methods are empty virtual placeholders — pre_drive()/post_drive() here do nothing by default; a real callback attaches by extending this class and overriding just the hook it needs.

The driver invokes registered callbacks with `uvm_do_callbacks:

task fifo_driver::drive_transaction(fifo_txn txn);
`uvm_do_callbacks(fifo_driver, fifo_driver_callback, pre_drive(this, txn))
@(posedge vif.clk);
// ... drive the pins as before ...
`uvm_do_callbacks(fifo_driver, fifo_driver_callback, post_drive(this, txn))
endtask

`uvm_do_callbacks builds an iterator over every callback currently attached to this specific driver instance and calls the named method on each, in registration order. A test attaches a callback at runtime, typically in build_phase, without fifo_driver itself ever needing to change:

class fifo_logging_callback extends fifo_driver_callback;
`uvm_object_utils(fifo_logging_callback)
virtual task pre_drive(fifo_driver drv, fifo_txn txn);
`uvm_info("CB", $sformatf("About to drive: %s", txn.op.name()), UVM_HIGH)
endtask
endclass

// In a test's build_phase:
fifo_logging_callback cb = fifo_logging_callback::type_id::create("cb");
uvm_callbacks#(fifo_driver)::add(env.agent.driver, cb);
Callback subclasses still need factory registration

It's easy to assume callbacks are separate from the factory system entirely, since they attach to an instance rather than being built by it — but the callback subclass itself still needs `uvm_object_utils (as fifo_logging_callback has above) for type_id::create() to work on it, exactly like any other uvm_object. Skipping it is a common, easy-to-miss mistake specifically because callbacks feel like a different, separate mechanism.

Compared to a factory override from The UVM Factory: an override replaces the driver's class entirely, one override active at a time. A callback adds behavior at specific named points, and — since `uvm_do_callbacks iterates over all of them — several independent callbacks can be attached to the same instance simultaneously without conflicting.

Controlling callback order, and removing one​

uvm_callbacks#(T)::add() — the call uvm_callbacks#(fifo_driver)::add(env.agent.driver, cb) used above — takes an optional third argument controlling where the new callback lands relative to ones already attached: UVM_APPEND (the default, run after everything already attached) or UVM_PREPEND (run before everything already attached). With several callbacks stacked on the same instance, that ordering argument is the only thing that determines which one's hook runs first. The matching removal call is uvm_callbacks#(T)::delete(obj, cb), which drops a specific callback instance back out of the queue for obj. Passing null as the object — uvm_callbacks#(T)::add(null, cb) — attaches the callback type-wide, to every instance of T, rather than to one specific object.

A callback can also be silenced without being removed at all: every uvm_callback instance carries its own callback_mode(0)/callback_mode(1) toggle (defaulting to enabled), and `uvm_do_callbacks skips any callback whose mode is currently off during iteration. This is the right tool when a test wants to temporarily suppress one callback's effect mid-run without losing its place in the attached order — delete() followed by a later add() would instead lose that ordering (a re-added callback lands wherever UVM_APPEND/UVM_PREPEND puts it, not necessarily back where it was).

uvm_event: a richer alternative to plain SystemVerilog events​

uvm_event #(uvm_object) reset_done_ev = new("reset_done_ev");

// Triggering side:
reset_done_ev.trigger();

// Waiting side:
reset_done_ev.wait_trigger();

uvm_event wraps a plain SystemVerilog event with extra bookkeeping — callbacks, and tracking of which processes are currently waiting — at the cost of real overhead compared to a native event. That tradeoff is worth being deliberate about: use a plain SystemVerilog event unless uvm_event's extra features are genuinely needed, rather than reaching for it by default.

A real race condition, and the method that avoids it

If trigger() and wait_trigger() happen to execute in the same delta cycle, the waiting process can miss the trigger entirely — a genuine race, not a hypothetical one. wait_ptrigger() exists specifically to close that gap, by persisting the triggered state rather than firing a one-shot notification. Separately, the "triggered" state persists until an explicit reset() — calling wait_trigger() repeatedly with no reset() between calls will keep firing immediately off the same stale trigger, not wait for a new one.

Other uvm_event methods worth knowing​

Beyond trigger()/wait_trigger()/wait_ptrigger(), a handful of smaller methods round out the class:

  • is_on() / is_off() — query whether the event has been triggered since its last reset(), without blocking (unlike wait_trigger(), which blocks until it has).
  • get_num_waiters() — returns how many processes are currently blocked in a wait_trigger()/wait_ptrigger() on this event, useful for sanity-checking that a synchronization scheme actually has the waiters it's supposed to.
  • cancel() — decrements the waiter count without triggering the event, for a process that stops waiting some other way (e.g. it was killed) so it doesn't get counted as still blocked.
  • Passing data with the trigger: trigger() accepts an optional payload (e.g. reset_done_ev.trigger(my_txn)), and the waiting side reads it back with get_trigger_data() after wait_trigger() returns — or in one call via wait_trigger_data(), which combines the wait and the data fetch. This lets an event carry more than a bare notification.

uvm_event_pool: sharing an event by name across components​

Two components that need to synchronize on the same event, without either one holding a direct reference to the other, can go through the global event pool instead:

// In one component (e.g. testbench-top's reset logic, or a dedicated reset driver):
uvm_event_pool::get_global_pool().get_global("reset_done").trigger();

// In any other component, anywhere in the tree:
uvm_event_pool::get_global_pool().get_global("reset_done").wait_trigger();

get_global(key) is the whole mechanism — both sides just ask the same global pool for the same string key, and get back the identical uvm_event handle, the same "shared lookup by name" idea uvm_config_db uses for configuration, applied here to synchronization instead.

get_global() never fails — it silently creates

Requesting a key that doesn't exist yet doesn't error — it auto-creates a new, empty event under that name and returns it. A typo in the key string on one side produces two different events that never see each other's triggers, with no error anywhere to flag the mismatch — it just silently doesn't work. Checking exists(key) defensively before relying on a pooled event, especially while a synchronization scheme is still being developed, catches this class of typo immediately instead of during a confusing debug session later.

What's next​

This closes out the reporting and utility-class coverage. The next section moves into the Register Abstraction Layer — a substantial extension for register-heavy DUTs, distinct enough from everything covered so far that it gets its own section.