Part 1 · Language Foundations · Intermediate

initial, final & Startup Ordering

Multiple initial blocks and order nondeterminism, final blocks for end-of-simulation reporting, and TB startup sequencing patterns.

initial blocks: concurrent, once, in no particular order

Every initial block starts a process at time zero that runs its body once. The detail that bites: when several initial blocks exist — across one module or many instances — the LRM says nothing about which starts first. They are concurrent processes, and the simulator may begin them in any order. Code that works because initial block A “happens to run” before initial block B is a latent race: it can break on a different simulator, a different version, or a different compile-time optimization level. The corollary for multi-instance designs: each instance of a module contributes its own copy of every initial block, all unordered with respect to each other.

systemverilog
module startup_race;
  int cfg;

  initial begin            // block A: configure
    cfg = 42;
  end

  initial begin            // block B: consume — RACE!
    $display("cfg = %0d", cfg);   // 42 or 0, simulator's choice
  end

  // FIX 1: one block, explicit order
  // initial begin cfg = 42; $display("cfg=%0d", cfg); end

  // FIX 2: event handshake — order by synchronization, not luck
  event cfg_done;
  initial begin cfg = 42; ->cfg_done; end
  initial begin @cfg_done; $display("cfg = %0d", cfg); end
endmodule

final blocks: guaranteed last words

A final block runs exactly once when simulation ends — whether by $finish, reaching the end of scheduled events, or a tool-initiated stop — and it executes in zero time : no delays, no event controls, function calls only. That makes it the reliable place for end-of-simulation accounting: printing pass/fail summaries, dumping statistics counters, flagging transactions still in flight. Unlike a report task you must remember to call before every $finish, a final block fires on every exit path, including an unexpected $finish buried in third-party code. Like initial blocks, multiple final blocks execute in an undefined order relative to each other — keep them independent.

systemverilog
module scoreboard_top;
  int match_count, mismatch_count;
  int outstanding;

  // ... checking logic increments the counters during the run ...

  final begin
    $display("==== END OF SIMULATION REPORT ====");
    $display("matches    : %0d", match_count);
    $display("mismatches : %0d", mismatch_count);
    if (outstanding != 0)
      $display("** %0d transactions never completed **", outstanding);
    if (mismatch_count == 0 && outstanding == 0 && match_count > 0)
      $display("TEST PASSED");
    else
      $display("TEST FAILED");
  end
endmodule
diagram
SIMULATION LIFECYCLE — WHERE initial AND final RUN

  time 0                                            end of sim
    │                                                    │
    ▼                                                    ▼
  ┌──────────────┐  ┌──────────────────────────┐  ┌─────────────┐
  │ all initial  │  │   main simulation:       │  │ all final   │
  │ blocks start │  │   always blocks, NBA,    │  │ blocks run  │
  │ (UNORDERED)  │  │   assertions, TB tasks   │  │ (UNORDERED, │
  └──────┬───────┘  └──────────────────────────┘  │  zero-time) │
         │                      ▲                  └─────────────┘
         │  reset  config     │                        ▲
         └─ enable traffic ─────┘            $finish ────┘
            (handshake-ordered,              end-of-events
             never luck-ordered)             tool stop — ALL paths

Testbench startup sequencing patterns

Real testbenches need a deterministic boot order: assert reset, configure interfaces and models, then release traffic. Since initial-block ordering can't provide it, you build it explicitly. The three standard patterns: single orchestrator — one initial block calls reset, configure, and run tasks in sequence, with fork/join_none for background services; event or flag handshakes — producers fire named events (->reset_done) that consumers wait on, scaling to cross-module ordering; and in methodology code, phasing — UVM's build/connect/run phases are exactly this idea hardened into a framework, which is why “how does UVM solve initial-block ordering?” is a common bridge question in interviews.

systemverilog
module tb_top;
  event reset_done, config_done;

  // Single orchestrator owns the order
  initial begin
    do_reset();              // drive rst_n low N cycles, release
    ->reset_done;
    configure_dut();         // program registers via bus task
    ->config_done;
    fork
      run_traffic();         // foreground stimulus
      watchdog_timer();      // background service
    join_any
    $finish;
  end

  // Independent monitor: waits for the handshake, not for luck
  initial begin
    @config_done;
    monitor_bus();           // starts only after config is real
  end
endmodule

Key takeaways

  • Multiple initial blocks start in undefined order — any cross-block dependency is a latent race.

  • Order startup with one orchestrator block or event handshakes, never with assumed scheduling.

  • final blocks run zero-time code on every exit path — the home for pass/fail summaries and leak checks.

  • UVM phases are the methodology-scale answer to initial-block ordering — a frequent interview bridge.

Common pitfalls

  • Reading configuration in one initial block that another initial block writes — works until the simulator changes.

  • Putting delays or @(event) waits in a final block — illegal; final code must complete in zero time.

  • Printing the pass/fail verdict from the end of a run task — skipped when an unexpected $finish fires elsewhere.

  • Forgetting that every module instance replicates its initial blocks — N instances, N unordered processes.