SYSTEMVERILOG SERIES · SV-16

SystemVerilog Series — SV-16: Program Block — VLSI Trainers
SystemVerilog Series · SV-16

Program Block

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.

🏗 Three Purposes of Program

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.

  1. Entry point — provides a clear, defined start to testbench execution (via its initial blocks).
  2. Encapsulated scope — data declared inside the program is local to it, separate from the design hierarchy.
  3. Reactive region scheduling — all program code executes in the Reactive region, after the design has completely settled for the current time step. This eliminates the classic testbench-vs-RTL race condition.

📄 The Program Construct

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
Think of a program as a leaf module with special scheduling. Like a module, it has ports, can be instantiated, and can contain type/data declarations and subroutines. Unlike a module, all of its procedural code runs in the Reactive region — after the entire design has settled — rather than competing with RTL code in the Active region.

📌 What is Allowed Inside a Program

Allowed in a program

  • initial blocks (one or more)
  • Type and data declarations (static lifetime)
  • Task and function declarations
  • Continuous assignments (assign)
  • Concurrent assertion items
  • Clocking block declarations
  • Port connections to modules and interfaces

NOT allowed in a program

  • always blocks (any variant)
  • Nested modules
  • Nested interfaces
  • Nested programs
  • UDP (user-defined primitive) instances

📌 Assignment Rules

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 locationFrom inside a programFrom 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
Why non-blocking for design variables? If the program used a blocking assignment to drive a design signal, the update would happen immediately in the Reactive region, potentially changing a signal after the design has already “settled” — creating a race with the NBA region. Using non-blocking assignments ensures all testbench-driven changes are committed in the NBA region as one atomic update, just like flip-flop outputs.

📋 Nesting and Implicit Instantiation

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

📋 Multiple Programs

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
Simulation ends when all programs exit. When every 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.

Eliminating Testbench Races

Two sources of non-determinism in Verilog create testbench races:

  1. Active events can process in any order — testbench code and RTL code compete arbitrarily.
  2. Sequential statements in 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.

Active
(RTL runs)
NBA
(<=commits)
Observed
(assertions)
Reactive
(program runs)
// 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

📌 How the Reactive Region Works

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:

  • Event controls on design signals: @(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
Clocking blocks make program code even cleaner. Programs that read design values exclusively through clocking blocks with #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.

📋 Zero-Skew Clocking Block Races

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:

  1. Program statements run in the Reactive region — after all explicit #0 transitions have propagated and the design has reached a quasi-steady state.
  2. Design variables can only be modified via non-blocking assignments from within a program — updates fire in NBA, not immediately in Reactive.
Recommendation: use #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.

Blocking Tasks Called from Programs

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.

Task called 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

Same task called from a PROGRAM

// 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.
  • When a blocking task in a design module returns to program code, the program automatically resumes in the Reactive region — not the Active region where the task was running.
  • Copy-out of output/inout parameters happens when the task returns, at which point the program re-enters Reactive.
  • Calling a design module task from a program is legal. The reverse — calling a program task from inside a design module — is illegal and a compile error. The design must remain unaware of the testbench.
  • Design module functions (which always complete in zero time) can be called from programs with no special handling.
Calling program tasks or functions from design modules is illegal. The design must not be aware of or coupled to the testbench. Only the testbench can call into the design — never the other way around.

$exit — Program Lifecycle

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.
$exit vs $finish: $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.

Implicit exit — when all initial blocks complete

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

📋 Quick Reference

Program vs module comparison

Featuremoduleprogram
Procedural code regionActiveReactive
always blocksAllowedNot allowed
initial blocksStarts in ActiveStarts in Reactive
Assignment to own varsBlocking or non-blockingBlocking (=) only
Assignment to design varsBlocking or non-blockingNon-blocking (<=) only
Nested modulesAllowedNot allowed
Nested programsAllowed inside moduleNot allowed inside program
Simulation end trigger$finish$exit (per program) → all exit → $finish
Call design module tasksAllowedAllowed (returns to Reactive)
Call program tasks from designIllegalIllegal

Race elimination rules

  • Program initial blocks schedule in Reactive — after Active, NBA, and Observed all complete.
  • Design variables assigned only via non-blocking (<=) from programs — updates fire in NBA, not immediately.
  • Program variables assigned only via blocking (=) — local to program scope.
  • Design module functions can be called from programs freely (zero time, no scheduling issues).
  • Design module blocking tasks return to the Reactive region when called from a program.
  • Calling program tasks/functions from design modules is a compile error.
Coming next: SV-17 covers Assertions — immediate assertions with pass/fail actions and severity levels, concurrent assertions with clock semantics, property declarations, and the sequence operator.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top