SystemVerilog · Module 17
Scheduling Semantics & Timing Regions
SystemVerilog scheduling semantics — the ordered event regions of one simulation time step (Preponed, Active, Inactive, NBA, Observed, Reactive, Re-Inactive, Re-NBA, Postponed). Why <= is race-free, how SVA sampling / clocking blocks / program blocks stay race-free, $display vs $strobe vs $monitor, and the practical rules that keep simulation deterministic across tools.
Module 17 · Page 17.6
Every race condition, every "passes on VCS, fails on Questa" mystery, and every reason <= exists traces back to one thing: what order events execute in within a single simulation time step. SystemVerilog does not collapse a time step into an instant — it processes pending events through a fixed sequence of regions (Preponed → Active → Inactive → NBA → Observed → Reactive → Re-Inactive → Re-NBA → Postponed), and only when all regions are empty does time advance. This ordering is why non-blocking assignments are race-free, why SVA sampling is deterministic, why clocking blocks and program blocks eliminate testbench races, and why $strobe shows a different value than $display. This lesson — the model that the whole language sits on, specified in IEEE 1800-2017 Chapter 4 — closes Module 17 and the SystemVerilog track.
1. Engineering Problem — Order Decides Correctness
Consider a register swap on a clock edge:
// Initial: a=3, b=5
always @(posedge clk) begin
a = b; // a becomes 5 immediately
b = a; // b reads the NEW a → 5, not the old 3
end
// Result: a=5, b=5 — WRONG. Not a swap, a bug.With blocking = the second statement reads the value the first just wrote. Worse, if two separate always blocks both use blocking assignments on shared signals, the answer depends on which block the simulator runs first — and that order is unspecified. VCS may pick one, Questa the other; your test passes on one tool and fails on the other. That is the textbook race condition.
The fix is not luck or #0 hacks — it is the language's event-region model. Non-blocking <= splits into "evaluate now, update later," so every clock-edge block reads pre-edge values regardless of execution order. SVA, clocking blocks, and program blocks each live in a region chosen so their reads happen on stable values. Understanding the regions turns "why does this race?" from a guess into a lookup.
2. Mental Model — A Time Step Is Ordered Layers, Not an Instant
The picture every engineer carries:
One simulation time does not happen all at once — it is processed as a fixed sequence of regions. Within a region, events run in unspecified order (so depending on intra-region order is a race); between regions the order is guaranteed. The design side runs Preponed → Active → Inactive → NBA → Observed; the testbench side then runs Reactive → Re-Inactive → Re-NBA → Postponed. If any region generates new events, the simulator loops back to Active before time advances. Race-free constructs work by putting their reads in a region that runs after the writes they care about.
Four invariants this picture preserves:
- Active = blocking
=, continuous assigns, gate eval, and NBA right-hand-side evaluation; NBA = the deferred<=left-hand-side updates. The split is what makes<=order-independent. - The testbench half runs after the design half. Reactive (program blocks) runs after NBA, so testbench code always reads fully-committed values.
- SVA samples in Preponed (before any writes) and evaluates in Observed (after NBA). Sampling on pre-write values is why assertions are deterministic.
- Postponed is final.
$strobe/$monitorrun there on stable values;$displayruns in Active and can show pre-update (stale) values.
3. Visual Explanation — The Nine Regions of One Time Step
Regions execute top to bottom; only when all are empty does time advance.
4. The Active Region — Where Races Live
The Active region does most of the work — and it processes everything in unspecified order. Depending on that order is, by definition, a race.
| Runs in Active | Example |
|---|---|
| Blocking assignments | a = b + 1; c = d & e; |
| Continuous assignments | assign y = a & b; |
| Gate-primitive evaluation | and (y, a, b); |
| Non-blocking RHS evaluation | b <= a + 1; — the a+1 is computed here |
$display / $write | $display("val=%0d", x); |
| Module input-port updates | a driving signal change reaches the port |
5. The NBA Region — Why <= Is Race-Free
A non-blocking assignment executes in two phases, deliberately separating read from write:
- Active region: the right-hand side of every
<=is evaluated and the result queued. The left-hand side is not yet changed. - NBA region: after all Active events finish, every queued update is applied simultaneously. All left-hand sides change at the same instant.
So every always @(posedge clk) block reads values from before the clock edge, no matter what order the blocks run — they are fully decoupled.
// Initial: a=3, b=5
always @(posedge clk) begin
a <= b; // Active: RHS = b = 5 → queue (a ← 5)
b <= a; // Active: RHS = a = 3 → queue (b ← 3)
end
// NBA: a := 5, b := 3 simultaneously → correct swap (a=5, b=3)
// Blocking version is the bug from §1:
always @(posedge clk) begin
a = b; // a = 5 now
b = a; // b reads NEW a = 5 → a=5, b=5 (wrong)
end6. Preponed & Observed — How SVA Stays Race-Free
Concurrent assertions must sample stable, pre-update values at the clock edge — not values caught mid-write. Two regions handle this:
- Preponed (region 1): before any process runs, assertions sample their input signals. These reads are locked in; no write that time step can contaminate them.
- Observed (region 5): after NBA completes (all writes done), assertion properties are evaluated against the sampled values. Pass/fail is decided here, on fully-committed state.
This is also exactly how a clocking block's input skew works — #1step samples in the Preponed region, which is why clocking blocks give race-free testbench sampling:
default clocking tb_cb @(posedge clk);
input #1step dout; // sample dout in Preponed — before any Active write
output #1 din; // drive din 1 time unit after the edge
endclocking
initial begin
##1; // advance one clock cycle via the clocking block
$display("dout=%b", tb_cb.dout); // sampled in Preponed → guaranteed stable
end7. The Reactive Region — Race-Free Testbench Code
Code inside a program block runs in the Reactive region — after Active, Inactive, and NBA. So program code always reads values the NBA region has already committed; it is structurally impossible for it to race with design code.
// ❌ module TB — reads dout in Active, the design also writes it there → race
module tb_bad;
always @(posedge clk)
$display("dout=%b", dout); // may see old or new value
endmodule
// ✅ program TB — reads dout in Reactive, after the design's NBA committed → race-free
program tb_good;
initial begin
@(posedge clk);
$display("dout=%b", dout); // always the stable, post-edge value
end
endprogramThis is the scheduling-level explanation of why Module 15's program blocks and clocking blocks eliminate the testbench-DUT race: they move the testbench's reads into a region that runs after the design's writes.
8. The Postponed Region — $strobe vs $display vs $monitor
Postponed is the last region of a time step — once there, no further events can be scheduled at the current time, so all values are final. $strobe and $monitor run here; $display runs back in Active.
| Task | Region | When it prints | Best for |
|---|---|---|---|
$display | Active | immediately, where the statement executes | mid-procedure state, progress messages |
$strobe | Postponed | end of time step, after all updates | final stable signal values |
$monitor | Postponed | whenever any argument changes (auto) | watching signals across the whole run |
$write | Active | like $display, no newline | building a line piecewise |
always @(posedge clk) begin
result <= temp | c; // NBA — result updates in the NBA region
$display("result=%b", result); // Active — prints the OLD result (pre-NBA)
$strobe ("result=%b", result); // Postponed — prints the NEW result (post-NBA)
end9. Practical Rules — What Engineers Actually Do
You don't need to memorise the algorithm; follow six rules and you will not write a race:
- Clocked RTL → always
<=. Decouples evaluation (Active) from update (NBA); all clock-edge blocks become order-independent. - Combinational logic → always
=.always_combwants immediate effect so dependent logic re-evaluates in the same Active pass. - Never mix
=and<=in onealwaysblock. It's a lint warning and a maintenance trap — pick one per block. - Testbench →
programblock or clocking block. Reactive-region (program) or Preponed-sampling (clocking block) reads are race-free. - Print signal values with
$strobe, not$display.$displayin an Active-region block can print pre-NBA (stale) values. - Never use
#0as a fix. A#0(Inactive region) deferral is a code smell — it hides a race instead of removing it. Fix the race.
10. Debugging Guide — The Mistakes Everyone Makes Once
scheduling semantics — bugs every engineer hits the first time
Symptom: A clocked block produces a one-cycle-off or simulator-dependent result, and synthesis adds unexpected combinational gates (or rejects the block).
Cause: a = b + 1; c = a * 2; in a clocked block — the second statement reads the new a from the same time step, burying a combinational dependency inside clocked logic.
always_ff @(posedge clk) begin a = b + 1; c = a * 2; end // ❌ c uses new aFix: use <= so both RHS read old values in Active and both LHS update in NBA. If c truly needs the new a, that's a second cycle or a separate always_comb.
always_ff @(posedge clk) begin a <= b + 1; c <= a * 2; endGuardrail: <= everywhere in clocked logic — no exceptions.
Symptom: Testbench checks see the previous cycle's value; intermittent, tool-dependent mismatches.
Cause: A module's always @(posedge clk) reads dout in the Active region, before the DUT's NBA update fires.
Fix: read in a region that runs after the design's NBA — a program block (Reactive), a clocking block with #1step input skew (Preponed), or $strobe.
program tb; initial begin @(posedge clk); check(dout); end endprogramGuardrail: testbench sampling belongs in Reactive/Preponed, never raw Active.
Symptom: $display in a clocked block prints last cycle's value of an <=-assigned signal.
Cause: $display runs in Active, before the NBA update to that signal is applied.
always @(posedge clk) $display("q=%b", q); // ❌ Active — pre-NBA qFix: use $strobe (Postponed) for final stable values.
always @(posedge clk) $strobe("q=%b", q); // ✅ post-NBAGuardrail: $strobe for signal values, $display for control-flow/progress messages.
Symptom: Time advances by a full clock period where you expected one time unit (or vice-versa).
Cause: ##1 means "advance one clock cycle" (one clock period); #1 means "advance one time unit." In a 10 ns clock, ##1 is 10 ns.
Fix: use ##N for clock-cycle delays in clocking-block context and #T for absolute time — never interchange them.
Guardrail: ## counts cycles, # counts time — different units, never mix carelessly.
11. Q & A
12. Cross-References & What's Next
This lesson covered the event-region model — the ordered regions of a time step, why <= is race-free, how SVA/clocking/program blocks stay race-free, and the $display/$strobe/$monitor distinction. It closes Module 17 (Advanced Topics) and the SystemVerilog language track.
- Previous: 17.5 — config & Library Mapping — build-time implementation selection; the regions here are the runtime execution model.
- Module index: Module 17 — Advanced Topics — DPI-C, bind, checkers, specify, config, scheduling.
The regions explain the "why" behind earlier modules:
- Blocking vs Non-Blocking Assignments — the
=/<=rules, now grounded in the Active/NBA split. - Program Block vs Module — the Reactive-region guarantee in full.
- Clocking Blocks — Deep Dive — Preponed sampling and skews.
With the language track complete, the natural next step is methodology: What UVM Is — and What It Is Not — every UVM environment is built on the race-free constructs this model underpins.
13. Quick Reference
// ── One simulation time step (executed top to bottom) ────────────────
// 1 PREPONED — SVA signal sampling (read-only)
// 2 ACTIVE — blocking (=), continuous assigns, gate eval, NBA RHS, $display
// 3 INACTIVE — #0 delays (avoid)
// 4 NBA — non-blocking (<=) LHS updates
// 5 OBSERVED — SVA property evaluation
// 6 REACTIVE — program-block code (race-free TB reads)
// 7 RE-INACTIVE — program #0
// 8 RE-NBA — program <= updates
// 9 POSTPONED — $strobe, $monitor (final stable values)
// regions 2–8 may loop back to ACTIVE before time advances
always_ff @(posedge clk or negedge rst_n) // Rule 1: clocked → <=
if (!rst_n) q <= 0; else q <= d;
always_comb y = a & b | c; // Rule 2: combinational → =
$strobe ("t=%0t q=%b", $time, q); // Postponed — always stable
$display("t=%0t q=%b", $time, q); // Active — may be stale
program tb; initial begin @(posedge clk); check(dout); end endprogram // Reactive — race-free
default clocking cb @(posedge clk); // #1step → Preponed sampling
input #1step dout; output #1 din;
endclocking14. Summary — Module 17 & Track Closer
A SystemVerilog time step is not an instant — it is a fixed sequence of regions: Preponed → Active → Inactive → NBA → Observed (design half), then Reactive → Re-Inactive → Re-NBA → Postponed (testbench half), looping back to Active if new events appear before time advances. Order within a region is unspecified (so depending on it is a race); order between regions is guaranteed (so race-free constructs work by reading in a region that runs after the writes they need).
This one model explains the rest of the language. <= is race-free because its RHS evaluates in Active but its LHS updates in NBA, decoupling clock-edge blocks. SVA is deterministic because it samples in Preponed (before writes) and evaluates in Observed (after NBA). Program and clocking blocks kill the testbench race by reading in Reactive/Preponed, after the design commits. $strobe/$monitor show final values in Postponed where $display (Active) can show stale ones. Follow the six rules — <= in clocked logic, = in combinational, never mix them, program/clocking blocks for testbenches, $strobe for values, no #0 workarounds — and your simulation stays deterministic across every tool.
This completes Module 17 and the SystemVerilog language curriculum. The race-free constructs grounded here — clocking blocks, program blocks, assertions — are the foundation every UVM verification environment is built on; methodology is the next track.