Chapter 8 of 16
Always Blocks and Processes: initial, always_comb, always_ff, always_latch
A systematic look at what initial, plain always, always_comb, always_ff, and always_latch are each for — and finally deliver on earlier chapters' promise to fully explain the difference between blocking (=) and non-blocking (<=) assignment.
Earlier chapters already used initial, always_comb, and always_ff in examples, but never formally covered where each one's boundaries are — and left one debt unpaid: what non-blocking assignment (<=) actually means, versus blocking assignment (=). This chapter goes through these procedural blocks systematically and settles that debt.
The initial block: runs once
An initial block starts executing at simulation time 0, runs through once, and is never re-triggered. It's not synthesizable — it exists purely in verification code, typically for generating stimulus or one-time setup:
initial begin
clk = 0;
a = 1'b0;
b = 1'b1;
#10 $display("initial block ran once");
endPlain always: fits anything, which is exactly the problem
Before SystemVerilog offered clearer categories, Verilog had a single always block, triggered based on a hand-written sensitivity list:
// Legacy style: combinational logic
always @(sel or a or b)
y = sel ? b : a;The problem is that sensitivity list is hand-written — miss one signal and simulation behavior diverges from the synthesized circuit's actual behavior (in simulation, a change on the missed signal won't trigger recomputation, but the synthesized combinational circuit will always respond to it regardless). This is a classic, notoriously subtle bug class. SystemVerilog replaces this "does anything, dangerously" always with three more explicit keywords: always_comb, always_ff, and always_latch.
always_comb: combinational logic, sensitivity list inferred automatically
always_comb begin
y = sel ? b : a;
endalways_comb automatically adds every signal read inside the block (here, sel, a, b) to its sensitivity list — you don't write one, and can't, which eliminates "forgot a sensitivity signal" bugs at the source. There's also a detail that differs from plain always: always_comb executes once right at the start of simulation (time 0), making sure the output is correct from the very beginning instead of waiting for the first trigger to compute anything.
always_ff: sequential logic, driven only by clock edges
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
endalways_ff explicitly declares intent — "this is sequential logic." It requires an edge-triggered event (posedge/negedge) and is typically used to describe flip-flops/registers. That explicit intent lets tools flag a warning when your code doesn't actually look like sequential logic (for example, accidentally using blocking assignment — see the next section).
always_latch: a latch, usually meaning "yes, I did mean to do this"
always_latch begin
if (enable)
q_latch = d;
endalways_latch infers its sensitivity list automatically just like always_comb, but semantically means "this is intentionally describing a latch (a level-sensitive storage element)." In digital design, latches are usually accidental (like a missing branch in an always_comb/case that causes the tool to infer an implicit latch) — cases where you actually want always_latch on purpose are relatively rare. Its real value is: if you do want a latch, this keyword states that intent explicitly, so the tool doesn't flag it as "probably a missing branch."
Blocking vs. non-blocking assignment: = and <=
This is the most important section in this chapter. The two assignment operators have completely different semantics:
- Blocking assignment (
=): statements execute immediately, in order, as written — the next statement waits for the current one to fully complete before starting, exactly like assignment in an ordinary programming language. Used inalways_comb(andinitial/testbench code). - Non-blocking assignment (
<=): doesn't write to the left-hand variable immediately. It first records "this variable should become this value," and only after every non-blocking assignment's right-hand side has been evaluated for the current simulation time step do all the updates happen together. Used inalways_ff.
Why does sequential logic need non-blocking assignment? Here's the classic example — swapping a and b on a clock edge:
// With non-blocking assignment: correctly implements a swap
always_ff @(posedge clk) begin
a <= b;
b <= a;
endBecause <= "records" the right-hand side first, both statements read a and b as they were before the clock edge, then update simultaneously afterward — exactly swapping the two registers' values.
// If blocking assignment is used by mistake: no swap happens
always_ff @(posedge clk) begin
a = b; // a immediately becomes b's value
b = a; // this reads the value of a that was JUST updated (i.e. the original b)!
endThe a read by the second line's b = a is already the new value the first line just wrote. The result: a and b both end up as the original b — not a swap, but a classic bug caused by using the wrong assignment style.
Remember this rule of thumb: use blocking = inside always_comb; use non-blocking <= inside always_ff; never mix both styles in the same block — tools will typically warn about mixing them, too.
Which block do I use?
| Block | Trigger | Assignment to use | Typical use |
|---|---|---|---|
initial | Simulation time 0, runs once | = (blocking) | Verification code: generating stimulus, one-time setup |
Plain always | Hand-written sensitivity list | Depends on the scenario | Superseded by the three more explicit forms below; not recommended in new code |
always_comb | Inferred automatically (fires when a read signal changes) | = (blocking) | Combinational logic |
always_ff | Clock edge (posedge/negedge) | <= (non-blocking) | Sequential logic (flip-flops, registers) |
always_latch | Inferred automatically, level-sensitive | = (blocking) | An intentional latch (uncommon) |
Summary
initialruns once, isn't synthesizable, and is a basic tool of verification code.- Plain
alwaysrelies on a hand-written sensitivity list, which is easy to get wrong and can desync simulation from synthesized behavior;always_comb/always_ff/always_latchreplace it with more explicit keywords. always_combinfers its sensitivity list and describes combinational logic;always_ffdescribes clock-driven sequential logic;always_latchis for an intentional latch.- Use blocking
=inalways_comb, and non-blocking<=inalways_ff— using the wrong style, especially blocking assignment in sequential logic, causes classic bugs like the swap example above.
Compared to plain always @(...), what problem does always_comb mainly solve?
In the always_ff block below, if a <= b; b <= a; is mistakenly written as a = b; b = a; (blocking assignment), what happens?
Which assignment operator should be used inside an always_ff sequential logic block? (type the symbol itself, e.g. = or <=)