Part 2 · OOP for Verification · Intermediate
Events & event Triggering
-> trigger vs @ wait, the .triggered same-timestep fix, passing event handles, and wait_order.
What an event is — and is not
A SystemVerilog event is the lightest synchronization object: it carries no data and has no persistent state across timesteps. It is a handle to a synchronization point. One process triggers it with -> ev (or non-blocking ->> ev, which triggers in the NBA region), and another process waits on it with @(ev). Because events are class-like handles, they can be assigned, passed to tasks and constructors, compared with ==, and set to null — which is what makes them useful for connecting components that never see each other directly.
The scheduling semantics are the part that bites. A -> ev trigger is an instantaneous occurrence in the Active region of the current timestep. It does not persist: if no process is already blocked at @(ev) when the trigger fires, the trigger is simply lost. This is the root of the classic event race.
The classic race: trigger before wait
If the triggering process runs before the waiting process within the same timestep, @(ev) blocks forever — the edge already happened. The fix is the ev.triggered property: it is true for the entire timestep in which the event fired, regardless of statement ordering. Use it inside a level-sensitive wait() instead of the edge-sensitive @.
module event_race;
event ev;
initial begin // Process A: triggers FIRST in this timestep
-> ev;
$display("[%0t] A: triggered ev", $time);
end
initial begin // Process B: edge wait — LOSES the race
@(ev); // ev already fired; blocks forever
$display("[%0t] B: saw edge", $time); // never prints
end
initial begin // Process C: level wait — ROBUST
wait (ev.triggered); // true for the whole timestep of the trigger
$display("[%0t] C: saw triggered", $time); // prints at time 0
end
endmoduleTIMESTEP 0, Active region — statement ordering race
order of execution @(ev) waiter wait(ev.triggered) waiter
────────────────── ──────────── ────────────────────────
A: -> ev fires
B: @(ev) starts wait BLOCKED FOREVER —
C: wait(.triggered) — UNBLOCKS (same timestep)
-> ev : edge — visible only to processes already waiting
ev.triggered : level — true for the entire timestep of the trigger
Rule: for same-timestep handshakes, always wait(ev.triggered).Persistent handshakes still need care
Note that ev.triggered only spans one timestep — it is not a sticky flag. If the waiter arrives one timestep late, even wait(ev.triggered) misses it. For handshakes that must survive arbitrary delays, use a mailbox or a bit flag with wait(flag) semantics instead.
Passing event handles and wait_order
Because events are handles, a testbench can declare them centrally and pass them into components, decoupling the trigger site from the wait site. This is the poor man's analysis port: the generator and the scoreboard never reference each other, only the shared event.
class driver;
event drv_done; // owned by driver
task run();
repeat (10) begin
#10; // drive a transaction (stub)
-> drv_done;
end
endtask
endclass
class scoreboard;
event drv_done_h; // handle COPIED from driver's event
function new(event e);
drv_done_h = e; // both handles refer to one event object
endfunction
task run();
forever begin
@(drv_done_h); // wakes when driver triggers ITS event
$display("[%0t] scoreboard: txn complete", $time);
end
endtask
endclass
module top;
initial begin
driver d = new();
scoreboard sb = new(d.drv_done); // pass the handle at construction
fork
d.run();
sb.run();
join_any
end
endmodulewait_order — enforcing a sequence of events
wait_order(a, b, c) blocks until the events trigger in exactly that order; any out-of-order trigger fails (optionally into an else branch). It is handy for checking phase ordering — for example reset done, then config done, then traffic started — without writing an FSM.
event rst_done, cfg_done, traffic_on;
initial begin
wait_order (rst_done, cfg_done, traffic_on)
$display("bring-up sequence correct");
else
$error("bring-up events out of order!");
endInterview angle
"Why does @(ev) hang when -> ev already executed?" — explain edge vs level and .triggered.
"Difference between -> and ->>?" — Active region vs NBA region trigger; ->> avoids some same-region races.
"How do two components synchronize without knowing each other?" — shared event handle passed at construction.
Key takeaways
-> ev is an instantaneous Active-region occurrence; it is lost if nobody is waiting.
wait(ev.triggered) is level-sensitive across the whole timestep — the standard fix for same-timestep races.
Events are handles: pass them into constructors to decouple trigger site from wait site.
wait_order checks event sequencing declaratively, with an else branch for violations.
Common pitfalls
Using @(ev) for a handshake where trigger may precede wait in the same timestep — silent hang.
Assuming ev.triggered is sticky — it is true only for the trigger's timestep, not afterward.
Triggering an event through a null handle — fatal runtime error; assign before use.
Using events to carry data by side-channel globals — use a mailbox; events signal moments only.