The previous article explained how the event queue drives simulation forward. But within a single timestep, there is a finer structure that determines the order in which different pieces of code evaluate. Get this wrong in a testbench and you will drive signals that the RTL cannot reliably read — creating race conditions that produce intermittent, seed-dependent failures. This article explains the scheduling regions that govern evaluation order and shows exactly where testbench race conditions come from.

10 min read  |  Intermediate Level

What You'll Learn

  • The five SystemVerilog scheduling regions and what fires in each
  • How blocking and non-blocking assignments map to different regions
  • Why reading a signal written in the same timestep can be non-deterministic
  • How testbench race conditions arise and how to eliminate them

The Scheduling Regions

Within a single simulation timestep, the simulator does not execute all code in one flat pass. The IEEE 1800 SystemVerilog standard defines a set of scheduling regions — ordered slots that determine when different types of code run relative to each other. Understanding these regions is the key to understanding why race conditions exist and how to avoid them.

The five regions, in order, are:

  • Active — the main evaluation region. Blocking assignments (=), the right-hand side evaluation of non-blocking assignments, continuous assign statements, and $display calls all fire here. Critically, the ordering of events within the Active region is non-deterministic — the simulator may process them in any order.
  • NBA (Non-Blocking Assignment update) — after all Active-region work settles, the left-hand side updates of all non-blocking assignments (<=) are committed together. This is what makes <= race-free for register updates.
  • Observed — clocking block inputs are sampled here, and concurrent assertion expressions are evaluated. Because this fires after NBA, it sees fully settled register values.
  • Reactive — clocking block output drives and program block code fire here. Testbench code that drives the DUT through a clocking block runs in Reactive, safely after RTL has finished updating.
  • Postponed — $strobe and $monitor fire here, at the very end of the timestep. All signals are fully settled, making this the correct region to read final signal values for logging.

The Active and NBA regions may iterate with delta cycles (as covered in article 4.1) before the simulator moves to Observed. The Observed, Reactive, and Postponed regions do not generate new delta cycles.

Blocking vs Non-Blocking Assignments

The distinction between blocking and non-blocking assignments is directly tied to which scheduling region each uses, which determines whether sequential logic is race-free.

A blocking assignment (=) evaluates the right-hand side and immediately updates the left-hand side, all within the Active region. The next statement in the same always block sees the updated value straight away. This makes blocking assignments appropriate for combinational logic and within-block sequential computations, but dangerous in clocked sequential logic when multiple blocks interact.


// Blocking assignment — immediate update in Active region
always @(*) begin
    temp = a & b;   // temp updates immediately
    y    = temp | c; // sees the new temp value on the next line
end

A non-blocking assignment (<=) splits into two steps: the right-hand side is evaluated in the Active region, but the left-hand side update is deferred to the NBA region. Every non-blocking assignment in a given timestep evaluates its RHS first, then all the LHS updates are committed together after Active work is done.


// Non-blocking assignments — RHS evaluated in Active, LHS written in NBA
always @(posedge clk) begin
    q1 <= d;    // RHS (d) captured now; q1 written later in NBA
    q2 <= q1;   // RHS (q1) captured now — still the OLD value of q1
end

In the example above, both RHS evaluations happen before either LHS is written. So q2 captures the old value of q1, not the new one. This is exactly the behaviour you want for a shift register — and it is only possible because non-blocking assignments defer their writes to NBA.

Use <= in all clocked always @(posedge clk) blocks. Use = in combinational always @(*) blocks. Mixing them in a clocked block is a common source of simulation-synthesis mismatch.

Quick Check

Q: Two always @(posedge clk) blocks both use blocking assignments (=) and write to the same signal x. What problem does this create?

Show Answer

Both blocks fire in the Active region. Since event ordering within Active is non-deterministic, whichever block happens to run last wins. The final value of x can vary between simulator runs or between seeds — a classic race condition. Non-blocking assignments avoid this by deferring all writes to NBA, where the last scheduled write wins in a defined way, but the root fix is not to have two drivers on the same signal.

Why Same-Timestep Reads Are Non-Deterministic

Within the Active region, the simulator has no guaranteed ordering between independent always blocks. Consider two blocks that both fire at the same simulation time:


// Block A — writes x with a blocking assignment
always @(posedge clk)
    x = new_val;   // blocking: writes x immediately in Active

// Block B — reads x in the same timestep
always @(posedge clk)
    y = x + 1;    // which value of x does this see?

Whether Block B sees the old or new value of x depends entirely on which block the simulator schedules first — and that order is not guaranteed by the standard. Different simulators, or even different simulation runs, may produce different results. Replacing the blocking assignment in Block A with x <= new_val fixes the problem: Block B always evaluates in Active (seeing the old x), and the new x is only committed in NBA after Active work is complete.

Testbench Race Conditions

The most common race condition in verification arises when a testbench drives a DUT input at the exact same simulation time as the clock edge the DUT samples on.

Both the RTL always @(posedge clk) block and the testbench stimulus code fire in the Active region at the same timestep. Their relative ordering is non-deterministic. If the testbench update happens to be processed first, the DUT samples the new value. If the RTL block runs first, the DUT samples the old value. The result changes depending on the simulator, the run, or even the elaboration order — which makes this class of bug notoriously hard to reproduce and debug.

Driving a DUT input at the exact clock edge from a plain always or initial block is a race condition. The RTL's always @(posedge clk) and the testbench drive compete in the Active region with non-deterministic ordering.

The Fix: Clocking Blocks

SystemVerilog's clocking block is the correct tool for eliminating testbench race conditions. A clocking block declares an explicit relationship between testbench signals and a clock, and uses the scheduling regions to enforce safe timing.


// Clocking block definition — typically in an interface
clocking cb @(posedge clk);
    default input  #1step;  // sample inputs 1 step before the edge
    default output #1;      // drive outputs 1ns after the edge
    input  dout;            // signal sampled from DUT
    output din;             // signal driven to DUT
endclocking

Two things happen automatically. Inputs declared in the clocking block are sampled in the Observed region — after NBA, so they see fully settled register values. Outputs are driven in the Reactive region — after RTL evaluation is complete for that timestep. Testbench code driving cb.din therefore never races with always @(posedge clk) in the DUT.


// Testbench driving through clocking block — race-free
initial begin
    @(posedge clk);         // wait for the edge
    cb.din <= 1'b1;         // drives in Reactive region, after RTL settled
end

If you are not yet using interfaces and clocking blocks, a simpler workaround is to drive signals slightly after the clock edge using a small delay: #1 din = 1'b1;. The #1 moves the drive to a future timestep where the clock edge has already been fully processed. This is less robust than a clocking block but eliminates the race in straightforward testbenches.

Where This Matters in Practice

Verification environments that do not use clocking blocks frequently produce results that differ between commercial simulators. A test passing on one tool and failing on another with identical RTL is a strong indicator of an Active-region race condition in the testbench. UVM-based environments largely avoid this because the UVM driver infrastructure uses clocking blocks or explicit post-edge delays as standard practice.

Quick Check

Q: A testbench uses $strobe to print a signal value. Another testbench block uses $display to print the same signal at the same timestep. Which one prints the final settled value?

Show Answer

$strobe prints the final settled value because it fires in the Postponed region — after all Active, NBA, Observed, and Reactive work is done. $display fires in the Active region and may print a mid-evaluation value if other blocks have not yet committed their updates in the same timestep.

Common Mistakes

Mistake 1: Using blocking assignments in clocked always blocks shared across multiple always blocks.

Wrong: Two always @(posedge clk) blocks both write to signal x using =.
Right: Use <= in all clocked blocks, and avoid multiple drivers on the same signal entirely.

Why? Blocking assignments write immediately in the Active region. With multiple blocks firing at the same time, their ordering is non-deterministic, so the final value of x is unpredictable and simulator-dependent.

Mistake 2: Driving DUT inputs from a testbench at the exact clock edge without a delay or clocking block.

Wrong: @(posedge clk); din = 1'b1; — the drive happens at the same timestep as the posedge.
Right: @(posedge clk); #1 din = 1'b1; or use a clocking block with a declared output skew.

Why? The testbench drive and the RTL's always @(posedge clk) both activate in the Active region at the same timestep. The simulator's choice of which runs first determines what the DUT samples — making the result non-deterministic across simulators and runs.

Mistake 3: Using $display to read a signal that is updated by a non-blocking assignment in the same timestep.

Wrong: $display("q = %b", q); inside the same always @(posedge clk) block that drives q <= d.
Right: Use $strobe("q = %b", q); to read the settled value, or move the display to the Postponed region.

Why? $display runs in the Active region and will print the old value of q — the NBA update has not yet occurred. $strobe runs in Postponed, after all assignments have committed, and prints the correct final value.

Practice Exercise

Challenge: Identify the Race

The following testbench snippet has a race condition. Identify which line causes it and rewrite it to be race-free.


// Testbench (no clocking block used)
initial begin
    clk = 0;
    forever #5 clk = ~clk; // 100MHz clock
end

initial begin
    din = 0;
    repeat(5) @(posedge clk);
    din = 1'b1;   // drive new value
    @(posedge clk);
    $display("dout = %b", dout); // check output
end
Hint

Look at when din is driven relative to the clock edge. Which scheduling region does @(posedge clk); din = 1'b1; place the drive in?

Solution

The race is on din = 1'b1. After @(posedge clk) unblocks, execution resumes immediately in the same timestep as the posedge — placing the drive in the Active region alongside the RTL's always @(posedge clk). The DUT may or may not see the new value depending on simulator ordering.

Two correct fixes:


// Fix 1: add a small post-edge delay
repeat(5) @(posedge clk);
#1 din = 1'b1;   // drive 1ns after the edge — different timestep

// Fix 2: use a clocking block (preferred)
repeat(5) @(posedge clk);
cb.din <= 1'b1;  // drives in Reactive region, after RTL has settled

Also replace $display with $strobe to ensure the output is read after all NBA updates have committed.

Summary

In this article, you learned:

  • The five scheduling regions — Active, NBA, Observed, Reactive, Postponed — define a strict order within each timestep that prevents some races while making others possible
  • Blocking assignments write immediately in Active; non-blocking assignments defer their LHS updates to NBA, making them safe for clocked register modelling
  • Code within the Active region has non-deterministic ordering between independent blocks — reading a signal written by another block in the same timestep is a race
  • Testbench race conditions arise from driving DUT inputs at the clock edge; clocking blocks or post-edge delays eliminate them by moving drives to the Reactive region or a future timestep
Did you find what you were looking for?

Quiz

No quiz questions available for this article.