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.
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
endmodulefinal 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.
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
endmoduleSIMULATION 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 pathsTestbench 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.
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
endmoduleKey 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.