Procedural Blocks & Control Flow¶
This is the layer most tutorials teach first and this site deliberately teaches later — because as a verification engineer, you read always_comb/always_ff in RTL far more often than you write it, while initial blocks, tasks, and blocking assignment are what your own testbench code is built from. Understanding both sides well enough to reason about who's racing whom is the actual goal.
initial vs. always_* — who's writing which¶
// Testbench code — runs once, top to bottom
initial begin
reset_dut();
drive_stimulus();
$finish;
end
// RTL code — the DUT re-evaluates on every trigger
always_ff @(posedge clk or posedge rst) begin
if (rst) count <= 0;
else count <= count + 1;
end
initial blocks execute their statement list once at time 0 (or whenever they're reached, for something like a dynamically-created class-based process) and then end — unless the last statement is itself a loop like forever, nothing re-triggers them. This is the natural home for testbench control flow: reset sequences, top-level stimulus, and the forever loops inside drivers/monitors that need to run for the whole simulation.
always (legacy) and its three SystemVerilog-specific replacements re-trigger every time something in their sensitivity list changes:
| Block | Intent | What the tool checks for you |
|---|---|---|
always_comb |
Combinational logic | Warns if you write something that isn't purely combinational (e.g. a latch by accident) |
always_ff |
Sequential (clocked) logic | Warns if you mix blocking and non-blocking assignment inside it |
always_latch |
Intentional level-sensitive latch | Warns if the block isn't actually latch-shaped |
always |
Legacy — any of the above, no checking | Nothing — this is exactly why the three variants above exist |
The three typed variants exist specifically so the simulator (and lint tools) can catch the "accidentally described a latch" and "mixed blocking/non-blocking in a flip-flop" bug classes at compile time instead of leaving them as timing bugs you find during verification — which is a good reason to flag it in review when you see plain always in new RTL.
final is the mirror image of initial — runs once at the very end of simulation, useful for a last chance to print a summary (e.g., "N transactions checked, M errors") after everything else has settled.
Branching: if/case and their variants¶
Plain case requires an exact bit-for-bit match, X/Z included — which is usually what you want. casex and casez relax that: casez treats Z in the case expression as a wildcard, casex treats both X and Z as wildcards.
casex is a well-known bug generator
casex wildcards any X in the signal being compared, not just in the case labels — so if the actual signal happens to be carrying an X (an uninitialized register, a real bug), casex will happily match it against a label that expects a specific value, silently hiding the exact class of bug X-checking exists to catch. Prefer casez (or plain case with explicit ? don't-cares) unless you have a specific, understood reason to reach for casex.
unique case and priority case add a synthesis/simulation-consistency guarantee on top of ordinary case: unique asserts (and a linter checks) that exactly one branch matches with no overlap, priority asserts that at least one branch matches, evaluated top-to-bottom. Both cause a runtime warning if the assumption is violated — useful in RTL, less commonly needed in testbench code, but you'll see them constantly when reading DUT source.
Loops¶
for (int i = 0; i < 8; i++) mem[i] = 0;
foreach (mem[i]) mem[i] = 0; // no manual index/bound bookkeeping
while (!queue.size()) @(posedge clk); // wait for something to arrive
forever begin // runs for the rest of simulation
@(posedge clk);
sample_coverage();
end
foreach is the one genuinely new loop construct — it iterates every element of an array (fixed, dynamic, associative, or queue) without you tracking bounds by hand, and it correctly handles sparse associative arrays (visiting only the keys that actually exist) where a manual for loop would need .first()/.next() bookkeeping instead. forever is what drivers and monitors are built from: a forever loop that blocks on an event or clock edge each iteration, running for the entire test.
Blocking vs. non-blocking assignment¶
a = b; // blocking — executes immediately, next statement waits for this one
a <= b; // non-blocking — schedules the update, doesn't block the calling process
This is the single most consequential syntax choice in the whole language, and the rule of thumb is short: use blocking (=) in testbench/procedural code and inside always_comb; use non-blocking (<=) inside always_ff. The reason is scheduling semantics, not style — non-blocking assignments are evaluated using the pre-update values of every signal on the right-hand side and only commit at the end of the current time step, which is exactly the behavior a real flip-flop has (every register updates "simultaneously" based on the previous cycle's values). Blocking assignment updates immediately, which is what you want for sequential procedural logic (a testbench task computing a value step by step) but produces simulation-order-dependent races if used for clocked RTL state.
Why this matters even if you never write RTL
You don't need to write always_ff to be affected by this. A driver task that samples a DUT output with blocking assignment in the same time step the DUT updates it with non-blocking assignment is a classic testbench race — whether you see the old value or the new value depends on process scheduling order, not on anything in your code being "wrong." This is exactly what clocking blocks exist to solve; see Interfaces.
Timing controls: #, @, wait¶
#10; // delay: advance 10 time units, then continue
@(posedge clk); // event control: block until this specific edge
@(a or b); // block until either a or b changes (implicit in always_comb's sensitivity)
wait (fifo_empty == 0); // block until a condition becomes true, re-checked on every change
#delay is almost never appropriate in synthesizable RTL and shows up constantly in testbenches (#5 rst = 0; in a reset sequence). @(...) is an edge/event trigger — the workhorse for anything clock-synchronous. wait(condition) is level-sensitive rather than edge-sensitive: if the condition is already true, it doesn't block at all, which makes it the right choice for "proceed once this becomes true, don't care exactly when" logic rather than "proceed on this specific transition."