$strobe shows something different from $display. This article explains that model from first principles.A simulation time step is not a single instant — it's an ordered sequence of scheduling regions, each with a specific purpose, that every event happening at that time step passes through in a defined order.
14 min read | Intermediate Level
What You'll Learn
- Why a single simulation time step is divided into an ordered sequence of regions, not a single instant
- What happens in each of the core regions: Preponed, Active, Inactive, NBA, Observed, Reactive, and Postponed
- Why a nonblocking assignment's update is deferred to a later region than where it's evaluated, and why that prevents a classic race condition
- How this model explains behavior already familiar from elsewhere: assertion sampling, program blocks, and $strobe vs $display
A Time Step Is Not a Single Instant
It's tempting to think of everything scheduled "at 10ns" as happening simultaneously, in no particular order. In reality, SystemVerilog defines an ordered sequence of regions within that same time step, and every event — a blocking assignment, a nonblocking assignment, a $display call, a concurrent assertion — is scheduled into a specific one of them:
The core regions, in order, are Preponed, Active, Inactive, NBA, Observed, Reactive, and Postponed. Design code (blocking and nonblocking assignments, $display) runs in the first four; verification-specific constructs (concurrent assertions, program blocks) run in Observed and Reactive. This separation is deliberate — it's what keeps testbench code from racing against the design it's checking.
Preponed: Sampling Before Anything Changes
Preponed runs once per time step, immediately after simulation time advances. Nothing is updated here — it's purely for sampling. This is exactly the "preponed region" referenced throughout this tutorial's Assertions chapter: the values a concurrent assertion uses are captured here, before any of that time step's own updates have had a chance to happen. That's why an assertion sees the value a signal had going into a clock edge, not whatever it might become during that same edge.
Active: Where Most Design and Testbench Code Runs
Active is where the bulk of ordinary execution happens: every blocking assignment executes here directly, continuous assignments and primitive evaluation happen here, and $display/$finish execute here. For a nonblocking assignment, only the right-hand side is evaluated in Active — the actual write is deferred to the NBA region, covered next.
Active can genuinely loop: if executing one event in this region triggers another event that also belongs in Active, the region processes it too, before moving on. This is why, for instance, a chain of combinational logic can fully settle within a single time step before anything advances.
Inactive: A Home for #0 Delays
Inactive holds anything explicitly delayed by #0 — a way to defer a blocking assignment slightly within the same time step. It's a legitimate part of the model, but relying on #0 to "fix" a timing problem is generally considered poor practice, since it tends to paper over a race rather than correctly modeling the intended behavior.
NBA: Where Nonblocking Assignments Actually Update
This is the region that makes nonblocking assignments safe for modeling sequential logic. The right-hand side of every nonblocking assignment is evaluated back in Active — using whatever values were current before this time step's updates — but the actual write to the left-hand side doesn't happen until NBA, a region that only runs after every Active-region read has already completed.
Here's exactly why that matters, using a simple 2-stage shift register:
// Correct: two independent flip-flops in series
always @ (posedge clk) begin
b <= a;
c <= b;
end
Both right-hand sides — a and b — are read in Active, using their values from before this edge. Both writes then apply together in NBA. Given a taking the values 1, 2, 3, 4 across four clock edges, this produces:
| After cycle | a | b | c |
|---|---|---|---|
| 1 | 1 | 0 | 0 |
| 2 | 2 | 1 | 0 |
| 3 | 3 | 2 | 1 |
| 4 | 4 | 3 | 2 |
c correctly lags a by two full cycles — exactly what two staged flip-flops should do. Now compare the same logic written with blocking assignments:
// Broken: collapses the intended 2-stage pipeline
always @ (posedge clk) begin
b = a;
c = b; // reads the value b was JUST assigned, not its old value
end
b = a updates b immediately, in place, in Active. c = b then executes right after, reading b's brand-new value — not the value it held before this edge. Using the exact same a sequence:
| After cycle | a | b | c |
|---|---|---|---|
| 1 | 1 | 1 | 1 |
| 2 | 2 | 2 | 2 |
| 3 | 3 | 3 | 3 |
| 4 | 4 | 4 | 4 |
b and c are now identical every single cycle — the intended 2-stage delay has collapsed into a single stage duplicated twice. This is the textbook reason sequential logic uses nonblocking assignments: NBA's deferred timing guarantees every statement in Active reads pre-edge values, exactly matching how real flip-flops update simultaneously in hardware.
Quick Check
Q: If a third stage, d <= c;, were added to the correct (nonblocking) version, how many cycles behind a would d lag?
Show Answer
Three cycles. Each additional staged nonblocking assignment adds exactly one more cycle of delay, since every stage's right-hand side is read using the previous cycle's already-settled value — a direct consequence of NBA deferring every update until after that time step's Active-region reads are done.
Observed: Where Concurrent Assertions Actually Evaluate
Observed runs once per time step, and it's specifically where a concurrent assertion's property expression is evaluated — using the values already captured back in Preponed. This two-step split (sample in Preponed, evaluate in Observed) is what SystemVerilog Concurrent Assertions and the rest of that chapter build on throughout.
Reactive: Home for Program Blocks and Assertion Actions
Reactive is where two specific things run: code inside a program block, and the pass/fail action block of a concurrent assertion (the $display that follows assert property(p)). This is exactly why SystemVerilog Program Block states that program blocks execute in the reactive region — it's a deliberate design choice, keeping testbench-side reactions separated from the design's own Active/Inactive/NBA activity for that same time step.
Postponed: Where $strobe and $monitor See the Truly Final Value
$display executes in Active, alongside everything else competing for that region. If two separate processes both react to the same event, there's no guarantee about which one's Active-region code runs first — so a $display reading a signal that another process is also about to update in that same time step might show a stale value, depending on execution order.
$strobe and $monitor avoid this entirely by running in Postponed — a region that only executes after every other region for that time step has fully completed. Whatever they print is guaranteed to be the final, settled value, regardless of which order Active-region processes happened to run in.
Quick Check
Q: If a testbench needs to log a signal's value and be certain it reflects everything that happened at that time step, is $display or $strobe the safer choice?
Show Answer
$strobe. Since it runs in Postponed, after every other region has settled, it removes any dependency on Active-region execution order between multiple competing processes — $display offers no such guarantee.
Where Does This Matter in Practice?
This model is the reason for several conventions taken for granted elsewhere in this tutorial: nonblocking assignments for sequential logic, blocking for combinational; concurrent assertions sampling in Preponed and evaluating in Observed; program blocks running in Reactive specifically to avoid racing the design; and $strobe/$monitor existing as separate system tasks from $display at all.
Common Beginner Mistakes
Mistake #1: Using blocking assignments for sequential logic
As the shift register example shows, this can silently collapse a multi-stage pipeline into duplicated single-stage behavior. Why? A blocking assignment's write takes effect immediately, in place, so a later statement in the same block can end up reading a value that was never supposed to be visible until the next clock edge.
Mistake #2: Assuming $display always shows the fully settled value
$display runs in Active, where multiple processes can be competing to update the same signal in an order that isn't fully deterministic. Why? If a check specifically depends on seeing a time step's truly final value, $strobe or $monitor removes that ambiguity; $display does not.
Mistake #3: Overusing #0 delays to work around a race
Reaching for #0 can appear to fix a timing issue without actually addressing its cause. Why? It's generally better to model the real relationship correctly — with nonblocking assignments, or with a program block for testbench code — than to paper over a race with an extra zero-delay.
Mistake #4: Assuming every region runs exactly once per time step
Active, Inactive, and NBA can genuinely loop, re-triggering each other if new events keep landing back in an earlier region. Why? A single time step can involve several passes through this cluster before settling — it isn't always one straight pass through the full sequence of regions.
Practice Exercise
Challenge: Predict a Broken Swap
A common way to swap two registers is a <= b; b <= a; using nonblocking assignments. If this were instead written with blocking assignments (a = b; b = a;), starting with a=5, b=9, what would a and b hold after one clock edge, using each version?
Hint
For the nonblocking version, both right-hand sides are read using their pre-edge values before either write happens. For the blocking version, the first assignment's write is visible to the second statement immediately.
Solution
Nonblocking: a becomes 9, b becomes 5 — a genuine swap, since both right-hand sides (b=9 and a=5) are read using their original values before either write applies.
Blocking: a = b; executes first, so a becomes 9 immediately. b = a; then reads a's new value, so b also becomes 9 — not a swap at all, just both registers ending up equal to the original b. This is the exact same class of bug as the shift register example, applied to a swap instead of a pipeline.
Summary
In this tutorial, you learned:
- A simulation time step is divided into an ordered sequence of scheduling regions, not a single instant
- Preponed samples values for assertions; Active runs blocking code and evaluates (but doesn't yet apply) nonblocking right-hand sides; Inactive holds
#0delays; NBA applies nonblocking updates - NBA's deferred timing is what makes nonblocking assignments correctly model simultaneous hardware updates, avoiding the classic blocking-assignment pipeline bug
- Observed evaluates concurrent assertions; Reactive runs program blocks and assertion pass/fail actions; Postponed is where
$strobe/$monitorsee the fully settled value - The Active-Inactive-NBA cluster can loop within a single time step if new events keep landing back in Active