Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
    • Classes, Objects & Handles
    • Inheritance and Polymorphism
    • Static, Encapsulation & Parameterized Classes
    • Virtual Interfaces: the Class-to-Pins Bridge
    • Events, Semaphores, Mailboxes and Processes
      • Events: Edge Waits and Level Fixes
      • Semaphores: One Key and Many
      • Mailboxes: Typed Pipes with Backpressure
      • Threads and Control: Fork, Disable, Wait, Process
      • Synchronization Patterns: Handshakes, Phasing and Objections
      • Lab: Generator-to-Driver Handshake
    • OOP Patterns for Testbenches
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Events, Semaphores, Mailboxes and Processes
  1. Topics
  2. SystemVerilog
  3. Object-Oriented SystemVerilog for Verification
  4. Events, Semaphores, Mailboxes and Processes
  5. Events: Edge Waits and Level Fixes

Part 2 · OOP for Verification · Intermediate

Events: Edge Waits and Level Fixes

Edge waits, same-timestep races and the level-sensitive fix with triggered.

On this page

    Why this matters

    The test hung forever at time 0, right after the trigger. The trigger had fired a moment before anyone was waiting, and @(ev) waits for the next one.

    After this lesson you can: trigger and wait on events, share an event handle between classes, and pick an edge or a level wait.

    An event carries no data

    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.

    Trigger before wait

    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

    @(ev)

    Edge

    Yes, blocks forever

    wait (ev.triggered)

    Whole timestep

    No, within the timestep

    wait (flag == 1)

    Persistent

    No, never

    wait_order (a, b, c)

    Sequence

    Fails to else instead

    design.sv
    1
    2
    3
    4
    // snippet
    // Losing order: the trigger fires before anyone waits.
    -> ev;      // nobody listening: this occurrence is lost
    @(ev);      // blocks forever waiting for the next one

    The losing edge above is the classic hang. The testbench below shows the level wait catching a trigger that already fired in the same timestep.

    Handles and ordering

    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.

    top_evt.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    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
    Terminal
    [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

    Common pitfall

    ev.triggered spans one timestep only, not the whole test. A waiter that arrives a timestep late still misses it, so handshakes that must survive arbitrary delays need a mailbox or a sticky flag with wait (flag).

    Interview

    Likely questions: why @(ev) hangs after -> ev already ran, how -> differs from ->>, and how two components sync without naming each other.

    • Edge versus level, plus .triggered as the same-timestep cure.

    • Active region versus NBA region trigger.

    • One shared event handle passed at construction.

    Check yourself The trigger fires at time 0 and the waiter starts at time 0, just after. Which waits succeed: @(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.

    Testbench

    Predict first -> 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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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
    endmodule
    Terminal
    PASS: level wait caught a trigger that fired first

    Remember

    • @(ev) waits for the next trigger and misses one that already fired.

    • wait (ev.triggered) sees a trigger from the same time step.

    • Pass one event handle to every component that needs it.

    Rule of thumb: if the trigger can come first, wait on the level.

    How this was checked

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

    Related topics

    • Next: Semaphores

    Previous

    54 / 259

    Next: Semaphores: One Key and Many

    Up next

    Up nextSemaphores: One Key and Many

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Mailboxes: Typed Pipes with Backpressure
    • Threads and Control: Fork, Disable, Wait, Process
    • Synchronization Patterns: Handshakes, Phasing and Objections