A Verilog design and its testbench contain many always and initial blocks, continuous assignments and procedural statements, all of which become active at different times during the simulation. A simulator runs on a single processor, so it needs precise rules for which of these runs first. Those rules are the scheduling semantics of Verilog, and they explain why a nonblocking assignment, a $display and a $strobe in the same time step can see different values.

12 min read | Beginner Level

What You'll Learn

  • The difference between update events and evaluation events
  • How the event queue divides every time step into the Active, Inactive, NBA and Monitor regions
  • How the simulator processes one time step and then advances time
  • What makes simulation results nondeterministic, and how coding rules avoid race conditions

Event Types

The simulator works with two kinds of events:

  • An update event is a change in the value of a net or variable.
  • An evaluation event is the execution of a process, such as an always block or a continuous assignment, in response to an update event it is sensitive to.

module tb;
  reg a, b, c;
  wire d;

  // 'always' is a process that gets evaluated when either 'a' or 'b' is updated.
  // When 'a' or 'b' changes in value it is called an 'update event'. When 'always'
  // block is triggered because of a change in 'a' or 'b' it is called an evaluation
  // event
  always @ (a or b) begin
    c = a & b;
  end

  // Here 'assign' is a process which is evaluated when either 'a' or 'b' or 'c'
  // gets updated
  assign d = a | b ^ c;
endmodule

One update event can start a whole chain. When a changes, both the always block and the assign are evaluated. If the always block then changes c, that is a new update event, and because c is also an input of the assign, the assign is evaluated again. The chain stops when no evaluation produces a new value.

Chain of events when a changes from 0 to 1: the update event on a causes two evaluation events, the always block computing c = a & b and the continuous assignment of d; the always block produces an update event on c, which is also an input of the assign and causes it to be evaluated again; the assign produces an update event on d. The order in which the always block and the assign run is not defined

Because events are created at different simulation times, the simulator keeps them in an event queue, sorted by time, so that they are executed in the correct order.

Quick Check

In the module above, b is 0 when a changes from 0 to 1. Does the always block cause a second evaluation of the assign?

Show Answer

No. The always block is evaluated, but c = a & b is still 0, so c does not change, there is no update event on c, and the assign is only evaluated once, because of the change on a.

Event Queue

All events that belong to the same simulation time form a time slot. Within a time slot, the event queue is divided into four regions. Events can be added to any region, but they are only executed from the Active region. When the Active region is empty, events are moved into it from the next region in line; when all regions are empty, simulation time advances to the next time slot.

Active Region

The Active region holds the events that are being processed at the current simulation time. This includes evaluating continuous assignments, executing blocking assignments, running $display, and evaluating the right-hand side of nonblocking assignments. Events in the Active region can be processed in any order, and the simulator is free to choose that order.

Inactive Region

The Inactive region holds events for the current time that must wait until all Active events are done. A statement after an explicit zero delay, #0, is scheduled here. When the Active region is empty, all Inactive events are moved to the Active region.

Nonblocking Assignment (NBA) Region

A nonblocking assignment q <= d; evaluates d in the Active region, but the update of q is scheduled in the NBA region. It is applied only after all Active and Inactive events of the current time are done. Any process woken up by these updates puts new events into the Active region, and the cycle starts again.

Monitor Region

The Monitor region runs $monitor and $strobe. It is processed last, after all other regions of the time slot are empty, so these tasks always print the final values of that time.

RegionContainsProcessed
ActiveBlocking assignments, continuous assignments, $display, right-hand sides of <=First, in any order
InactiveStatements after #0When Active is empty
NBAUpdates of nonblocking assignmentsWhen Active and Inactive are empty
Monitor$monitor, $strobeLast, when all others are empty

Synthesis result: None. The event queue is a model of how a simulator executes code; hardware has no event regions. The scheduling rules matter for synthesis only indirectly: code that follows them, with nonblocking assignments in clocked blocks, simulates the same way the synthesized flip-flops behave.

Example


module tb;
  reg x, y, z;

  initial begin
    #1  x = 1;
        y = 1;
    #1  z = 0;
  end
endmodule

The simulation starts at time 0, where the initial block begins and immediately reaches #1. The rest of the block is scheduled for a future time. At time 1, x and y are assigned 1 in the Active region, and the next #1 schedules the last statement for time 2, where z is assigned 0. Before their first assignment, all three variables are x.

Three time slots for the example, each with Active, Inactive, NBA and Monitor regions: at T = 0 the initial block starts and waits with #1; at T = 1 the Active region executes x = 1 and y = 1 and waits again with #1; at T = 2 the Active region executes z = 0 and the block ends; all other regions are empty

Event Processing Flow

Within one time slot, the simulator repeats these steps:

  1. Execute all events in the Active region, in any order. New events created while doing so are added to the matching region.
  2. When the Active region is empty, move all Inactive events to the Active region and go back to step 1.
  3. When Active and Inactive are both empty, move all NBA events to the Active region, apply the updates, and go back to step 1, because the updates can wake up other processes.
  4. When Active, Inactive and NBA are all empty, run the Monitor events.
  5. Advance simulation time to the next time slot that has events.
Flow of one simulation time step: events for time T start in the Active region, where blocking assignments, continuous assignments and $display run and nonblocking right-hand sides are evaluated; when Active is empty, statements after #0 in the Inactive region run; when both are empty, the Nonblocking region updates the left-hand sides of nonblocking assignments, and any processes woken by these updates go back to Active; when everything is updated, the Monitor region prints $monitor and $strobe; then time advances to the next Future events, which become Active

The code below shows the regions at work in a single time step:


module regions;
  reg [3:0] a;

  initial begin
    a = 1;
    a <= 2;                              // Update scheduled in the NBA region
    $display ("display : a=%0d", a);     // Active region
    #0 $display ("after #0: a=%0d", a);  // Inactive region
    $strobe ("strobe  : a=%0d", a);      // Monitor region
  end
endmodule
display : a=1
after #0: a=1
strobe  : a=2

The nonblocking update to 2 waits in the NBA region. The first $display runs in the Active region and prints 1. The #0 moves the rest of the block to the Inactive region, which still comes before the NBA region, so the second $display also prints 1. $strobe is called at the same time, but only prints in the Monitor region, after the update, so it shows 2.

Quick Check

If a <= 2; were replaced by a = 2;, what would the three lines print?

Show Answer

All three would print 2. A blocking assignment updates a immediately in the Active region, before the first $display runs.

Why Clocked RTL Uses Nonblocking Assignments

In a real chip, every flip-flop on a clock samples its input at the same instant, and only then do all outputs change. The NBA region recreates exactly that: at a clock edge, every always @(posedge clk) block reads its inputs in the Active region, and all registers are updated together afterwards in the NBA region. This is why shift registers, pipelines and state machines written with <= simulate the same way as the flip-flops that synthesis builds from them.

What makes simulation nondeterministic ?

Because the events in the Active region can be processed in any order, the same design and testbench can give different results on different simulators, or even after a small change to the code. This happens when two processes at the same time read and write the same variable, which is called a race condition.


module race;
  reg clk = 0;
  reg a = 0, b = 0;

  always #5 clk = ~clk;

  always @(posedge clk) a = 1;    // Block 1
  always @(posedge clk) b = a;    // Block 2: reads a at the same time
endmodule

At the first rising edge of clk, both blocks are woken up in the Active region. If Block 1 runs first, b gets the new value 1; if Block 2 runs first, b gets the old value 0. Both orders are legal, so the result is not defined by the language.

Two always blocks on the same clock edge, a = 1 and b = a, with a starting at 0: if the first block runs first, b becomes 1; if the second block runs first, b becomes 0; with nonblocking assignments a <= 1 and b <= a, both read the old value a = 0 and the updates happen in the NBA region, so b is 0 on every simulator

Writing both blocks with nonblocking assignments removes the race. Both read a in the Active region, where it is still 0, and both updates happen later in the NBA region, so b is 0 after the first edge and 1 after the second, on every simulator. Other sources of nondeterminism are statements in different processes that write the same variable at the same time, and testbenches that change inputs at exactly the clock edge with blocking assignments.

Simulator Upgrades That Change Results

A design with a hidden race can pass for years and then fail after a simulator upgrade or when optimizations are turned on, simply because the new version processes Active events in a different order. Verification teams therefore follow strict coding rules: nonblocking assignments in clocked always blocks, blocking assignments in combinational blocks, and testbench stimulus driven away from the active clock edge. Lint tools check these rules automatically.

Common Beginner Mistakes

Mistake #1: Blocking assignments in clocked always blocks

Wrong: always @(posedge clk) q1 = d; always @(posedge clk) q2 = q1;
Right: always @(posedge clk) q1 <= d; always @(posedge clk) q2 <= q1;

Why? With blocking assignments, q2 may receive the new or the old value of q1, depending on which block runs first. With nonblocking assignments, it always receives the old value, which is how two flip-flops in a row behave.

Mistake #2: Using #0 to fix a race

Wrong: Adding #0 before a statement so that it runs "after" the other block
Right: Fix the cause, normally by using nonblocking assignments

Why? #0 only moves the statement to the Inactive region. If the other process also uses #0, or updates its value through a nonblocking assignment, the race comes back. Code full of #0 is hard to understand and fragile.

Mistake #3: Printing with $display to check nonblocking results

Wrong: q <= d; $display("q=%b", q);, expecting the new value
Right: Use $strobe or $monitor, which run in the Monitor region

Why? $display runs in the Active region, before the NBA region has applied the update.

Mistake #4: Assuming processes run in the order they are written

Wrong: Expecting the first always block in the file to run before the second at the same time
Right: Write code whose result does not depend on the order of processes

Why? Only statements within one begin ... end block are guaranteed to run in order. The order of different processes in the Active region is up to the simulator, and it may change between versions or with different options.

Practice Exercise

Challenge: Trace the Regions

For the code below, list the order in which the four print statements produce output at time 10, and the value each one prints.


module tb;
  reg [3:0] v;

  initial begin
    v = 0;
    #10;
    v <= 5;
    $strobe  ("A: v=%0d", v);
    $display ("B: v=%0d", v);
    v = 3;
    #0 $display ("C: v=%0d", v);
  end

  initial #10 $display ("D: v=%0d", v);
endmodule
Hint

Place each statement in its region. Remember that the two initial blocks wake up at time 10 in an undefined order, and that the NBA update overwrites any blocking assignment made earlier in the same time step.

Solution
B: v=0
C: v=3
A: v=5

Line D is printed in the Active region at time 10, either before B (showing 0) or after the blocking assignment v = 3 (showing 3), depending on which initial block the simulator runs first, so its position and value are a race. B runs in the Active region before v = 3 and prints 0. C runs in the Inactive region, after v = 3 but before the NBA update, and prints 3. A, from $strobe, prints in the Monitor region after the NBA region has set v to 5.

Summary

In this tutorial, you learned:

  • An update event is a change of value, and an evaluation event is a process that runs because of it
  • Events are kept in an event queue sorted by time, and each time slot has an Active, Inactive, NBA and Monitor region
  • Blocking assignments and $display run in the Active region, #0 statements in the Inactive region, nonblocking updates in the NBA region, and $strobe and $monitor in the Monitor region
  • Time advances only when all regions of the current time slot are empty
  • Active events can run in any order, so two processes reading and writing the same variable at the same time create a race
  • Nonblocking assignments in clocked blocks remove these races and match the behavior of real flip-flops
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5