SystemVerilog · Module 15
Program Block vs Module — Race-Free Simulation
SystemVerilog program block — the construct that solves the RTL-testbench race by running in the Reactive region (after Active + NBA). Why the race exists, how the scheduler ordering eliminates it, the constructs program forbids (always, clock generators), the $exit vs $finish discipline, and how UVM moved past program blocks while keeping their semantic intent.
Module 15 · Page 15.1
When a testbench module and the RTL share the same clock edge, they compete for the same scheduler region. The result is a race condition — and the winner is decided by simulator evaluation order, not by design intent. The program block was added in SystemVerilog precisely to eliminate this race by moving testbench code into its own scheduler region — Reactive, which runs strictly after the RTL's Active and NBA regions. By the time the program block's code executes, every flip-flop in the DUT has settled to its final value for the current clock edge. The race is structurally impossible because RTL and testbench are in different, ordered scheduling regions.
1. Engineering Problem — The Active-Region Race
In Verilog and pre-program SystemVerilog, both RTL and testbench code run in the Active region of the simulator scheduler. When a clock edge arrives, both the flip-flop update logic (always_ff @(posedge clk) q <= d;) and the testbench sampling code (@(posedge clk); read_q();) try to execute in the same scheduler region — with no guaranteed ordering between them.
If the testbench reads a signal before the flip-flop has updated it at the same time step, it sees the old value. If it reads after, it sees the new value. Which one it sees depends entirely on the simulator's internal evaluation order — different tools, different runs, and even different optimisation flags can give different results.
The race is not a bug you wrote. It is a structural property of having both RTL and testbench in the Active region. No amount of careful coding inside the testbench can fix it; the only reliable solution is to move the testbench out of the Active region entirely. That is exactly what program does.
2. Mental Model — The Reactive Region Is Where the Dust Settles
The picture every engineer carries:
A SystemVerilog time step is a fixed sequence of scheduling regions: Preponed → Active → Inactive → NBA → Observed → Reactive → Postponed. RTL flip-flop updates commit in NBA (non-blocking assignments). Module testbench code wakes in Active — before NBA — so it sees pre-edge values. A
programblock'sinitialblocks run in Reactive — after NBA — so they see the fully-settled post-edge values. The dust has settled before the program block opens its eyes.
Three invariants this picture preserves:
- The region ordering is non-negotiable. IEEE 1800 §4.4 defines the region sequence; every conformant simulator must respect it. The race-elimination is therefore a language guarantee, not a coincidence of one simulator's implementation.
programis for testbench, not RTL. Modules host clocks, instantiate sub-modules, runalways_ff. Programs hostinitialandfinalblocks only — they cannot containalways,always_ff,always_comb, clock generators, or module instances. The constraints are intentional: the program block is the testbench control layer, not the design.- Writes from a program schedule to next-Active. Program-block assignments to DUT inputs are scheduled to the next time step's Active region — modelling the natural setup-before-edge timing of synchronous interfaces. Reads in Reactive, writes for next Active: the program block is half-cycle-shifted from the RTL.
3. Visual Explanation — Where program Code Lives
The seven scheduling regions of a single time step, with program's home region (Reactive) highlighted:
Program block initial / final code runs in the Reactive region
event regionsThe teaching is in the visual ordering. Active and NBA are where RTL lives; Observed hosts SVA sampling; Reactive is where program-block testbench code wakes. The horizontal arrow shows the irreversible left-to-right time-flow within one time step — by the time Reactive runs, NBA has already committed every non-blocking assignment from the current edge.
4. Syntax & Semantics — program, Ports, $exit
4.1 The basic form
program test_name (
input logic clk,
input logic rst_n,
output logic req,
input logic ack,
input logic [7:0] data_out
);
initial begin
// This initial block runs in the REACTIVE region.
// All signal reads here see post-NBA (fully settled) values.
req = 0;
@(posedge clk); // wait for clock edge
// By the time we're here (Reactive), the DUT has already committed
// all non-blocking assigns. data_out and other outputs are stable.
req = 1; // drive output — schedules to next Active
@(posedge clk iff ack); // wait until ack
assert (data_out !== 8'hX); // safe: data_out settled in NBA
$display("[TEST] data_out = 0x%h", data_out);
$exit; // graceful end of this program
end
final
$display("[TEST] Program block exiting");
endprogramA program block has a port list, can connect to interfaces, contains initial and final blocks, and is instantiated from a module like any other unit. The differences from module are in what's allowed inside and which scheduler region the code runs in.
4.2 What program forbids — and why
| Construct | Allowed in program? | Why |
|---|---|---|
initial block | ✓ | The primary structural element — runs in Reactive |
final block | ✓ | End-of-simulation cleanup / reporting |
| Tasks, functions, classes | ✓ | Standard SV building blocks |
always, always_ff, always_comb, always_latch | ✗ | Run continuously; belong in modules, not in test programs |
Clock generator (always #5 clk = ~clk) | ✗ | A clock is shared infrastructure for RTL; must live in a module |
| Module instantiation | ✗ | Program is the test consumer, not a design hierarchy node |
interface instantiation | ✓ via port | Connect by port to interfaces declared in the top module |
The constraints are deliberate: a program block is a testbench control program, not a piece of RTL infrastructure. Anything that runs forever (clocks, always blocks, monitors that fire on every edge) belongs in modules; the program orchestrates the test by waiting on clock edges and driving / sampling DUT signals.
4.3 $exit vs $finish
// ── $exit: graceful — ends this program; sim ends when ALL programs exit
program test_a (input clk);
initial begin
run_test();
$exit; // lets other programs' final blocks run cleanly
end
endprogram
// ── Implicit $exit: the last initial block completing exits the program
program test_b (input clk);
initial begin
run_test();
// When this initial ends naturally, the program exits.
// No $exit needed if it is the only initial block.
end
endprogram
// ── $finish: abrupt — kills ALL simulation immediately
program test_c (input clk);
initial begin
run_test();
$finish; // halts everything; other programs' final blocks may not run
end
endprogramDefault to $exit inside a program block. The language treats program termination as a clean shutdown — when every program block has exited, simulation ends. $finish is the nuclear option; reserve it for unrecoverable errors. The discipline parallels the $finish race from Module 14.3: kill background work cleanly or lose end-of-test data.
5. Simulation View — How the Scheduler Eliminates the Race
The IEEE 1800-2017 region order for a single time step is fixed: Preponed → Active → Inactive → NBA → Observed → Reactive → Postponed. The race-elimination property of program falls out of this ordering in three steps:
- RTL flip-flop updates use non-blocking assigns.
always_ff @(posedge clk) q <= d;schedulesq = dinto the NBA region of the current time step. The flip-flop outputqdoes not change in Active; it changes in NBA. - Module testbench code wakes in Active. A module
initialblock with@(posedge clk);wakes when the clock-edge event is delivered in the Active region. At this moment,qstill holds its pre-edge value — NBA has not run yet, so the assignment from step 1 is pending but not committed. - Program testbench code wakes in Reactive. A program
initialblock with@(posedge clk);wakes in the Reactive region — after Active and after NBA. The assignment from step 1 has committed;qnow holds its post-edge value. The program block sees the settled value.
The race in a module testbench is therefore: "Active runs Active-code in some order; if RTL's <= evaluates before testbench's read, the schedule queues an NBA update and the testbench reads pre-edge q; if testbench's read evaluates before RTL's <=, the testbench still reads pre-edge q." Either way the module-tb reads pre-edge q. The program-tb cannot make this mistake — Reactive is strictly after NBA.
Writes from a program block are subtler: a program-block assignment to a DUT input does not take effect in the current Reactive region. It is scheduled to the Active region of the next time step. This models the natural setup-before-edge stimulus pattern of synchronous interfaces.
6. Waveform — Module Reads Pre-Edge, Program Reads Post-Edge
The waveform below shows the race in motion. The DUT registers d into q on every posedge clk. A module-based testbench reads q at the same posedge clk and sees the pre-edge q — wrong. A program-based testbench wakes in Reactive (later in the same time step) and sees the post-edge q — correct.
Figure — module-tb reads pre-NBA q (RACE); program-tb reads post-NBA q (CORRECT)
10 cyclesBoth annotations sit at cycle 2 because both testbenches wake at the same $time. They differ only by scheduling region within that time step — module-tb in Active (before NBA), program-tb in Reactive (after NBA). The trace surfaces what the prose says: same edge, different region, opposite result.
7. Synthesis — Not Applicable
program is a procedural simulation construct. It has no hardware footprint and no place in synthesisable RTL — synthesis tools reject the program keyword outright. This section is intentionally omitted; the topic does not warrant it.
The corresponding hardware-side discipline for "read settled values, not in-flight values" is to use registered outputs that have already propagated through the flip-flop one cycle earlier. That is RTL design discipline, not a procedural primitive.
8. Verification View — Connecting via Interface + Clocking Block
The standard production pattern is to wire program blocks to DUTs through an interface that carries a clocking block. The interface bundles the signals; the clocking block defines the testbench sampling point (with explicit input/output skews — covered in 15.3 (coming)). The program block accesses signals through the clocking block, getting both the Reactive-region timing and the clocking-block skew controls in one composition.
// ── Interface: all DUT signals bundled together ───────────────
interface apb_if (input logic pclk);
logic presetn, psel, penable, pwrite, pready, pslverr;
logic [31:0] paddr, pwdata, prdata;
// Clocking block defines the testbench sampling point.
clocking cb @(posedge pclk);
default input #1 output #1; // skews — see 15.3
input pready, pslverr, prdata;
output psel, penable, pwrite, paddr, pwdata;
endclocking
modport TB (clocking cb, output presetn);
modport DUT (input psel, penable, pwrite, paddr, pwdata, presetn,
output pready, pslverr, prdata);
endinterface
// ── Program block: drives + samples via the clocking block ────
program apb_test (apb_if.TB bus);
initial begin
bus.presetn = 0;
repeat(4) @(bus.cb); // wait 4 clocking-block edges
bus.presetn = 1;
bus.cb.psel <= 1; // applied with output skew
bus.cb.paddr <= 32'h1000;
bus.cb.pwrite <= 1;
bus.cb.pwdata <= 32'hCAFE;
@(bus.cb);
bus.cb.penable <= 1;
@(bus.cb iff bus.cb.pready); // wait until pready — sampled with input skew
bus.cb.psel <= 0;
bus.cb.penable <= 0;
$display("[TEST] Write complete at %0t", $time);
$exit;
end
endprogram
// ── Top level wires everything together ───────────────────────
module top;
logic pclk = 0;
always #5 pclk = ~pclk; // clock MUST be in a module
apb_if bus (.pclk(pclk));
apb_slave dut (.bus(bus.DUT));
apb_test tb (.bus(bus.TB));
endmoduleThis composition — program block + interface + clocking block — was the canonical race-free SV testbench pattern from 2005 to roughly the UVM 1.0 era. Modern UVM testbenches typically use modules (not programs) for the test wrapper and rely on clocking blocks alone to provide the race-free sampling discipline — see §9 industry usage for why.
8.1 module vs program — side-by-side comparison
| Feature | module | program |
|---|---|---|
| Scheduler region | Active + NBA — same as RTL | Reactive — after all RTL updates |
| Race condition with RTL? | Yes — scheduling-order dependent | No — structurally impossible |
always blocks allowed? | Yes (always, always_ff, always_comb) | No — only initial and final |
| Clock generators | Yes — must live here | No — use a module |
| Sub-module instantiation | Yes | No — cannot instantiate modules |
| Terminates simulation? | No — runs until $finish | Yes — last initial exiting triggers sim end |
| Used for | RTL, DUT, clocks, interfaces, bus-functional models | Test programs, directed / random stimulus, checker logic |
9. Industry Usage — Why Modern UVM Moved Past program
program was added to SystemVerilog 2005 to bring Vera / OpenVera / Specman E heritage into the language — those proprietary HVLs had separate scheduling regions for testbench code, and program was the SV mechanism for the same race-elimination. For the first generation of SV verification environments (2005–2010), program blocks were the canonical test wrapper.
Then UVM happened. UVM 1.0 (2011) and onward standardised on module-based test wrappers (uvm_test is a class instantiated inside a module, not a program). Race-free sampling is achieved instead via clocking blocks in interfaces — the same Reactive-region timing the program block provided, but available to module-based code without needing the program construct.
Why the shift? Three reasons:
- UVM needs
forever/alwaysfor monitors. UVMuvm_monitor::run_phase()containsforeverloops sampling DUT outputs — which a program block cannot host (onlyinitialandfinalare allowed insideprogram). The UVM architecture needs module-hosted continuous activity. - One scheduling discipline beats two. Mixing modules and programs in the same testbench creates two scheduling models the verification engineer must reason about. Standardising on modules + clocking blocks gives one model.
$exitsemantics confuse multi-test regression. Program-block$exitsemantics interact poorly with regression harnesses that run multiple tests per simulation invocation. UVM'suvm_phasemachinery (objections, drain time) replaces program-block lifecycle with finer-grained control.
program is still in the language, still race-free, still occasionally seen in legacy and pedagogical contexts. New testbench architectures default to module wrappers + clocking blocks instead. The race-elimination semantics that program introduced now live in clocking blocks, available to any context — see 15.2 Clocking Block Deep Dive for the construct that does the heavy lifting in modern UVM.
10. Design Review Notes — What a Senior Will Flag
| Pattern in the diff | What review will say |
|---|---|
program test; always #5 clk = ~clk; endprogram | "Compile error — always is forbidden inside program. Clock generators live in modules; pass clk into the program through its port list." |
program test; ... $finish; endprogram | "Use $exit for program termination. $finish is the nuclear option that kills every still-running program / module immediately; $exit lets other programs' final blocks run cleanly." |
program test; apb_slave dut(...); endprogram | "Cannot instantiate modules inside program. Instantiate the DUT in the top-level module; pass it to the program through an interface." |
program test (input clk, output req); initial begin @(posedge clk); req = 1; end endprogram | "Driving outputs directly from program-block code without a clocking block produces zero-setup-time pulses at the next Active region. Use a clocking block with an output skew so the DUT sees stimulus with realistic setup time. See 15.2 / 15.3." |
module tb; program inner; ... endprogram inner i(); always_ff @(posedge clk) check(); endmodule | "Mixing module-hosted assertion / check logic with program-hosted testbench reintroduces the race for the module-side code. If you commit to a program for testbench race-elimination, all sampling must go through the program (or through clocking blocks). Half-program is half-fixed." |
New UVM testbench using program for the test wrapper | "UVM convention is module wrapper with clocking blocks for race-free sampling. program is legacy / pre-UVM. Most lint flows flag program-blocks-in-UVM-environments as an anti-pattern." |
program test without a port list, accessing top-level signals via hierarchical reference | "Hierarchical signal access bypasses the interface discipline. Even though program block reads in Reactive are race-free, hierarchical accesses make the testbench tightly coupled to one DUT structure. Use ports + interfaces." |
final begin $display("FAIL"); end inside a program with no actual check | "A final block is for end-of-sim reporting, not for deciding pass / fail. Move the check into the initial body's main flow; use final only for summary output." |
The single highest-value rule: understand which scheduling region the testbench code runs in. program makes it explicit (Reactive); clocking blocks make it explicit (post-NBA per the block's clock); pure-module testbench code is the dangerous default (Active, racy). The discipline question for every testbench review is: "how is this sampling race-free?" — if the answer isn't "program block" or "clocking block," you have a race.
11. Debugging Guide — Real Failures, Real Fixes
Module testbench reads q=0 right after DUT registers d=1
ACTIVE-REGION-RACEmodule tb;
logic clk = 0, d = 0, q;
dff dut (.clk, .d, .q);
always #5 clk = ~clk;
initial begin
d = 1;
@(posedge clk);
$display("q = %b (expected 1)", q); // prints 0 — RACE
end
endmoduleq = 0 (expected 1) even though the DUT clearly registered d=1 on the same posedge clk. Behaviour is simulator-dependent: VCS may print 0, Questa may print 1, Xcelium may print one then the other across versions. Cannot be debugged by inspecting the trace — Verdi shows q updating at the right edge.always_ff @(posedge clk) q <= d; and the testbench's @(posedge clk); wake in the Active region. The DUT's <= schedules an NBA-region update; the testbench's $display reads q before NBA runs. The testbench sees pre-edge q = 0.program block. The program's initial runs in Reactive — after NBA — so q is settled by the time $display runs. Or, in a module-based testbench, sample through a clocking block (see 15.2). Either way the structural region separation eliminates the race.Compile error: 'always' not allowed inside program
ALWAYS-IN-PROGRAMprogram bad_test;
logic clk = 0;
always #5 clk = ~clk; // ERROR
initial begin /* ... */ end
endprogram"always block not allowed inside program" from VCS / Questa / Xcelium. Confusing because always is the bread-and-butter Verilog construct.initial and final blocks — by design. always runs continuously; it belongs in module-hosted infrastructure, not in a test program. The compile error is the language enforcing the architectural rule.module top; logic clk = 0; always #5 clk = ~clk; ... program_inst tb(.clk); endmodule. Pass clk into the program through its port list. The program block now sees the clock and waits on it; the clock generator runs in the module where always is allowed.Other programs' final blocks never run
FINISH-INSTEAD-OF-EXITprogram test_a;
initial begin
run_directed_test();
$finish; // BUG: kills test_b before its final runs
end
endprogram
program test_b;
initial run_random_test();
final $display("[test_b] coverage = %0d%%", get_coverage());
endprogramtest_a reports complete. test_b's coverage summary never prints. Regression log is missing the per-test coverage data. Engineer assumes test_b crashed or didn't run.$finish in test_a aborted the entire simulation immediately — including test_b and its final block. $finish does not allow other programs' final blocks to execute. The expected graceful shutdown never happened.$exit instead of $finish inside program blocks. $exit ends only this program; the simulator continues running other programs (which is what allows their final blocks to fire). Simulation ends naturally when every program has exited. Reserve $finish for unrecoverable errors where you genuinely don't want any cleanup.DUT sees req=1 in the same time step the program drove it — setup time = 0
NO-CLOCKING-BLOCK-SKEWprogram test (input clk, output logic req);
initial begin
@(posedge clk);
req = 1; // assigned in Reactive
// scheduled to next Active at the SAME-time-step delta
// DUT sees req=1 with effectively zero setup time
end
endprogramreq → ack handshake completing at the same time as the request — sometimes. Simulator timing report flags zero-cycle setup. Real hardware would never produce this stimulus shape; the testbench is non-physical.@(posedge clk). The DUT effectively sees the input change in the same time step, simulating zero setup time. Real synchronous interfaces always have one-cycle setup.bus.cb.req <= 1;. The clocking block's output #N skew shifts the actual signal change N time units after the clock edge, modelling the realistic setup-time relationship the DUT expects. Full discipline covered in 15.2 Clocking Block Deep Dive.Half the testbench is race-free, half is racy — debug nightmare
MIXED-MODULE-PROGRAMmodule tb;
program inner;
initial begin @(posedge clk); check_in_program(); end
endprogram
inner i ();
always_ff @(posedge clk) check_in_module(); // STILL HAS RACE
endmodulecheck_in_program() calls always see correct DUT outputs. The check_in_module() calls intermittently see pre-edge values — failing 5% of the time across seeds. Coverage looks fine; assertion failures are clustered in the module-hosted check, not the program-hosted one.always_ff @(posedge clk) still wakes in Active, still reads pre-edge values, still races with the DUT. The program-block fix is structural — it has to be applied to every testbench sampling point.12. Interview Insights — What Interviewers Actually Probe
The Active-region race: a module-based testbench's @(posedge clk) wakes in the Active region — before the NBA region commits the DUT's flip-flop updates from that edge. The testbench reads pre-edge signal values; the DUT thinks it has updated them. Which order resolves depends on simulator evaluation order — non-deterministic.
A program block's initial runs in the Reactive region — strictly after Active and NBA. By the time program code executes, every non-blocking assignment from this clock edge has committed. The testbench reads post-edge settled values. The race is structurally impossible because RTL (Active+NBA) and testbench (Reactive) are in different, ordered regions.
13. Exercises
1. Design — race-free APB writer (Foundation)
Build a program apb_writer (apb_if.TB bus); that performs three APB writes to different addresses through bus.cb (the clocking block). Confirm the writes are visible to the DUT one cycle after they are issued (the clocking block's output-skew behaviour).
2. Debug — the silent race (Intermediate)
A teammate's module testbench reads q immediately after posedge clk and sometimes sees the pre-edge value. Same code, same DUT, different result across simulators. Identify the race, walk through which scheduling regions cause it, and propose two distinct architectural fixes (one moving the testbench into a program, one keeping it in the module but using a clocking block).
3. Code review — the dirty program (Intermediate)
A teammate submits a program test; containing always #5 clk = ~clk;, a module instantiation, and a call to $finish. Identify all three architectural violations and write the canonical fixes (one fix per violation).
4. Trade-off — program vs module + clocking block (Advanced)
A junior teammate proposes a coding rule: "always use program for testbench code — it's the canonical SV race-free construct." Argue the case against the blanket rule. When does module + clocking block strictly beat program? Consider the requirements of UVM monitors (forever loops), multi-test regression harnesses, and architectural-consistency concerns.
14. Summary
program runs in the Reactive region — strictly after Active and NBA. RTL flip-flop updates have already committed by the time program code executes, so the program always reads post-edge settled values. The Active-region race that plagues module-based testbench sampling is structurally impossible inside a program block.
Defaults to memorise. Programs allow only initial and final — no always, no clock generators, no sub-module instantiation. Use $exit for graceful program termination; reserve $finish for unrecoverable errors. Drive DUT inputs through a clocking block (with output skew) to model realistic setup-time stimulus. Program blocks are legacy/pedagogical in modern UVM — production testbenches use module + clocking block for the same race-free semantics with broader architectural compatibility.
Next up: 15.2 — Clocking Block Deep Dive (coming next) — the construct that gives modern module-based UVM testbenches the same Reactive-region timing the program block introduced, plus explicit input/output skew control, default direction conventions, and multi-clock support.