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.

diagram
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 event

Who 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.

systemverilog
// 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.

systemverilog
// #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

  1. Never rely on the order of two processes in the same region — that order is not yours to assume.

  2. Cross the RTL/TB boundary only via region separation: nonblocking in RTL, cb skews (or reactive timing) in TB.

  3. Treat every #0 in a testbench as a code smell documenting a race someone did not fix.

  4. -> 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.