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 @.

systemverilog
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
endmodule
diagram
TIMESTEP 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.

systemverilog
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
endmodule

wait_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.

systemverilog
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!");
end

Interview 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.