Protected Course Page
Enter the password to view Day 11 of the UVM 30-Day plan.
Day 11: Callbacks and Synchronization — Extending UVM Components Without Subclassing
A 30-day SystemVerilog/UVM verification series. Sometimes subclassing is the wrong answer. Today we cover the three escape hatches UVM gives you for those cases.
The factory solves most extension problems: swap a component's class, done. But the factory has two limits. First, it replaces a component entirely, not parts of it. Second, it cannot inject behavior into a component that was already created. Sometimes you need to hook into a driver right before it drives a pin, or wait for a monitor to publish something without giving it a callback path, or synchronize five processes on one event. Callbacks, events, and barriers cover those cases.
1. uvm_callback — Extending Without Subclassing
A callback is a small class with optional virtual methods that a component invokes at well-defined points. You attach a callback object to a specific component instance, and the component calls the hooks at runtime. The component does not know the callback exists; the callback does not know the component's internals.
Step 1 — declare the callback base class with a virtual method for each hook point:
class fifo_driver_cb extends uvm_callback;
`uvm_object_utils(fifo_driver_cb)
function new(string name = "fifo_driver_cb");
super.new(name);
endfunction
// Called before the driver drives each item
virtual task pre_drive(fifo_driver drv, fifo_tx tr);
// default: do nothing
endtask
// Called after the driver drives each item
virtual task post_drive(fifo_driver drv, fifo_tx tr);
endtask
endclass
Step 2 — register the callback type with the component and call the hooks:
class fifo_driver extends uvm_driver #(fifo_tx);
`uvm_component_utils(fifo_driver)
`uvm_register_cb(fifo_driver, fifo_driver_cb)
virtual task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
`uvm_do_callbacks(fifo_driver, fifo_driver_cb, pre_drive(this, req))
@(vif.drv_cb);
vif.drv_cb.wr_en <= req.wr_en;
vif.drv_cb.rd_en <= req.rd_en;
vif.drv_cb.wdata <= req.wdata;
`uvm_do_callbacks(fifo_driver, fifo_driver_cb, post_drive(this, req))
seq_item_port.item_done();
end
endtask
endclass
Step 3 — a test attaches a concrete callback:
class fifo_err_cb extends fifo_driver_cb;
`uvm_object_utils(fifo_err_cb)
virtual task pre_drive(fifo_driver drv, fifo_tx tr);
// Corrupt one in ten items
if ($urandom_range(0, 9) == 0)
tr.wdata = ~tr.wdata;
endtask
endclass
// In the test's build_phase:
fifo_err_cb cb = fifo_err_cb::type_id::create("cb");
uvm_callbacks#(fifo_driver, fifo_driver_cb)::add(env.wr_agent.drv, cb);
fifo_driver cannot see what a test's callbacks will do without knowing every test. Use callbacks for cross-cutting concerns (error injection, monitoring, coverage), not for the primary function of the component.
2. uvm_event — Signaling Between Components
An uvm_event is a named notification object. One component calls trigger(); any number of others call wait_trigger(). Unlike a plain event, it's a class instance, so it can be handed around via uvm_config_db or the virtual sequencer without a global.
// In the env's build_phase:
uvm_event#() tx_done_ev = new("tx_done_ev");
uvm_config_db#(uvm_event#())::set(this, "*", "tx_done_ev", tx_done_ev);
// In the monitor, when a write completes:
function void write_wr(fifo_tx tr);
tx_done_ev.trigger();
endfunction
// In a sequence that must wait for a write to complete:
uvm_event#() tx_done_ev;
if (!uvm_config_db#(uvm_event#())::get(null, get_full_name(), "tx_done_ev", tx_done_ev))
`uvm_fatal("NOEV", "no tx_done_ev")
task body();
`uvm_do(wr_seq)
tx_done_ev.wait_trigger(); // blocks until the monitor sees the write land
`uvm_do(rd_seq)
endtask
event is a hierarchical signal — you need the path to wait on it. A uvm_event is a class handle, so it travels through config_db, the virtual sequencer, or any other mechanism that carries objects. It also has wait_trigger_data() and get_num_waiters(), which plain events lack.
3. uvm_barrier — Synchronizing N Processes
An uvm_barrier blocks waiters until exactly N of them have arrived. Then it releases all of them. This is the standard way to align several independent processes — three agents, or a driver and two monitors — before they begin a coordinated action.
// In the env's build_phase:
uvm_barrier b = new("b");
b.set_threshold(2); // wait for 2 waiters
// In each agent's run_phase, before starting traffic:
b.wait_for();
// Both waiters return from wait_for() at the same simulated time,
// so their next actions are synchronized to within a delta cycle.
Barriers are also useful at the end of a test — all agents hit the barrier, then the test drops objection. That guarantees no agent is still driving when the test is torn down.
wait_for(), the barrier blocks forever and the test hangs. Set the threshold to exactly the number of processes that will arrive, and no more. Use get_num_waiters() in check_phase to detect a mismatch.
4. When Not to Use These
| Mechanism | Use it for | Do not use it for |
|---|---|---|
uvm_callback | Cross-cutting concerns: error injection, coverage hooks, logging | Primary component behavior — use the factory override instead |
uvm_event | Signalling between loosely-coupled components (monitor → sequence) | Synchronizing two threads in the same component — use fork/join |
uvm_barrier | Aligning N processes before a shared action | Two-process handshakes — use a plain event or fork/join |
Each of these is an escape hatch, not the primary architecture. Every callback, every event, every barrier makes the testbench harder to read. Use them where the layered architecture genuinely cannot express the intent, and document the reason in a comment.
5. What Day 12 Will Add
Tomorrow: End-of-Test and Exceptions. How to end a test deterministically, how to handle DUT resets mid-test, how to inject errors so the scoreboard learns to expect them, and the objection drain-time pattern that ensures all in-flight traffic completes before teardown.
6. Quiz
Ten questions on callbacks, events, and barriers.
Coming Up Next: Day 12 — End-of-Test and Exceptions
- Objection drain time — waiting for in-flight traffic to complete before dropping objection.
- Reset handling — how to detect and respond to a mid-test reset without corrupting the scoreboard.
- Error injection — making the scoreboard expect errors when the DUT is expected to flag them.
- Timeout patterns — the "test hangs" failure mode and how to convert it into an actionable fail.
Series: 30 days of SystemVerilog/UVM verification. Day 11's escape hatches.