TO Be coming

Nov 13, 2026

UVM Course : Day 11: Callbacks and Synchronization — Extending UVM Components Without Subclassing

Day 11: Callbacks and Synchronization — Extending UVM Components Without Subclassing
Advertisement
Top banner — drop your 728×90 or responsive ad unit here.

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);
The power of callbacks: the same driver, unchanged, can be extended with error injection in one test, timing variation in another, and coverage hooks in a third. The driver's source code never mentions any of those tests.
The cost: callbacks make behavior hard to trace. A reader of 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
uvm_event vs. plain event: a plain 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.

The threshold trap: if the threshold is set to 3 but only 2 processes ever call 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

MechanismUse it forDo not use it for
uvm_callbackCross-cutting concerns: error injection, coverage hooks, loggingPrimary component behavior — use the factory override instead
uvm_eventSignalling between loosely-coupled components (monitor → sequence)Synchronizing two threads in the same component — use fork/join
uvm_barrierAligning N processes before a shared actionTwo-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.

Question 1 of 10
Loading…
    0 / 10
    Advertisement
    Bottom banner — drop your 728×90 or responsive ad unit here.

    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.
    ← Back to the 30-Day UVM Course
    See all 30 days, week by week.

    Series: 30 days of SystemVerilog/UVM verification. Day 11's escape hatches.

    SiteMap