Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
    • Callbacks, Command Line, Seeds and Versions
    • Synchronisation and Reporting
      • Events, Barriers and Pools
      • Report Catchers and Waivers
      • Custom Report Servers and the Verdict Line
      • Heartbeats and Watchdogs
      • Transaction Recording
    • Testbenches at Scale
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Synchronisation and Reporting
  1. Topics
  2. UVM Verification
  3. Advanced UVM in Practice
  4. Synchronisation and Reporting
  5. Events, Barriers and Pools

Part 10 · Advanced Topics · Intermediate

Events, Barriers and Pools

uvm_event and its five waits, the wait_trigger versus wait_ptrigger race, trigger with data, the global event pool behind the interrupt agent, uvm_barrier, uvm_pool and uvm_queue, and when to use each instead of TLM or config_db.

On this page

    A subsystem regression has 400 seeds. Seed 317 hangs until the global timeout; every other seed passes. The hung test is waiting on irq_ev.wait_trigger(), and the waveform shows the interrupt line high, with the monitor's trigger at exactly the clock edge on which the test called wait_trigger(). The test arrived one scheduling step late, on the same edge, and waited for a ring that had already happened.

    This lesson is about the objects UVM gives you for that kind of coordination: uvm_event, uvm_barrier, and the global pools that let unrelated components find them by name. Each solves a real problem, each has one trap, and the race above is the most famous.

    The idea: a doorbell, an echo, and a flag on the mailbox

    A SystemVerilog event is a doorbell. ->ev rings it; @ev listens for the next ring. If you start listening a moment after it rang, you hear nothing until it rings again. uvm_event wraps that doorbell in an object with state, so it can also be read like a flag on a mailbox: once triggered it stays on until someone calls reset(). It records the trigger time, can carry a data object with the trigger, counts its waiters, and supports callbacks that run before and after each trigger.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // snippet: an event callback (IEEE 1800.2 form)
    class irq_cb extends uvm_event_callback #(uvm_object);
      `uvm_object_utils(irq_cb)
      function new(string name = "irq_cb"); super.new(name); endfunction
      virtual function bit pre_trigger(uvm_event #(uvm_object) e, uvm_object data);
        return 0;                         // 1 would cancel the trigger
      endfunction
      virtual function void post_trigger(uvm_event #(uvm_object) e, uvm_object data);
        `uvm_info("IRQCB", "dma_irq triggered", UVM_HIGH)
      endfunction
    endclass
    // attach: irq_ev.add_callback(cb);   (append by default)

    That gives you three ways to wait, and choosing among them is the whole skill. wait_trigger() listens for the next ring. wait_ptrigger() also accepts a ring that already happened in the current time step (the echo). wait_on() returns at once if the flag is up, however long ago it was raised, and otherwise waits for the first trigger.

    The picture: one edge, two waiters

    Sequence diagram: the interrupt monitor triggers the event; a test calling wait_trigger after it in the same time step blocks, while one calling wait_ptrigger returns immediately.
    Both waiters run in the same time step as the trigger, but after it. wait_trigger needs a new trigger; wait_ptrigger sees the one that already happened.

    Build it: the bench's interrupt event

    The bench's interrupt monitor never hands its virtual interface to anyone. It fetches a global event by name in build_phase and triggers it on every rising edge of the line:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // snippet: from irq_monitor in the course DMA bench (dma_tb.sv)
    function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      if (!uvm_config_db#(virtual irq_if)::get(this, "", "vif", vif))
        `uvm_fatal("NOVIF", "irq_if not set for the interrupt monitor")
      irq_ev = uvm_event_pool::get_global("dma_irq");
    endfunction
    
    task run_phase(uvm_phase phase);
      bit last = 0;
      forever begin
        @(posedge vif.clk);
        if (vif.irq !== last) begin
          irq_item it = irq_item::type_id::create("it");
          it.level = vif.irq;
          last = vif.irq;
          ap.write(it);
          if (it.level) irq_ev.trigger();
          `uvm_info("IRQ_MON", $sformatf("irq -> %0b", it.level), UVM_MEDIUM)
        end
      end
    endtask

    dma_irq_agent_test waits on the same name. Nothing connects the two components: the global event pool is the meeting point. uvm_event_pool::get_global("dma_irq") returns the one event stored under that name, creating it on first use, so it does not matter which side asks first.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // snippet: from dma_irq_agent_test in the course DMA bench (dma_tb.sv)
    task run_phase(uvm_phase phase);
      uvm_event    irq_ev = uvm_event_pool::get_global("dma_irq");
      axis_pkt_seq pkt    = axis_pkt_seq::type_id::create("pkt");
      bit [31:0] status;
      phase.raise_objection(this);
      reg_write(KEY, 32'h0F0F_0F0F);
      reg_write(LEN, 4);
      reg_write(CTRL, 32'h3);
      if (!pkt.randomize() with { n == 4; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
      fork
        pkt.start(env.in_agt.sqr);
      join_none
      irq_ev.wait_trigger();                   // no virtual interface needed here
      reg_read(STATUS, status);
      `uvm_info("IRQ", $sformatf("interrupt event seen, STATUS=%08h", status), UVM_LOW)
      reg_write(STATUS, 32'h6);
      #100ns;
      phase.drop_objection(this);
    endtask

    This is the decoupling that makes the interrupt agent reusable (its full story is in Interrupt Agents). The wait is safe here only because the job takes several hundred nanoseconds: the test is blocked in wait_trigger() long before the line rises.

    The race, in plain SystemVerilog

    uvm_event::wait_trigger() is @m_event and wait_ptrigger() checks m_event.triggered first; that is the whole difference, and the library's own comment on wait_trigger warns that a wait arriving after a same-delta trigger "may never occur and thus cause deadlock". The model below runs both waits against a module-level event, each arriving just after the trigger in the same time step:

    event_race.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // The same-time-step race in plain SystemVerilog. Both waiters arrive at 10,
    // just after the trigger. @ev waits for the NEXT trigger; ev.triggered is still
    // true for the rest of the time step.
    module event_race;
      event ev;
      bit   saw_at, saw_triggered;
    
      initial #10 -> ev;                      // the "monitor" triggers at 10
    
      initial begin                           // waiter 1: @ev (what wait_trigger does)
        #10; #0;                              // same time, after the trigger
        fork
          begin @ev; saw_at = 1; end
          #5;
        join_any
        if (saw_at) $display("@ev:              seen");
        else        $display("@ev:              missed (it waits for the next trigger)");
      end
    
      initial begin                           // waiter 2: ev.triggered (what wait_ptrigger checks)
        #10; #0;
        fork
          begin wait (ev.triggered); saw_triggered = 1; end
          #5;
        join_any
        if (saw_triggered) $display("wait(triggered):  seen");
        else               $display("wait(triggered):  missed");
      end
    endmodule
    Diagram
    wait(triggered):  seen
    @ev:              missed (it waits for the next trigger)

    Two honest caveats. First, #0 makes the order deterministic here; in a real bench the order of two processes waking on the same edge is up to the simulator, which is why the hang appears on one seed or after a tool upgrade. Second, a scratch run of the same race through uvm_event on Verilator 5.048 did not miss: that simulator treats an event declared inside a class differently from a module-level one. A simulator that passes is not proof that your wait is safe; the rule is what you code to.

    The rest of the event API

    Call

    Returns at once if...

    Use it for

    wait_trigger()

    never: waits for the next trigger

    Repeating events where you want the next occurrence, and the waiter is known to be early.

    wait_ptrigger()

    it was triggered earlier in this time step

    A waiter that may arrive on the same edge as the trigger.

    wait_on(delta)

    it is on (triggered since the last reset())

    One-shot conditions: "reset done", "config done". Pass delta = 1 to yield one #0 first.

    wait_off(delta)

    it is off

    Waiting for someone to reset() the event.

    wait_trigger_data(obj) / wait_ptrigger_data(obj)

    as above

    Receiving the object passed to trigger(obj).

    reset(wakeup)

    (function)

    Turning the flag off; wakeup = 1 releases current waiters first.

    get_trigger_time(), is_on(), get_num_waiters()

    (functions)

    Diagnostics, and end-of-test checks for stranded waiters.

    A trigger can carry an object, which turns the event into a tiny one-to-many message. The scratch run below passes the STATUS and COUNT a handler read; the waiter casts it back:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // snippet: trigger with data (scratch run, sync_tb.sv)
    class irq_info extends uvm_object;
      `uvm_object_utils(irq_info)
      int unsigned status;
      int unsigned count;
      function new(string name = "irq_info");
        super.new(name);
      endfunction
    endclass
    
    // waiter
    uvm_object obj;
    irq_info   info;
    done_ev.wait_trigger_data(obj);
    if (!$cast(info, obj)) `uvm_fatal("CAST", "dma_done carried the wrong type")
    
    // triggerer
    info = irq_info::type_id::create("info");
    info.status = 'h2;
    info.count  = 8;
    done_ev.trigger(info);

    Note

    In IEEE 1800.2 and UVM 1.2, uvm_event is the parameterised class uvm_event#(T) with T defaulting to uvm_object, and the global pool holds uvm_event#(uvm_object). A typed event such as uvm_event#(irq_info) gives a compile-time type, but it cannot live in uvm_event_pool; keep it in a config object or a typed uvm_pool.

    Barriers: N threads meet, then all go

    A uvm_barrier releases its waiters only when a threshold number of them have arrived. The typical use is a rendezvous: three agents finish their own configuration at different times, and none may start traffic until all are ready. The barrier pool works like the event pool:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // snippet: three agents meet at a barrier (scratch run, sync_tb.sv)
    uvm_barrier cfg_done = uvm_barrier_pool::get_global("cfg_done");
    cfg_done.set_threshold(3);
    fork
      begin #120ns; cfg_done.wait_for(); /* APB agent starts */    end
      begin #40ns;  cfg_done.wait_for(); /* stream agent starts */ end
      begin #300ns; cfg_done.wait_for(); /* sink agent starts */   end
    join
    Diagram
    scratch run: +UVM_TESTNAME=barrier_test
    UVM_INFO sync_tb.sv(114) @ 40000: uvm_test_top [BAR] stream agent configured, waiting
    UVM_INFO sync_tb.sv(113) @ 120000: uvm_test_top [BAR] APB agent configured, waiting
    UVM_INFO sync_tb.sv(115) @ 300000: uvm_test_top [BAR] sink agent configured, waiting
    UVM_INFO sync_tb.sv(114) @ 300000: uvm_test_top [BAR] stream agent released
    UVM_INFO sync_tb.sv(113) @ 300000: uvm_test_top [BAR] APB agent released
    UVM_INFO sync_tb.sv(115) @ 300000: uvm_test_top [BAR] sink agent released

    The barrier's trap is its count. If the threshold says 3 and a configuration with only two agents runs, both wait forever. If a waiting thread is killed (a reset, a disable fork), its arrival is still counted unless someone calls cancel(). Set the threshold from the same config object that decides how many agents exist.

    Pools and queues: global by name

    uvm_pool#(KEY,T) is an associative array wrapped in an object, and uvm_queue#(T) a queue; each has a get_global_pool() / get_global_queue() singleton per type. The event and barrier pools are string-keyed pools of objects. One behaviour matters more than the rest: `get()` on a missing key inserts it. For object pools it creates a new object under that name; for a pool of integers or handles it stores the default value. The scratch run shows the integer case:

    Diagram
    scratch run: +UVM_TESTNAME=pool_test
    UVM_INFO sync_tb.sv(135) @ 0: uvm_test_top [POOL] exists(dma2.jobs)=0
    UVM_INFO sync_tb.sv(136) @ 0: uvm_test_top [POOL] get(dma2.jobs)=0, num()=3 after the get

    Use exists() before get() whenever a missing key means a bug. And use global pools sparingly: they are global mutable state, invisible in the component tree, shared by every test in the run.

    Which mechanism, when

    You need to...

    Use

    Why not the others

    Pass every transaction to any number of listeners

    TLM analysis port

    An event carries one moment and drops data a slow waiter misses; a port delivers every item, in order, through connect() you can see in the topology.

    Signal a moment ("interrupt", "reset done") to whoever cares

    uvm_event, by name or by handle

    A port needs a connection and a subscriber class; config_db is for values, not moments.

    Make N threads meet before any continues

    uvm_barrier

    N events plus a counter is a barrier you wrote yourself.

    Hand a setting to a component before it runs

    config_db or a config object

    Events and pools have no build-phase precedence rules and no audit trail.

    Share a registry across unrelated code (rare)

    uvm_pool / uvm_queue

    Prefer a config object reachable from the env; a global pool is the last resort.

    Watch it run

    The bench's interrupt test uses wait_trigger() safely because the test is early. The scratch run exercises the other waits on a UVM event, including the name typo that the next section is about:

    Diagram
    scratch run: +UVM_TESTNAME=ev_race_test
    UVM_INFO sync_tb.sv(69) @ 70000: uvm_test_top [RACE] wait_on 50 ns later: returned at once (is_on=1, trigger time 20000)
    UVM_INFO sync_tb.sv(73) @ 70000: uvm_test_top [RACE] after reset: is_on=0
    UVM_INFO sync_tb.sv(82) @ 90000: uvm_test_top [DATA] dma_done carried STATUS=2 COUNT=8
    UVM_INFO sync_tb.sv(97) @ 290000: uvm_test_top [TYPO] waiting on "dma-done": never triggered; pool now holds 4 events
    • wait_on returned 50 ns after the trigger: the event stays on until reset(). That is the safe wait for a one-shot condition.

    • trigger with data delivered the handler's object to a waiter that never saw the handler.

    • The typo created a fourth event. The run triggered dma_done; the waiter listened on dma-done, which nothing will ever trigger.

    What breaks: the event nobody triggers

    An SoC team split interrupt handling across two packages. The interrupt VIP triggered "dma_irq"; a sequence written by another engineer waited on "dma_irq0", copied from a spec that numbered the line. The test hung until the 9,200-second default timeout, on every seed, and the first two days went into the DUT, because the waveform clearly showed the interrupt firing.

    The root cause is that get_global() never fails: it creates whatever name you ask for. Two fixes became the team's rule. Event names are constants in the agent's package (localparam string DMA_IRQ_EV = "dma_irq";), so a typo is a compile error. And the base test's check_phase walks the event pool and reports any event that has waiters but was never triggered, which turns the next hang into a one-line diagnosis:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet: in the base test
    function void check_phase(uvm_phase phase);
      uvm_event_pool pool = uvm_event_pool::get_global_pool();
      string name;
      super.check_phase(phase);
      if (pool.first(name)) do begin
        uvm_event ev = pool.get(name);
        if (ev.get_num_waiters() > 0 && ev.is_off())
          `uvm_warning("EV_STRANDED", $sformatf("event \"%s\" has %0d waiter(s) and was never triggered", name, ev.get_num_waiters()))
      end while (pool.next(name));
    endfunction

    Interview

    "What is the difference between wait_trigger and wait_ptrigger, and when would you use wait_on?"

    A weak answer: "wait_ptrigger is persistent, so it is safer." Persistent for how long? Safer than what?

    A strong answer: "wait_trigger is @event: it waits for the next trigger, so if the trigger happens earlier in the same time step, for example the monitor and the test both waking on one clock edge, it misses it and may hang. wait_ptrigger first checks the event's triggered state, which lasts only until the end of the time step, so it covers that race and nothing older. For one-shot conditions like reset done I use wait_on, which returns as long as the event is on, until someone resets it. And I keep event names as package constants, because get_global silently creates a new event for a typo."

    Expect the follow-ups: When would you use an event instead of an analysis port? (a moment with no data stream, many unknown listeners; never to pass every transaction) and What does reset(1) do? (wakes current waiters, then turns the event off).

    Check yourself A reset agent triggers "reset_done" once at 200 ns. A sequence started at 500 ns calls wait_ptrigger() on it. What happens, and what should it call?

    It hangs: the trigger was in an earlier time step, so wait_ptrigger() waits for a new one that never comes. A one-shot condition needs wait_on(), which returns because the event is still on (nobody reset it).

    Check yourself A barrier's threshold is set to 4 in the base test. A derived test disables one of the four agents. What do you see, and what is the fix?

    The three remaining agents wait forever at the barrier; the test ends on the global timeout. The threshold must come from the same configuration that decides which agents exist, for example the number of active agents in the env config object.

    Check yourself Why is uvm_pool::get() on a missing key dangerous in a scoreboard that counts jobs per channel?

    It inserts the key with a default value instead of failing, so a typo or an unexpected channel silently creates a new entry with count 0. Use exists() first when a missing key is a bug, and report it.

    Next: synchronisation decides who waits. The next lesson is about what a message means once it is reported, and how to waive one known error without hiding the next real one: Report Catchers and Waivers.

    Related topics

    • Up: Synchronisation and Reporting

    • Interrupt Agents

    • Analysis broadcast

    • The config_db contract

    Previous

    174 / 280

    Next: Report Catchers and Waivers

    Up next

    Up nextReport Catchers and Waivers

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Custom Report Servers and the Verdict Line
    • Heartbeats and Watchdogs
    • Transaction Recording