Events, barriers and heartbeats

Synchronise parts of a bench that share no handshake: named events that carry data, barriers for an N-way rendezvous, and a heartbeat that catches a bench that is alive but idle.

Lesson
U5.5
Time
30 min
Runs on
Verilator with UVM
Check
4 questions

A scoreboard thread blocks for good at 60 ns, waiting on a FIFO get that nothing feeds since a reconfiguration dropped the connection. Nothing reports it: the driver keeps driving and the monitor keeps collecting. The test runs on to its two-hour timeout with a scoreboard that checked nothing after the first six items. A heartbeat would have stopped the run 45 ns after the thread stalled, naming the component that fell silent.

Three agents and a 25 ns heartbeat tick; sb stops raising at 60 ns
01234tick (ns)80105130155180window checkednone: opens80-105105-130130-155155-180drv raised-yesmon raised-yessb raised-noHBFAIL-sbfirst checked window: fatal by default

The first tick only opens the first window and clears the counts. From the second tick on, every listed component must have raised since the previous tick.

Named events with data

The item handshake synchronises a sequence with its driver and nothing else. Sometimes a monitor must tell a sequence that an overflow happened, or two sequences on different sequencers must start a burst together. Then you need something that both sides can reach without a handle to each other. UVM's answer is a named event from a pool.

uvm_event_pool::get_global_pool().get("overflow") returns the uvm_event with that name, creating it on first use. Every caller that asks for the same name gets the same object, so a producer and a consumer written in different files meet without any wiring. The example checks this: after reset, pool.get("overflow") == ev prints 1.

A uvm_event #(T) carries a payload. trigger(data) fires it and stores the object; get_trigger_data() returns that object, and get_trigger_time() the time of the last trigger. is_on() is 1 from the trigger until reset(). The example triggers at 10 ns with an incr_item whose x is 255, and the waiting process prints wait_trigger woke, x=255.

Three ways to wait

The waiting side is where most event bugs live, because the three waiting calls answer different questions.

CallReturns whenMisses a trigger that already happened?Measured
wait_trigger()The next trigger callYes, alwaysCalled at 0, woke at 10
wait_ptrigger()At once if triggered in this time step; else the next triggerYes, unless called in the trigger's own time stepCalled at 10 after the trigger, returned at 10
wait_on()At once if is_on(); else when it becomes onNo, until resetCalled at 15, returned at 15 with trigger time 10

The waiting calls of uvm_event. A fourth, wait_off, is the mirror of wait_on.

wait_ptrigger is described as a persistent trigger, and the word suggests more than it delivers. It checks the triggered flag of the underlying SystemVerilog event, and that flag is set only for the time step in which the trigger occurred. A process that calls wait_ptrigger one nanosecond later blocks until the next trigger, exactly as wait_trigger would. The example's second process calls it at 10 ns, a delta after the trigger, and it returns; move the call to 11 ns and it hangs.

For the question "has this happened yet?", use wait_on(). It reads the event's on state, which trigger sets and only reset clears, so a late consumer still gets through, and get_trigger_time() tells it how late it is. Pair it with reset() once the condition has been consumed, or every later wait_on returns at once.

Both the state and the data are shared. One event has one on flag and one data handle. A second trigger before the first was consumed overwrites the data and leaves a single on, and a reset by one listener clears it for all. The pattern below fits one consumer and one outstanding announcement. When several listeners must each see every announcement, give each its own event, with the same one-outstanding rule so that two announcements cannot merge into one. A uvm_tlm_fifo does not broadcast either: get removes the entry it returns, so one shared FIFO hands each payload to whichever consumer gets there first. For fan-out, keep one FIFO per consumer, and put clones in them if consumers change what they receive.

Waits for the next trigger

// monitor: ev.trigger(it) at 10 ns
// sequence, at 15 ns:
ev.wait_ptrigger();   // blocks: 10 ns is a past time step
start_burst();

The sequence waits for an overflow that already happened. If none follows, the test hangs.

Checks the state

// monitor: ev.trigger(it) at 10 ns
// sequence, at 15 ns:
ev.wait_on();         // returns at once: still on
ev.reset();           // consume it
start_burst();

This one consumer sees the overflow whenever it arrives, early or late, and clears it for the next one.

One more pitfall: wait_trigger after the trigger blocks forever. The example forks such a process at 10 ns and labels its message never printed; the summary shows no such line.

Barriers for a rendezvous

A uvm_barrier holds processes until a set number of them have arrived. new("burst", 3) makes one with a threshold of 3; each process calls wait_for(), and the third arrival releases all three in the same time step. get_num_waiters() reports how many are parked, and set_threshold changes the count.

The example parks A at 30 ns and B at 37 ns. C arrives at 55 ns, prints 2 already waiting, and all three print passed the barrier at 55 ns. With the default auto_reset of 1 the barrier re-arms itself after a release, so a loop of three sequences can meet at the same barrier on every iteration. Set set_auto_reset(0) to make it a one-shot gate that stays open.

The use is coordinated stimulus. Three sequences on three agents must start a burst in the same clock, or a sequence must not continue until every monitor has seen reset end. A barrier is the right tool when every party must arrive; an event is right when one party announces and the others listen.

A heartbeat for a silent bench

Objections, from U1, end a test when every component has dropped. They cannot tell that a component is still raising its objection while doing nothing useful. A uvm_heartbeat adds that check. It watches an objection of its own, here hb_obj, and a list of components; each component proves it is alive by raising and dropping hb_obj whenever it makes progress.

  1. Create the objection and the heartbeat

    hb_obj = new("hb_obj") and hb = new("hb", this, hb_obj). The second argument is the component that reports failures.

  2. Choose the mode

    set_mode(UVM_ALL_ACTIVE): every listed component must raise in every window. UVM_ANY_ACTIVE asks for at least one, UVM_ONE_ACTIVE for exactly one.

  3. Start it with an event

    set_heartbeat(tick, comps) lists the components and starts watching. The first tick.trigger() opens the first window; each later one checks the window just closed and opens the next.

  4. Prove liveness

    In each component, hb_obj.raise_objection(this); hb_obj.drop_objection(this); at each unit of work. The example does it every 10 ns.

  5. Stop it

    hb.stop() before the test ends, or the check continues into the final windows.

The example's agents are stubs named drv, mon and sb; sb stops raising at 60 ns. The heartbeat starts at 55 ns with a 25 ns tick. The first tick, at 80 ns, is not a check: it opens the first window and clears the counts, so even a component silent from the start would pass it. The window from 80 to 105 ns has no raise from sb, and at 105 ns the heartbeat reports HBFAIL naming uvm_test_top.sb, the previous tick time and sb's last update time, 60. The report repeats at 130, 155 and 180 ns, once per window, until stop() at 185 ns.

In the library HBFAIL is a UVM_FATAL: the first miss ends the run. The example demotes it to an error with set_report_severity_id_override(UVM_FATAL, "HBFAIL", UVM_ERROR) in build_phase, so you can see the window repeat and the summary count four errors. Leave it fatal in a real bench. A component that has stopped making progress cannot be trusted for the rest of the run. A run that stops at 105 ns with a name is worth more than one that reaches the timeout with a full log.

Decide what counts as progress before you wire it. A free-running pulse in run_phase proves only that the pulse's own thread is alive. Raise the heartbeat objection where work completes, in the monitor's write or the scoreboard's compare, so that "alive" means "doing its job". And expect it only when work is due. During reset, under backpressure or after the scoreboard has drained, silence is legal: stop() the heartbeat or widen its window there, and start it again when traffic resumes. The tick event is yours to drive, so tie the window to something meaningful, such as a few hundred bus cycles, rather than a fixed delay.

Choosing the tool

NeedToolWhy
A sequence waits for its driver to finish an itemThe handshake, finish_itemAlready there; nothing else to wire
One component announces; one consumer may arrive lateuvm_event from the pool, with wait_onNo handles needed; the state survives until the consumer's reset
N parties must all arrive before any continuesuvm_barrierCounts arrivals and releases all at once
Detect a component that stopped workinguvm_heartbeatNames the silent component at the end of the window

The Cookbook's guideline: prefer blocking TLM calls, and reach for events and barriers when the protocol does not provide the synchronisation.

Named events are easy to overuse. A bench where every component triggers and waits on global events is hard to read, because the control flow is no longer in any one file. Keep them for the few cross-cutting signals, give them names that say what happened, and document the producer next to the get call.

Try it

Three parts in one run. The event part shows the three waits against one trigger at 10 ns, then a reset and the pool returning the same object. The barrier releases all three at 55 ns, when the third arrives. The heartbeat then reports sb in every window after its last raise at 60 ns; the four UVM_ERROR lines are the demoted HBFAIL, and the summary counts them. The spelling "recieve" is the library's.

Delete the set_report_severity_id_override line and the run stops at 105 ns with a fatal error. Change the #0 before wait_ptrigger to #1 and that process never returns: the test then hangs in the first join, which is the failure mode to recognise.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 10 ns trigger fired, is_on=1 10 ns wait_trigger woke, x=255 10 ns wait_ptrigger (called at 10, after the trigger) returned 15 ns wait_on (called at 15) returned, trigger time 10 25 ns reset: is_on=0; pool.get("overflow") is the same object: 1 55 ns C arrives, 2 already waiting 55 ns A passed the barrier 55 ns B passed the barrier 55 ns C passed the barrier 60 ns sb falls silent UVM_ERROR @ 105: uvm_test_top [HBFAIL] Did not recieve an update of hb_obj for component uvm_test_top.sb since last event trigger at time 80 : last update time was 60 UVM_ERROR @ 130: uvm_test_top [HBFAIL] Did not recieve an update of hb_obj for component uvm_test_top.sb since last event trigger at time 105 : last update time was 60 UVM_ERROR @ 155: uvm_test_top [HBFAIL] Did not recieve an update of hb_obj for component uvm_test_top.sb since last event trigger at time 130 : last update time was 60 UVM_ERROR @ 180: uvm_test_top [HBFAIL] Did not recieve an update of hb_obj for component uvm_test_top.sb since last event trigger at time 155 : last update time was 60 185 ns heartbeat stopped UVM_ERROR : 4

Check yourself

Predict the outputWhat line does the $display print? UVM's own messages are not part of the answer.
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    uvm_event #(uvm_object) ev = uvm_event_pool::get_global("go");
    int p, q, r;
    phase.raise_objection(this);
    fork
      begin #10; ev.trigger(); end
      begin #10; #0; ev.wait_ptrigger(); p = $time; end
      begin #20; ev.wait_on(); q = $time; end
      begin #5; ev.wait_trigger(); r = $time; end
    join
    $display("ptrigger %0d, wait_on %0d, wait_trigger %0d", p, q, r);
    phase.drop_objection(this);
  endtask
endclass
module tb;
  initial run_test("my_test");
endmodule
ConceptA monitor triggers uvm_event_pool::get_global("overflow") at 10 ns. A sequence calls wait_ptrigger() on the same event at 12 ns, and no further overflow occurs. What happens?
ConceptA barrier is created with new("sync", 3) and left with its default settings. Four sequences call wait_for() on it, arriving at 5, 12, 30 and 41 ns. When does each return?
ConceptA heartbeat in UVM_ALL_ACTIVE mode watches a driver, a monitor and a scoreboard. It was started at time 0, and its event ticks at 1, 2, 3 µs and so on. The scoreboard raises the heartbeat objection every 100 ns until its thread dies just after its raise at 3.2 µs; the others keep raising. With HBFAIL at its default severity, what does the run do?

Remember

  • A uvm_event from the global pool is shared by name; trigger(data) carries an object that get_trigger_data() returns.
  • wait_trigger waits for the next trigger; wait_ptrigger returns at once only within the trigger's own time step; wait_on asks whether the event is on, until reset().
  • A uvm_barrier releases every waiter when the threshold is reached and, by default, re-arms for the next round.
  • A uvm_heartbeat fails a component that did not raise its objection within a window of its tick event; HBFAIL is fatal by default.
  • Use the handshake when it exists, events for announcements, barriers for a rendezvous, and a heartbeat to catch a bench that is alive but idle.