Part 7 · Advanced & Integration · Intermediate
Scheduling Regions: the Full Map
The complete stratified scheduler — preponed through postponed — and where RTL, reactive TB code, assertions, and $monitor each execute.
The complete region map
Every simulation time step runs the same fixed pipeline of regions. Processes do not run 'in parallel' in any meaningful scheduling sense — they run in region order , with iteration loops back to Active when new events are created. Knowing which region your code executes in answers every 'why did I see that value?' question at the boundary.
ONE SIMULATION TIME STEP — STRATIFIED SCHEDULER
┌─ PREPONED ────────────────────────────────────────────┐
│ • assertion/cb input sampling (#1step values) │
│ • read-only: nothing may write here │
└──────────────────────┬────────────────────────────────┘
┌─ ACTIVE ─────────────▼────────────────────────────────┐
│ • module (design) processes: always/always_ff/comb, │◄──┐
│ continuous assigns, blocking-assign updates │ │
└──────────────────────┬────────────────────────────────┘ │
┌─ INACTIVE ───────────▼────────────────────────────────┐ │
│ • #0-delayed module processes │ │ iterate
└──────────────────────┬────────────────────────────────┘ │ while new
┌─ NBA ────────────────▼────────────────────────────────┐ │ events
│ • nonblocking assignment UPDATES (lhs gets value) │───┘
└──────────────────────┬────────────────────────────────┘
┌─ OBSERVED ───────────▼────────────────────────────────┐
│ • concurrent assertions EVALUATE (on Preponed data) │
└──────────────────────┬────────────────────────────────┘
┌─ REACTIVE ───────────▼────────────────────────────────┐
│ • program-block code, assertion action blocks, │◄──┐
│ class-based TB started from program context │ │
└──────────────────────┬────────────────────────────────┘ │
┌─ RE-INACTIVE ────────▼────────────────────────────────┐ │ iterate
│ • #0-delayed reactive processes │ │
└──────────────────────┬────────────────────────────────┘ │
┌─ RE-NBA ─────────────▼────────────────────────────────┐ │
│ • reactive nonblocking updates, CLOCKING BLOCK │───┘
│ OUTPUT DRIVES land here │
└──────────────────────┬────────────────────────────────┘
┌─ POSTPONED ──────────▼────────────────────────────────┐
│ • $monitor / $strobe print FINAL settled values │
│ • read-only: end-of-step snapshot │
└───────────────────────────────────────────────────────┘
│
▼ advance simulation time to next eventWho runs where
Placement of each kind of code
RTL always/always_ff/always_comb and continuous assigns — Active (reads + blocking updates), with nonblocking left-hand sides updated in NBA.
Concurrent assertions — sample operands in Preponed, evaluate in Observed, run pass/fail action blocks in Reactive. They judge pre-edge values by design.
program-block and reactive TB code — Reactive, after the design has settled for this iteration; its nonblocking drives land in Re-NBA.
Clocking block inputs — sampled in Preponed; clocking block output drives — applied in Re-NBA.
$display — prints wherever the calling process runs (Active for modules, Reactive for programs). $monitor/$strobe — Postponed, the settled end-of-step values.
// One edge, three different observations of the SAME signal:
always_ff @(posedge clk) begin
count <= count + 1;
$display("display: %0d", count); // Active: OLD value (pre-NBA)
$strobe ("strobe: %0d", count); // Postponed: NEW settled value
end
assert property (@(posedge clk) count < 10);
// Assertion: samples count in PREPONED — the pre-edge value,
// which equals what $display printed, NOT what $strobe printed.
// At the edge where count goes 4 -> 5:
// display: 4 (read in Active, before NBA update)
// assertion sees: 4 (Preponed sample)
// strobe: 5 (Postponed, after NBA update)#0 and event-ordering subtleties
The Inactive and Re-Inactive regions exist solely for #0 delays. A #0 does not 'wait for everything else' — it merely re-queues the process after the currently runnable same-strata processes, within the same time step. If those processes also use #0, you are back to unspecified relative order, one region later.
// #0 moves the second read AFTER currently-active processes...
initial begin
@(posedge clk);
#0; // re-queued into Inactive
v = data_in; // often sees the driver's blocking write
end
// ...but if TWO processes both #0 to "win", their relative
// order is again undefined. #0 is an ordering band-aid, not a fix.
// Named events have ordering rules of their own:
event e;
initial begin // process P1
@e; $display("P1 saw e"); // must be WAITING before trigger
end
initial begin // process P2
->e; // if P2 runs before P1 blocks at @e,
end // the trigger is LOST (not queued)
// Robust alternative: wait(flag) on a level, or use a mailbox/semaphore.Practical ordering rules
Never rely on the order of two processes in the same region — that order is not yours to assume.
Cross the RTL/TB boundary only via region separation: nonblocking in RTL, cb skews (or reactive timing) in TB.
Treat every #0 in a testbench as a code smell documenting a race someone did not fix.
-> event triggers are instantaneous, not queued — a process must already be waiting, or use level-sensitive wait()/mailboxes.
Key takeaways
The time step is a fixed pipeline: Preponed → Active/Inactive/NBA → Observed → Reactive/Re-Inactive/Re-NBA → Postponed.
Assertions judge Preponed samples; $display shows in-region values; $monitor/$strobe show Postponed settled values.
Reactive TB code runs after design settling, and its cb drives land in Re-NBA — that is the race immunity.
#0 shifts ambiguity one region later; it never removes it.
Common pitfalls
Comparing a $display value against an assertion failure and concluding the assertion is wrong — different regions, different snapshots.
Stacking #0 delays until a test passes — the order is still unspecified among the #0 processes.
Triggering ->e before any process waits on it — the event is lost silently.
Assuming two always blocks at the same edge run top-to-bottom in file order.