Part 1 · Language Foundations · Intermediate
always_comb, always_ff, always_latch
Intent-checking semantics vs plain always, sensitivity inference, what tools enforce, and a latch-inference example.
Why plain always was a problem
Verilog's plain always @(...) block carries no declared intent: the same construct models flip-flops, latches, and combinational logic, and the tool has no way to know which one you meant . Forget a signal in the sensitivity list and simulation quietly disagrees with synthesis (synthesis ignores the list; simulation obeys it). Forget an else branch and synthesis infers a latch you never wanted — silently. SystemVerilog's specialized blocks turn both mistakes into checkable contracts : you declare the hardware you intend, and the compiler/synthesizer errors or warns when the body doesn't match.
always_comb infers its sensitivity automatically from every variable read in the body (including inside called functions — an improvement over always @*, which misses signals read only inside functions). It also executes once at time zero so outputs are correct before the first input change, forbids any other process from writing its outputs, and lets tools flag incomplete assignments that would create latches. always_ff requires an explicit edge-sensitive list and tells synthesis “flip-flops only”. always_latch exists for the rare intentional latch, so reviewers know it isn't an accident.
// Combinational: no sensitivity list to forget
always_comb begin
unique case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
default: y = '0; // complete assignment: no latch possible
endcase
end
// Sequential: edge list required, flops intended
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= '0;
else if (en) count <= count + 1'b1;
end
// Intentional latch: documented by the keyword itself
always_latch begin
if (gate_en) q_lat <= d; // no else: holds value — by design
endThe latch-inference trap, before and after
Combinational logic must assign its outputs on every path through the block. Miss a branch and the output must “remember” its previous value on that path — and memory in unclocked logic is a level-sensitive latch. With plain always @* this slips through to synthesis as an unintended latch with all its timing-analysis pain; with always_comb the tool is entitled (and linting flows are configured) to flag the incomplete assignment immediately.
// BUG: grant unassigned when state==2'b11 → latch inferred
always @* begin
case (state)
2'b00: grant = req[0];
2'b01: grant = req[1];
2'b10: grant = req[2];
endcase // no default, no error from plain always
end
// FIX 1: default value first — every path now assigns grant
always_comb begin
grant = 1'b0; // safety assignment
case (state)
2'b00: grant = req[0];
2'b01: grant = req[1];
2'b10: grant = req[2];
endcase
end
// FIX 2: a default: arm inside the case works equally wellWHY A MISSING BRANCH BECOMES A LATCH
always @* / always_comb state==2'b11 path:
│ grant not assigned
▼ │
┌──────────────────────┐ ▼
│ case (state) │ "keep previous value"
│ 00: grant = req[0] │ │
│ 01: grant = req[1] │ ▼
│ 10: grant = req[2] │ unclocked storage element
│ (11: ???) │ │
└──────────────────────┘ ▼
┌─────────────────────┐
plain always : silent │ level-sensitive │
always_comb : tool flags │ LATCH en=(state │
incomplete assignment │ !=11) d=req[...] │
└─────────────────────┘What tools actually enforce
The LRM makes some checks mandatory and leaves others as quality-of-implementation. Reliably enforced: always_comb and always_latch variables may not be written by any other process (compile error); always_ff requires exactly one edge-sensitive event control and no other timing controls in the body. Commonly flagged by synthesis and lint, but not guaranteed at compile time everywhere: latch inference inside always_comb, and non-flop behavior inside always_ff. Interviewers probe exactly this boundary — “does always_comb prevent latches?” No: it makes them detectable and reportable; the default-assignment-first habit is what prevents them.
Quick contrast
always @* — sensitivity from body but misses function-internal reads; no write-exclusivity; no time-zero execution guarantee.
always_comb — full sensitivity (incl. functions), runs once at time 0, exclusive writes, latch checks.
always_ff — edge list mandatory, body must look like flops, exclusive writes; use nonblocking assigns.
always_latch — same checks as always_comb but declares the latch is intentional.
Key takeaways
The specialized always blocks are contracts: tools verify the body matches declared hardware intent.
always_comb beats always @* — function-read sensitivity plus mandatory time-zero execution.
Latch = unassigned path in combinational code; default-assign first, or always provide default/else.
always_comb doesn't prevent latches — it makes them detectable; the coding habit prevents them.
Common pitfalls
Writing an always_comb output from a second process — compile error that surprises plain-Verilog habits.
Putting #delays or extra event controls inside always_ff — the single-edge-list contract forbids it.
Relying on always @* when functions read signals — those reads are missing from the inferred list.
Treating synthesis latch warnings as noise — an unintended latch is a functional and timing bug.