Why Learn Delay and Event Control? Every testbench needs to decide when things happen: when to apply the next input, when to wait for a clock edge, and when to check a result. Delay and event controls are how Verilog describes time, and the @(posedge clk) you write in every flip-flop is one of them. In this tutorial, you'll learn how to pause a block for a fixed time, for a signal change, for a named event, or until a condition becomes true.

Statements inside initial and always blocks normally run one after the other in zero simulation time. A timing control placed in front of a statement makes the block wait before executing it. Verilog has two kinds: delay control, which waits for a fixed amount of time, and event control, which waits until something happens in the simulation, such as a signal changing or a named event being triggered.

14 min read | Beginner Level

What You'll Learn

  • Use # delays, and how x, z and negative delay values are handled
  • Wait for edges and value changes with @, and for several events with or and commas
  • Synchronize processes with named events, and build complete sensitivity lists with @*
  • Use wait for level-sensitive control, and know which of these controls synthesis supports

Delay Control

A delay control is written as # followed by a number or an expression. It delays the execution of the statement after it by that many time units:


#10 a = 1;          // Wait 10 time units, then assign
#(a + b) b = 0;     // The delay can be any expression
#5;                 // A delay with an empty statement just waits

The rules for the delay value are:

  • The value is in the time unit of the module, set by `timescale, and can be a fraction down to the time precision.
  • The expression is evaluated when the block reaches it, so a delay can depend on signal values.
  • If the value is x or z, it is treated as 0.
  • A delay cannot be negative. If the expression gives a negative result, it is interpreted as an unsigned number with the width of the expression, which is usually a large delay.

The example below uses a time unit of 1 ns and a precision of 1 ps, and tries each kind of delay value:


`timescale 1ns/1ps

module tb;
  reg [3:0] a, b;

  initial begin
    {a, b} <= 0;
    $display ("T=%0t a=%0d b=%0d", $realtime, a, b);

    #10;
    a <= $random;
    $display ("T=%0t a=%0d b=%0d", $realtime, a, b);

    #10 b <= $random;
    $display ("T=%0t a=%0d b=%0d", $realtime, a, b);

    #(a) $display ("T=%0t After a delay of a=%0d units", $realtime, a);
    #(a+b) $display ("T=%0t After a delay of a=%0d + b=%0d = %0d units", $realtime, a, b, a+b);
    #((a+b)*0.01) $display ("T=%0t After a delay of %0d * 10ps", $realtime, a+b);

    #(b-a) $display ("T=%0t Expr evaluates to a negative delay", $realtime);
    #('h10) $display ("T=%0t Delay in hex", $realtime);

    a = 'hX;
    #(a) $display ("T=%0t Delay is unknown, taken as zero a=%h", $realtime, a);

    a = 'hZ;
    #(a) $display ("T=%0t Delay is in high impedance, taken as zero a=%h", $realtime, a);

    #0.001 $display ("T=%0t Delay of 1ps", $realtime);
  end

endmodule
Output
xcelium> run
T=0 a=x b=x
T=10000 a=0 b=0
T=20000 a=4 b=0
T=24000 After a delay of a=4 units
T=29000 After a delay of a=4 + b=1 = 5 units
T=29050 After a delay of 5 * 10ps
T=42050 Expr evaluates to a negative delay
T=58050 Delay in hex
T=58050 Delay is unknown, taken as zero a=x
T=58050 Delay is in high impedance, taken as zero a=z
T=58051 Delay of 1ps
xmsim: *W,RNQUIE: Simulation is complete.

The %t format prints time in the simulation precision of 1 ps, so T=24000 means 24 ns. Each delay adds to the time at which the previous statement ran:

StatementDelayTime after it
#10; twice10 ns + 10 ns20 ns
#(a)a = 4, so 4 ns24 ns
#(a+b)4 + 1 = 5 ns29 ns
#((a+b)*0.01)5 × 0.01 ns = 50 ps29.05 ns
#(b-a)1 - 4 in 4 bits = 13 ns42.05 ns
#('h10)16 ns58.05 ns
#(a) with a = x, then z058.05 ns
#0.0011 ps58.051 ns

Two details in the log are worth noticing. The $display right after each nonblocking assignment still shows the old values, because <= updates the variable at the end of the time step: at T=10, a is still 0. And b - a does not give -3: both operands are 4 bits wide, so the result wraps around to 4'b1101, which is 13.

Delays such as 10ps or 1ns, with the unit written after the number, are SystemVerilog time literals. In Verilog, a delay is always a plain number in the module's time unit, so 10 ps is written as 0.01 when the unit is 1 ns.

Synthesis result: Synthesis ignores # delays, so they have no effect on the hardware. Delays are used in testbenches to sequence stimulus and to model clocks, and in behavioral models of parts outside the chip. To make hardware wait, design a counter or a state machine that counts clock cycles.

Quick Check

If a and b were declared as reg [7:0] instead of reg [3:0], how long would #(b-a) wait, assuming the same values a = 4 and b = 1?

Show Answer

251 time units. With 8-bit operands, 1 - 4 wraps to 8'b11111101, which is 251 as an unsigned number. The width of the expression decides how large the "negative" delay becomes.

Event Control

A change in the value of a net or variable is an event, and an event control @( ) makes the block wait for one. Changes can also be qualified by direction:

EventTriggers on
@(sig)Any change in the value of sig
@(posedge sig)A change toward 1: 0 to 1, 0 to x or z, and x or z to 1
@(negedge sig)A change toward 0: 1 to 0, 1 to x or z, and x or z to 0

A few rules apply:

  • Only a change of value is an event. Assigning a signal the value it already has does not trigger anything.
  • For a vector, posedge and negedge look only at the least significant bit.
  • An event control can watch an expression. The expression is evaluated again whenever one of its operands changes, and an event occurs only if the result changes.

module tb;
  reg a, b;

  initial begin
    a <= 0;

    #10 a <= 1;
    #10 b <= 1;

    #10 a <= 0;
    #15 a <= 1;
  end

  // Start another procedural block that waits for an update to
  // signals made in the above procedural block

  initial begin
    @(posedge a);
    $display ("T=%0t Posedge of a detected for 0->1", $time);
    @(posedge b);
    $display ("T=%0t Posedge of b detected for X->1", $time);
  end

  initial begin
    @(posedge (a + b)) $display ("T=%0t Posedge of a+b", $time);

    @(a) $display ("T=%0t Change in a found", $time);
  end
endmodule
Output
ncsim> run
T=10 Posedge of a detected for 0->1
T=20 Posedge of b detected for X->1
T=30 Posedge of a+b
T=45 Change in a found
ncsim: *W,RNQUIE: Simulation is complete.

The second initial block sees a rise from 0 to 1 at T=10, and b rise from x to 1 at T=20, which also counts as a positive edge. The third block is more interesting. a + b is a 1-bit expression here, because both operands are 1 bit wide:

Timeaba + b (1 bit)Edge?
00xx
20110 (2 wraps to 0)x to 0 is a negedge
300110 to 1 is a posedge

So the first positive edge of a + b is at T=30, not T=20. The block then waits for any change of a, which next happens at T=45.

Synthesis result: always @(posedge clk) is how synthesizable code describes flip-flops, and always @(posedge clk or negedge rst_n) adds an asynchronous reset. Synthesis expects these edge events only at the top of an always block. Event controls in the middle of a block, or on expressions such as a + b, are for testbenches only.

Quick Check

A 4-bit signal cnt changes from 4'b0101 to 4'b0110. Does @(posedge cnt) trigger? Does @(cnt)?

Show Answer

@(posedge cnt) does not trigger, because only the least significant bit is checked, and it goes from 1 to 0, which is a negative edge. @(cnt) does trigger, because the value of the vector changed.

Named Events

A named event is declared with the event keyword. It holds no value and has no duration: it is simply a signal that one process can raise and other processes can wait for. It is triggered with the -> operator and waited on with @, which makes it a convenient way to synchronize processes in a testbench. Named events can also be declared as arrays.


event done;          // Declare
-> done;             // Trigger it in one process
@(done);             // Wait for it in another process

module tb;
  event a_event;
  event b_event[0:4];

  initial begin
    #20 -> a_event;

    #30;
    ->a_event;

    #50 ->a_event;
    #10 ->b_event[3];
  end

  always @ (a_event) $display ("T=%0t [always] a_event is triggered", $time);

  initial begin
    #25;
    @(a_event) $display ("T=%0t [initial] a_event is triggered", $time);

    #10 @(b_event[3]) $display ("T=%0t [initial] b_event is triggered", $time);
  end
endmodule
Output
ncsim> run
T=20 [always] a_event is triggered
T=50 [always] a_event is triggered
T=50 [initial] a_event is triggered
T=100 [always] a_event is triggered
T=110 [initial] b_event is triggered
ncsim: *W,RNQUIE: Simulation is complete.

The always block is waiting from the start, so it catches all three triggers of a_event. The second initial block is still inside its #25 when a_event fires at T=20, so it misses that trigger. An event is not stored: a process only sees it if it is already waiting at that moment. The block then catches the trigger at T=50, waits 10 more units, and from T=60 waits for b_event[3], which arrives at T=110.

Timeline of named events: a_event is triggered at T=20, 50 and 100 and b_event[3] at T=110; the always block waits on a_event the whole time and sees all three triggers; the initial block is busy in #25 at T=20 and misses that trigger, waits on a_event from T=25 and sees it at T=50, is busy in #10 until T=60, then waits on b_event[3] and sees it at T=110, so it does not see the T=100 trigger

Quick Check

If the second initial block started with #15 instead of #25, which lines would it print, and when?

Show Answer

It would be waiting by T=20, so it would print "a_event is triggered" at T=20. After #10 it would wait for b_event[3] from T=30, and print "b_event is triggered" at T=110, as before.

Testbench Synchronization

In a testbench, one block often generates stimulus while another checks results. The generator triggers an event such as -> packet_sent after each transaction, and the checker waits on @(packet_sent) before comparing the output. Named events keep the two blocks in step without shared flags, and without either block needing to know how long the other one takes.

Event or operator

To wait for any one of several events, list them separated by the or keyword. A comma , can be used instead of or and means the same thing. The list of events after @ is called the sensitivity list.


module tb;
  reg a, b;

  initial begin
    $monitor ("T=%0t a=%0d b=%0d", $time, a, b);
    {a, b} <= 0;

    #10 a <= 1;
    #5  b <= 1;
    #5  b <= 0;
  end

  // Use "or" between events
  always @ (posedge a or posedge b)
    $display ("T=%0t posedge of a or b found", $time);

  // Use a comma between
  always @ (posedge a, negedge b)
    $display ("T=%0t posedge of a or negedge of b found", $time);

  always @ (a, b)
    $display ("T=%0t Any change on a or b", $time);
endmodule
Output
ncsim> run
T=0 posedge of a or negedge of b found
T=0 Any change on a or b
T=0 a=0 b=0
T=10 posedge of a or b found
T=10 posedge of a or negedge of b found
T=10 Any change on a or b
T=10 a=1 b=0
T=15 posedge of a or b found
T=15 Any change on a or b
T=15 a=1 b=1
T=20 posedge of a or negedge of b found
T=20 Any change on a or b
T=20 a=1 b=0
ncsim: *W,RNQUIE: Simulation is complete.
Waveforms of a and b with the times each always block triggers: always @(posedge a or posedge b) triggers at T=10 and 15; always @(posedge a, negedge b) triggers at T=0, 10 and 20; always @(a, b) triggers at T=0, 10, 15 and 20

At T=0, both signals go from x to 0. For b, x to 0 is a negative edge, so the second block triggers. The third block triggers on every change. The $monitor line always comes last in each time step, because $monitor prints at the end of the time step, after all other activity.

Implicit Event Expression List

An always block that describes combinational logic must run again whenever any of its inputs changes. That means every signal read inside the block has to appear in the sensitivity list. Forgetting one is a very common cause of functional bugs in RTL.


module tb;
  reg a, b, c, d;
  reg x, y;

  // Event expr/sensitivity list is formed by all the
  // signals inside () after @ operator and in this case
  // it is a, b, c or d
  always @ (a, b, c, d) begin
    x = a | b;
    y = c ^ d;
  end

  initial begin
    $monitor ("T=%0t a=%0b b=%0b c=%0b d=%0b x=%0b y=%0b", $time, a, b, c, d, x, y);
    {a, b, c, d} <= 0;

    #10 {a, b, c, d} <= $random;
    #10 {a, b, c, d} <= $random;
    #10 {a, b, c, d} <= $random;
  end
endmodule
Output
ncsim> run
T=0 a=0 b=0 c=0 d=0 x=0 y=0
T=10 a=0 b=1 c=0 d=0 x=1 y=0
T=20 a=0 b=0 c=0 d=1 x=0 y=1
T=30 a=1 b=0 c=0 d=1 x=1 y=1
ncsim: *W,RNQUIE: Simulation is complete.

Now suppose a new input e and output z = ~e are added to the block. The sensitivity list must be updated by hand to include e, or z would not update when only e changes:


module tb;
  reg a, b, c, d, e;
  reg x, y, z;

  // Add "e" also into sensitivity list
  always @ (a, b, c, d, e) begin
    x = a | b;
    y = c ^ d;
    z = ~e;
  end

  initial begin
    $monitor ("T=%0t a=%0b b=%0b c=%0b d=%0b e=%0b x=%0b y=%0b z=%0b",
              $time, a, b, c, d, e, x, y, z);
    {a, b, c, d, e} <= 0;

    #10 {a, b, c, d, e} <= $random;
    #10 {a, b, c, d, e} <= $random;
    #10 {a, b, c, d, e} <= $random;
  end
endmodule
Output
ncsim> run
T=0 a=0 b=0 c=0 d=0 e=0 x=0 y=0 z=1
T=10 a=0 b=0 c=1 d=0 e=0 x=0 y=1 z=1
T=20 a=0 b=0 c=0 d=0 e=1 x=0 y=0 z=0
T=30 a=0 b=1 c=0 d=0 e=1 x=1 y=0 z=0
ncsim: *W,RNQUIE: Simulation is complete.

At T=20, only c and e change, and z correctly goes to 0. Without e in the list, this time step would still trigger the block because c changed, so the bug would hide here and only show up in a run where e changes on its own.

Verilog-2001 solves the problem with @*, also written @(*). It builds the sensitivity list automatically from every signal that is read inside the block:


module tb;
  reg a, b, c, d, e;
  reg x, y, z;

  // Use @* or @(*)
  always @ * begin
    x = a | b;
    y = c ^ d;
    z = ~e;
  end

  initial begin
    $monitor ("T=%0t a=%0b b=%0b c=%0b d=%0b e=%0b x=%0b y=%0b z=%0b",
              $time, a, b, c, d, e, x, y, z);
    {a, b, c, d, e} <= 0;

    #10 {a, b, c, d, e} <= $random;
    #10 {a, b, c, d, e} <= $random;
    #10 {a, b, c, d, e} <= $random;
  end
endmodule
Output
ncsim> run
T=0 a=0 b=0 c=0 d=0 e=0 x=0 y=0 z=1
T=10 a=0 b=0 c=1 d=0 e=0 x=0 y=1 z=1
T=20 a=0 b=0 c=0 d=0 e=1 x=0 y=0 z=0
T=30 a=0 b=1 c=0 d=0 e=1 x=1 y=0 z=0
ncsim: *W,RNQUIE: Simulation is complete.

The log is identical to the version with the hand-written list, but the code no longer needs to be edited when an input is added.

Synthesis result: always @* with blocking assignments synthesizes to pure combinational logic: here an OR gate for x, an XOR gate for y and an inverter for z. Synthesis tools build the same logic even when the sensitivity list is incomplete, usually with a warning. The simulation of the RTL then behaves differently from the synthesized netlist, which is called a simulation-synthesis mismatch.

Lint Checks in RTL Sign-Off

Before a design is handed to synthesis, its RTL goes through lint tools that check coding rules. Incomplete sensitivity lists are one of the standard checks, because the resulting mismatch only shows up in gate-level simulation or on silicon. Many design teams simply require always @* for all combinational blocks so that the problem cannot occur.

Level Sensitive Event Control

The wait statement delays a block until a condition is true. If the condition is already true when the block reaches it, the block continues immediately; otherwise it waits until the condition becomes true. This is called level-sensitive control, because it checks the current level of the condition rather than waiting for a change.


wait (ready);              // Continue when ready is 1
wait (count == 10) go = 1; // The statement runs once count is 10

The example below runs a 4-bit counter on a clock with a period of 20 time units, and uses both wait and @ to watch it:


module tb;
  reg [3:0] ctr;
  reg clk;

  initial begin
    {ctr, clk} <= 0;

    wait (ctr);
    $display ("T=%0t Counter reached non-zero value 0x%0h", $time, ctr);

    wait (ctr == 4) $display ("T=%0t Counter reached 0x%0h", $time, ctr);

    @(ctr);
    $display ("T=%0t Counter reached 0x%0h", $time, ctr);

    @(ctr == 9);
    $display ("T=%0t Counter reached 0x%0h", $time, ctr);

    $finish;
  end

  always #10 clk = ~clk;

  always @ (posedge clk)
    ctr <= ctr + 1;

endmodule
Output
xcelium> run
T=10 Counter reached non-zero value 0x1
T=70 Counter reached 0x4
T=90 Counter reached 0x5
T=170 Counter reached 0x9
Simulation complete via $finish(1) at time 170 NS + 1
./testbench.sv:19     $finish;

The counter increments on every rising clock edge, at T=10, 30, 50 and so on. Each control waits in a different way:

  • wait (ctr) continues once ctr is non-zero, at T=10.
  • wait (ctr == 4) continues when the condition becomes true, at T=70.
  • @(ctr) waits for the next change of ctr, which is the increment to 5 at T=90.
  • @(ctr == 9) waits for the result of the comparison to change. It changes from 0 to 1 when ctr becomes 9, at T=170.

Synthesis result: wait and named events are not synthesizable. They describe how a testbench or a behavioral model waits, not hardware. In RTL, waiting for a condition is done with a state machine that stays in a state until the condition is true on a clock edge.

Quick Check

Suppose ctr is already 9 when a block reaches @(ctr == 9). When does the block continue? What if it reached wait (ctr == 9) instead?

Show Answer

With @(ctr == 9), the block waits for the comparison to change, which happens when ctr increments to 10 and the result goes from 1 to 0. That is probably not what was intended. wait (ctr == 9) continues immediately, because the condition is already true.

Common Beginner Mistakes

Mistake #1: Leaving signals out of a combinational sensitivity list

Wrong: always @(a, b) y = a & b & c;
Right: always @* y = a & b & c;

Why? In simulation, y does not update when only c changes, so the block behaves like it remembers an old value. Synthesis ignores the list and builds a plain AND gate, so the netlist and the RTL simulation disagree.

Mistake #2: Triggering a named event before anyone is waiting

Wrong: Triggering -> start at T=0 in one block while another block executes @(start) at the same time step, or later

Right: Make sure the waiting process reaches @(start) before the event is triggered, for example by adding a small delay before the trigger, or use a flag variable with wait (flag)

Why? An event is not stored. If the trigger happens before the other process is waiting, as at T=20 in the named event example, the trigger is lost and the waiting block may hang forever. At the same time step, the order of the two blocks is not defined, so the result can even differ between simulators.

Mistake #3: Using @ where wait is needed

Wrong: @(ready == 1); to continue when ready is 1
Right: wait (ready == 1);

Why? @ waits for a change. If ready is already 1, @(ready == 1) waits until it changes again, which might be when it goes back to 0. wait checks the current level and continues at once if the condition is already true.

Mistake #4: Forgetting the width of a delay expression

Wrong: #(b - a) with 4-bit a = 4 and b = 1, expecting either an error or a delay of 0
Right: Make sure delay expressions can never be negative, for example #((b > a) ? b - a : 0)

Why? The subtraction is done in 4 bits, so the result wraps to 13, and the simulation waits 13 time units without any warning. With 32-bit integers, the same mistake gives a delay of more than four billion time units.

Mistake #5: Expecting delays in RTL to reach the hardware

Wrong: always @(posedge clk) #2 q <= d; to model a 2 ns clock-to-output delay in hardware
Right: Leave delays out of synthesizable RTL; timing comes from the library cells and is checked with static timing analysis

Why? Synthesis ignores the #2. The RTL simulation shows a delay that the netlist does not have, which can hide or create race conditions that do not exist in the real circuit.

Practice Exercise

Challenge: Request and Acknowledge Handshake

Write a testbench with three initial blocks and the variables req and ack, both starting at 0:

  • A producer sets req = 1 at T=5, waits for a rising edge on ack, prints a message, clears req and triggers a named event done.
  • A consumer waits until req is 1, waits 3 more time units, sets ack = 1, waits until req is 0 again and then clears ack.
  • A watcher waits for done and prints a message.

Predict the output before you run it.

Hint

Use wait for the level conditions on req, @(posedge ack) for the edge, and -> done with @(done) for the event. The watcher starts waiting at T=0, so it cannot miss the event.

Solution

module tb;
  reg req, ack;
  event done;

  initial begin                     // Producer
    req = 0; ack = 0;
    #5 req = 1;
    @(posedge ack);
    $display ("T=%0t ack received", $time);
    req = 0;
    -> done;
  end

  initial begin                     // Consumer
    wait (req);
    #3 ack = 1;
    wait (!req);
    ack = 0;
  end

  initial begin                     // Watcher
    @(done) $display ("T=%0t transaction done", $time);
  end
endmodule

Expected output:

T=8 ack received
T=8 transaction done

The consumer sees req = 1 at T=5 and raises ack 3 units later, at T=8. The producer wakes up on that edge, prints its message, clears req and triggers done, all at T=8. The watcher has been waiting since T=0, so it prints next, and the consumer then sees req = 0 and clears ack.

Summary

In this tutorial, you learned:

  • #delay waits a fixed time in the module's time unit; x and z delays count as 0, and negative results wrap to large unsigned values
  • @(sig) waits for any change, while @(posedge sig) and @(negedge sig) wait for changes toward 1 or 0, checking only the LSB of a vector
  • An event on an expression occurs only when the expression's result changes
  • Named events are triggered with -> and waited on with @, and are lost if no process is waiting
  • or and commas combine events in a sensitivity list, and @* builds a complete list for combinational logic
  • wait (cond) is level sensitive and continues at once if the condition is already true
  • Only always @(posedge clk) style edge events and @* are used in synthesizable RTL; delays, wait and named events are for testbenches
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5