The program construct as a testbench container — how it schedules in the Reactive region to eliminate design-testbench races, assignment rules that prevent non-determinism, multiple cooperating programs, blocking tasks called from programs, and the $exit lifecycle task.
The module construct is ideal for hardware description but awkward for testbenches — it has no entry point, no guaranteed execution order relative to the design, and no protection against races with RTL code. The program block solves all three problems.
initial blocks).A program block looks and connects like a module — same port syntax, same instantiation mechanism — but has specialised execution semantics.
// ANSI-style ports — simplest form program test(input clk, input [16:1] addr, inout [7:0] data); initial ... endprogram // Using an interface port program test(interface device_ifc); initial ... endprogram // Closing name (optional, must match) program test(...); ... endprogram : test // Instantiated at the top level like any module module top; cpu dut(...); test tb(...); // program connected to DUT ports endmodule
initial blocks (one or more)assign)always blocks (any variant)The assignment rules for programs enforce the clean separation between testbench and design, preventing the testbench from creating races within the design’s Active region.
| Variable location | From inside a program | From inside a module |
|---|---|---|
| Program variable (declared in a program) | Blocking (=) only | Illegal — compile error |
| Design variable (declared in a module) | Non-blocking (<=) only | Both allowed |
program tb(input clk, output logic [7:0] bus_out); int pkt_count; // program variable — local initial begin pkt_count = 0; // OK: blocking to program variable bus_out <= 8'hFF; // OK: non-blocking to design variable // pkt_count <= 0; // ERROR: NBA to program variable // bus_out = 8'hFF; // ERROR: blocking to design variable end endprogram
Programs can be nested inside modules or interfaces to share variables. Nested programs with no ports, and top-level programs that are never explicitly instantiated, are implicitly instantiated once — their instance name is the same as their declaration name.
module test(...); int shared; // visible to both nested programs below program p1; initial shared++; // p1 can read/write shared endprogram // p1 implicitly instantiated once program p2; initial shared--; // p2 also accesses shared endprogram // p2 implicitly instantiated once endmodule // Top-level program — not explicitly instantiated — auto-runs once program monitor; initial forever @(posedge clk) $display("tick"); endprogram
Any number of program definitions and instances are allowed. Programs can be fully independent or cooperative, sharing data through nesting, packages, or hierarchical references.
// Independent programs — no shared state program generator(...); ... endprogram program checker(...); ... endprogram // Cooperative programs — share data via a mailbox mailbox #(Packet) gen2drv = new(); program gen_prog(...); initial forever begin Packet p = new; void'(p.randomize()); gen2drv.put(p); end endprogram program drv_prog(...); Packet p; initial forever begin gen2drv.get(p); drive_bus(p); end endprogram
initial block in every program instance has completed, all programs implicitly call $exit and simulation terminates. This prevents the testbench from having to manually call $finish in most cases.Two sources of non-determinism in Verilog create testbench races:
always/initial blocks don’t execute as a single atomic event — another process can run between statements.The program block eliminates both by scheduling all its code in the Reactive region — after the design has fully settled.
// In a module's initial block — races with RTL Active events module bad_tb; initial begin @(posedge clk); data_check = dut.out; // RACE: dut.out may not be settled yet end endmodule // In a program block — no race: runs after design settles program good_tb(...); initial begin @(posedge clk); // Reactive: waits for clock edge data_check = dut.out; // safe: dut.out is stable end endprogram
Any statement inside a program block that is sensitive to changes in design signals (signals declared in modules, not in the program itself) is scheduled in the Reactive region. This includes:
@(clk), @(posedge clk)initial blocks inside programs — they start in the Reactive region (unlike module initial blocks which start in Active)program tb(...); initial begin @(posedge clk); // S1 scheduled into Reactive when clk goes high $display(dut.state); // reads design state AFTER NBA committed @(posedge clk); // wait for next clock — back to Reactive drv.data <= 8'hAB; // NBA to design signal — committed in NBA next step end endprogram // Contrast with a module initial block: // @(posedge clk) in module schedules into Active — may see glitching RTL state
#0 input skews are insensitive to read-write races, because #0 inputs are sampled in the Observed region — before the Reactive region runs. The program then reads stable, post-NBA values. With non-zero skews (especially #1step), the sampled value comes from the previous time step, making it completely glitch-free.When a clocking block sets both input and output skews to #0, its inputs are sampled in the Observed region at the same time its outputs are driven in the NBA region. This can still cause races because sampling and driving happen in the same time step. The program block minimises but does not completely eliminate this risk through two mechanisms:
#0 transitions have propagated and the design has reached a quasi-steady state.#1step (the default input skew) rather than #0. The #1step skew samples in the Postponed region of the previous time step — before the current clock edge’s Active region even begins. This completely eliminates sampling races for inputs.A program is allowed to call tasks defined in design modules, but the interaction has specific semantics that differ from calling the same task from a module.
module M; task T; S1: a = b; // executes in Active // b may not yet have NBA update #5; S2: b <= 1; // always before Observed endtask endmodule
// S1 executes in REACTIVE region:
// b already has its post-NBA value.
// S2 still fires immediately after #5
// — NOT postponed to Reactive,
// even though called from a program.Programs have a defined lifecycle. Each program instance ends either explicitly via $exit or implicitly when all its initial blocks complete. When all program instances have exited, simulation terminates with an implicit $finish.
program tb(...); initial begin run_test(); $display("Test complete"); $exit(); // explicit exit — terminates all spawned threads too end endprogram // When ALL programs exit (explicitly or implicitly), // $finish is called automatically — simulation ends. // $exit also terminates ALL processes spawned by the calling program: // fork…join_none threads, background monitors, etc. // Use this for clean testbench teardown.
$finish immediately ends simulation for everyone regardless of other programs. $exit ends only the calling program — simulation continues until all other programs also exit. Use $exit when running multiple independent test programs that each finish at different times; use $finish for an emergency hard stop.program auto_exit(...); initial begin 100.randomize_and_send(); // runs 100 tests // When this initial block reaches its end: // → program implicitly calls $exit // → all threads spawned by this program are killed // → if no other programs running → simulation ends end endprogram
| Feature | module | program |
|---|---|---|
| Procedural code region | Active | Reactive |
| always blocks | Allowed | Not allowed |
| initial blocks | Starts in Active | Starts in Reactive |
| Assignment to own vars | Blocking or non-blocking | Blocking (=) only |
| Assignment to design vars | Blocking or non-blocking | Non-blocking (<=) only |
| Nested modules | Allowed | Not allowed |
| Nested programs | Allowed inside module | Not allowed inside program |
| Simulation end trigger | $finish | $exit (per program) → all exit → $finish |
| Call design module tasks | Allowed | Allowed (returns to Reactive) |
| Call program tasks from design | Illegal | Illegal |
initial blocks schedule in Reactive — after Active, NBA, and Observed all complete.<=) from programs — updates fire in NBA, not immediately.=) — local to program scope.