An event is a handle to a synchronization point. One process triggers it with -> ev, or ->> ev for a nonblocking trigger in the NBA region, and another waits with @(ev).
Events can be assigned, passed into constructors, compared with == and set to null. That makes them a cheap way to connect components that never name each other.
A -> ev trigger is an instantaneous occurrence in the Active region. If no process is already blocked at @(ev) when it fires, the trigger is lost and the late waiter blocks forever.
The fix is wait (ev.triggered), which is true for the whole timestep of the trigger. It turns the edge into a level, so trigger-first and wait-first orders in one timestep both succeed.
Form | Region | Misses a past trigger |
|---|---|---|
| Edge | Yes, blocks forever |
| Whole timestep | No, within the timestep |
| Persistent | No, never |
| Sequence | Fails to else instead |
// snippet
// Losing order: the trigger fires before anyone waits.
-> ev; // nobody listening: this occurrence is lost
@(ev); // blocks forever waiting for the next oneThe losing edge above is the classic hang. The testbench below shows the level wait catching a trigger that already fired in the same timestep.
Declare an event centrally and copy the handle into each component that needs it. The trigger site and the wait site then share one object without sharing a name.
wait_order (rst_done, cfg_done, traffic_on) blocks until the events fire in exactly that order, with an else branch for violations. It suits bring-up sequencing without an FSM.
The listing below shares one event object between two classes.
timeunit 1ns; // one time unit for the classes and the module
timeprecision 1ps;
class driver_c;
event drv_done;
task run();
repeat (10) begin
#10;
-> drv_done;
end
endtask
endclass
class scoreboard_c;
event drv_done_h;
int seen;
function new(event e);
drv_done_h = e;
endfunction
task run();
forever begin
@(drv_done_h);
seen++;
$display("[%0t] scoreboard: txn complete", $time);
end
endtask
endclass
module top_evt;
initial begin
driver_c d; // declare first: initializers here would run at time 0
scoreboard_c sb;
d = new();
sb = new(d.drv_done); // copy the handle: both share one event
fork
d.run();
sb.run();
join_any
#1;
if (sb.seen != 10) $fatal(1, "FAIL: scoreboard saw %0d of 10 triggers", sb.seen);
$display("PASS: scoreboard saw all 10 triggers through the shared event");
$finish;
end
endmodule[10000] scoreboard: txn complete
[20000] scoreboard: txn complete
[30000] scoreboard: txn complete
[40000] scoreboard: txn complete
[50000] scoreboard: txn complete
[60000] scoreboard: txn complete
[70000] scoreboard: txn complete
[80000] scoreboard: txn complete
[90000] scoreboard: txn complete
[100000] scoreboard: txn complete
PASS: scoreboard saw all 10 triggers through the shared event@(ev), wait (ev.triggered), wait (flag == 1) with the flag set beside the trigger?@(ev) blocks forever. Both level forms succeed: triggered covers the firing timestep and the flag persists past it.
-> ev runs first, and in the same time step another process starts wait (ev.triggered). Does the waiter wake?Yes. triggered stays true for the rest of that time step, so a waiter that arrives after the trigger still sees it. @(ev) in its place would block forever.
One trigger, one level waiter armed after it, and a check that the waiter saw it.
module tb_event_level;
timeunit 1ns;
timeprecision 1ps;
event ev;
int level_seen = 0;
int errors = 0;
initial begin
-> ev;
fork
begin
wait (ev.triggered);
level_seen = 1;
end
join
#1;
if (level_seen !== 1) errors++;
if (errors == 0) $display("PASS: level wait caught a trigger that fired first");
else $fatal(1, "FAIL: level_seen=%0d", level_seen);
$finish;
end
endmodulePASS: level wait caught a trigger that fired firstRun with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.