Scheduling regions and nonblocking updates

Follow what a simulator does inside one time step, see why nonblocking assignments make clocked logic predictable, and recognise a race when you write one.

Lesson
6.2
Time
30 min
Runs on
Your browser
Check
5 questions

A testbench drives the DUT input with din = next; immediately after @(posedge clk). On one simulator the DUT captures the old value at that edge. After an unrelated file is added to the compile list, it captures the new one, and every expected value is off by a cycle. Neither simulator is wrong: the standard does not say which of two processes woken by the same edge runs first.

A blocking write at the edge where the DUT samples
0123clk edge1234din (blocking at edge)11223344q (DUT)old or 1111 or 2222 or 3333 or 44din (NBA at edge)11223344q with NBA driveold112233order unspecified

With a blocking write, q depends on which process runs first. With a nonblocking write, the DUT always sees the value from before the edge.

One time step has several phases

Simulation time advances in time steps, one for each moment at which something happens. Within a time step, the standard sorts pending work into regions and runs them in a fixed order, going back to earlier regions whenever later work creates new events.

  1. Preponed

    Values are sampled before anything changes in this time step. Clocking-block inputs with #1step skew and concurrent assertions read these values.

  2. Active

    Processes woken in this time step run: blocking assignments, continuous assignments, $display. Nonblocking assignments evaluate their right-hand side here, but do not update yet.

  3. Inactive

    Processes delayed by #0 resume here, then the scheduler returns to Active if that created new work.

  4. NBA

    All pending nonblocking updates are applied, in the order they were scheduled. Processes waiting on those signals wake, and the scheduler loops back to Active.

  5. Observed

    Concurrent assertions evaluate, using the Preponed samples. Clocking-block events trigger here, after the design has settled.

  6. Reactive

    Code in program blocks and the action blocks of concurrent assertions run; they can create new Active work in the same time step.

  7. Postponed

    Read-only: $strobe and $monitor print the final values of the time step. Nothing may change any more.

The full list also has Re-Inactive and Re-NBA regions, which mirror Inactive and NBA for testbench code. Clocking-block drives with #0 skew land in Re-NBA, as lesson 6.3 shows.

Nonblocking assignments split read from write

A nonblocking assignment, q <= d;, reads d in the Active region and writes q later, in the NBA region. Every process woken by the same clock edge therefore reads the values from before the edge, whatever order the processes run in.

That is what makes a pipeline written with <= behave like flip-flops. With a1 <= d; a2 <= a1;, stage 2 receives the old value of stage 1, so data moves one stage per edge.

A blocking assignment, q = d;, writes immediately. Inside one process the order of statements is fixed, so b1 = d; b2 = b1; always copies the new d into both stages: the pipeline collapses into a single register, every time.

Where the order is left open

The standard fixes the order of statements inside one begin-end block, and the order of the regions. It does not fix the order in which separate processes woken in the same region run.

Suppose one process writes a signal with a blocking assignment at a clock edge, and another process reads it at the same edge. The reader may see either the old or the new value. That is a race. Different simulators, or the same simulator after a harmless-looking edit, may choose differently.

A race is a bug in the code, not a property of a simulator. Never test which value "wins"; remove the race instead.

Blocking write at the edge

@(posedge clk);
din = next;          // DUT reads din
                     // at this same edge

The DUT may capture the old or the new value; the standard does not say which.

Nonblocking write at the edge

@(posedge clk);
din <= next;         // updated in the
                     // NBA region

Every Active-region reader at this edge sees the old value; the DUT captures next at the following edge.

Three ways to keep a testbench race-free

Write clocked state with nonblocking assignments, in the DUT and in testbench models alike. Then everything that reads at the edge sees the values from before it.

Change testbench stimulus away from the sampling edge, for example at the falling edge, as lesson 6.1 did. Or drive and sample through a clocking block, which places both at fixed points in the time step; that is the next lesson.

Avoid #0 and small #1 delays as fixes. They move work into another region or time step, but the result then depends on details that the next edit can change.

Seeing the regions in output

$display prints in the Active region, so right after @(posedge clk) it shows the values from before the edge's nonblocking updates. $strobe waits until the Postponed region and shows the values at the end of the time step.

Printing the same register both ways at one edge is a quick way to see whether it is updated by a nonblocking assignment. In the example below, $display shows 00 and $strobe shows 11 for the same signal at the same time.

Try it

At t=5, $display ran in the Active region and saw the pipeline before its nonblocking updates, so both stages read 00. $strobe printed in the Postponed region, after the updates.

The nonblocking pipeline moves data one stage per edge, while the blocking one copies each new value into both stages. Swap the two lines inside the blocking always block, so it reads b2 = b1; b1 = d;, and predict the new b values.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: t=5 Active: a1=00 a2=00 t=5 Postponed: a1=11 a2=00 | b1=11 b2=11 t=15 Postponed: a1=22 a2=11 | b1=22 b2=22 t=25 Postponed: a1=33 a2=22 | b1=33 b2=33

Check yourself

Predict the outputPredict the exact output.
module tb;
  logic clk = 0; logic q = 0, r = 0;
  always #5 clk = ~clk;
  always @(posedge clk) begin q <= 1; r <= q; end
  initial begin @(posedge clk); $strobe("%0d %0d", q, r); #1 $finish; end
endmodule
ConceptWhen is the right-hand side of q <= d; evaluated?
ConceptTwo always @(posedge clk) blocks run at the same edge. One does a = 1;, the other does b = a;. What does the standard say b becomes?
Read the waveformThe pipeline is a1 <= d; a2 <= a1;. What does a2 hold just after edge 3?
Inputs at each rising edge
0123edge1234d before the edge11223344

Both stages start at 00.

Spot the bugEach option is the body of always @(posedge clk) for a two-stage pipeline, where s2 must hold the value s1 held before the edge. Which option collapses the pipeline?

Remember

  • A time step runs its regions in order: Preponed, Active, Inactive, NBA, Observed, Reactive, Postponed.
  • A nonblocking assignment reads in Active and writes in NBA, so every reader at an edge sees pre-edge values.
  • Statements inside one block run in order; separate processes in the same region run in an unspecified order.
  • A blocking write and a read of the same signal at the same edge, in different processes, is a race.
  • $display shows Active-region values; $strobe shows the final values of the time step.

Practice