SystemVerilog Basics

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");
end

Plain 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;
end

always_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;
end

always_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;
end

always_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 in always_comb (and initial/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 in always_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;
end

Because <= "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)!
end

The 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?

BlockTriggerAssignment to useTypical use
initialSimulation time 0, runs once= (blocking)Verification code: generating stimulus, one-time setup
Plain alwaysHand-written sensitivity listDepends on the scenarioSuperseded by the three more explicit forms below; not recommended in new code
always_combInferred automatically (fires when a read signal changes)= (blocking)Combinational logic
always_ffClock edge (posedge/negedge)<= (non-blocking)Sequential logic (flip-flops, registers)
always_latchInferred automatically, level-sensitive= (blocking)An intentional latch (uncommon)

Summary

  • initial runs once, isn't synthesizable, and is a basic tool of verification code.
  • Plain always relies on a hand-written sensitivity list, which is easy to get wrong and can desync simulation from synthesized behavior; always_comb/always_ff/always_latch replace it with more explicit keywords.
  • always_comb infers its sensitivity list and describes combinational logic; always_ff describes clock-driven sequential logic; always_latch is for an intentional latch.
  • Use blocking = in always_comb, and non-blocking <= in always_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 <=)