@event Vs wait(event.triggered) in SystemVerilog

In short: Both @(e) and wait(e.triggered) pause a process until event e fires, but they treat timing differently. @(e) is edge-sensitive: the process must already be blocked on it before the trigger, or it misses the event. wait(e.triggered) is level-sensitive within the current time step: it still catches the event even if the trigger happened an instant earlier in the same time slot. Use wait(e.triggered) when a race between the trigger and the wait is possible.

SystemVerilog gives two ways to wait for a named event. The difference is subtle but causes real bugs. @(e) only unblocks on a fresh trigger that happens after the process is already waiting. wait(e.triggered) checks a flag that stays true for the rest of the current time step, so the order of the trigger and the wait within the same step no longer matters. This is the classic fix for a “missed event” race.

A simple way to picture it

Imagine waiting for a doorbell. @(e) is like listening at the door: you only hear the bell if you are already listening at the exact moment it rings. If you walk up a second too late, you missed it and must wait for the next ring. wait(e.triggered) is like a bell that also lights a lamp that stays on for the rest of the minute. Even if you arrive just after it rang, the lamp is still lit, so you know it happened and you go in.

The core difference in one look

Aspect@(e)wait(e.triggered)
SensitivityEdge: needs a new trigger while blockedLevel within the current time step
Order of trigger vs waitMust wait first, then triggerEither order works in the same step
Race safetyCan miss the eventCatches the event in the same step
Still needs same time step?YesYes
Typical useWhen the wait is guaranteed to run firstWhen trigger and wait may race

The missed-event race with @(e)

Here the trigger runs before the wait, both at time 0. Because @(e) is edge-sensitive, the process was not yet blocked when the trigger fired, so it misses it and the timeout branch wins.

class ping;
  event e;

  task trigger();
    ->e;                  // fires the event now
  endtask

  task wait_edge();
    @(e);                 // edge-sensitive: must be blocked first
    $display("[%0t] caught with @(e)", $time);
  endtask

  task run();
    trigger();            // trigger happens BEFORE the wait
    wait_edge();
  endtask
endclass

module tb;
  initial begin
    ping p = new();
    fork
      p.run();
      begin #10 $display("[%0t] missed the event", $time); end
    join_any
    disable fork;
  end
endmodule

The event is triggered and gone before @(e) starts listening, so the “missed the event” line prints at time 10. The @(e) would wait forever otherwise.

The fix with wait(e.triggered)

Swap the edge wait for wait(e.triggered). The triggered flag is set for the whole current time step, so even though the trigger ran first, the wait still sees it and unblocks immediately.

class ping;
  event e;

  task trigger();
    ->e;
  endtask

  task wait_level();
    wait(e.triggered);    // level-sensitive in this time step
    $display("[%0t] caught with wait(e.triggered)", $time);
  endtask

  task run();
    trigger();            // trigger still happens first
    wait_level();
  endtask
endclass

module tb;
  initial begin
    ping p = new();
    fork
      p.run();
      begin #10 $display("[%0t] missed the event", $time); end
    join_any
    disable fork;
  end
endmodule

Now the “caught with wait(e.triggered)” line prints at time 0, the timeout never fires, and the race is gone.

Blocking vs non-blocking trigger

There are also two ways to fire an event. ->e is the blocking trigger; it takes effect in the active region of the current step. ->>e is the non-blocking trigger; it schedules the trigger for the non-blocking region of the same step, a little later. The non-blocking form is useful when you want the waiters that are already blocked to be the ones that see it, avoiding some same-step ordering surprises.

event blocking_e;
event nonblk_e;

initial begin
  ->  blocking_e;   // blocking trigger: active region
  ->> nonblk_e;     // non-blocking trigger: NBA region, slightly later
end

In practice, reach for wait(e.triggered) to make a waiter race-safe, and consider ->> when you need the trigger to land after processes have had a chance to block in the same step.

Which one should you use?

  • Use @(e) when you can guarantee the waiting process is already blocked before any trigger, for example a monitor that sits on the event from time 0.
  • Use wait(e.triggered) whenever the trigger and the wait could happen in either order in the same time step. This is the safe default for handshakes between parallel processes.
  • Consider ->> (non-blocking trigger) when you want the trigger to take effect after waiters have blocked in the same step.

Expected behaviour, in plain words

In the first example the trigger fires before @(e) is listening, so the event is missed and the timeout prints “missed the event” at time 10. In the second example wait(e.triggered) sees the flag set in the same time step and prints its “caught” line at time 0, so the timeout never runs. I review this against the language rules rather than run it, so confirm the exact timestamps in your own simulator or on EDA Playground.

Common mistakes to avoid

  • Using @(e) across a race. If the trigger can fire before the wait blocks, the event is lost. Use wait(e.triggered) instead.
  • Assuming wait(e.triggered) works across time steps. The triggered flag is only set for the current time step; you still must reach the wait in the same step.
  • Confusing the two triggers. -> is blocking (active region), ->> is non-blocking (NBA region). They land at slightly different points in the step.
  • Triggering before any process is listening with @. A single unmatched trigger simply vanishes.
  • Relying on ordering between parallel threads. Do not assume which of two forked threads runs first; design for either order.

Keep learning

Events are one way threads talk to each other. Compare them with the constructs in fork join in SystemVerilog, and see class basics in encapsulation in SystemVerilog. For synchronization inside a testbench, read the UVM testbench architecture. More is in our SystemVerilog guides and interview questions.

Frequently asked questions

What is the difference between @(e) and wait(e.triggered)?

@(e) is edge-sensitive: the process must already be blocked on it before the event fires, or it misses the trigger. wait(e.triggered) is level-sensitive within the current time step, so it catches the event even if the trigger happened an instant earlier in the same step.

Why does @(e) sometimes miss an event?

Because it only reacts to a trigger that occurs while the process is already waiting. If the trigger fires before the process reaches @(e), even in the same time step, there is nothing to react to and the event is lost.

How does wait(e.triggered) avoid the race?

When an event is triggered, its triggered property stays true for the rest of that time step. wait(e.triggered) checks that flag, so it succeeds whether the trigger came just before or just after the wait, as long as both are in the same step.

Does wait(e.triggered) work across different time steps?

No. The triggered flag is only set for the current time step. The waiting process must execute the wait in the same time step as the trigger to catch it.

What is the difference between -> and ->> for triggering?

-> is a blocking trigger that takes effect in the active region of the current time step. ->> is a non-blocking trigger that schedules the event in the non-blocking region of the same step, so it lands slightly later.

When should I prefer @(e) over wait(e.triggered)?

Use @(e) when you can guarantee the waiting process is already blocked before any trigger, such as a monitor that waits from the start. When the trigger and wait can happen in either order, wait(e.triggered) is the safer choice.

Similar Posts