always_ff, always_comb and always_latch
The previous article replaced the type. This one replaces the block.
Plain always @(posedge clk) and always @(*) describe how a block is triggered, but nothing in the syntax tells the tool, or a reader, what kind of hardware you intended. A typo that puts a blocking assignment into a clocked block, or leaves a signal unassigned in a supposedly combinational block, is caught the hard way: synthesis warnings, or a silent sim/synth mismatch from the Section 8.1 checklist. SystemVerilog’s three intent-driven always variants exist to catch those mistakes earlier, by making you declare the intent up front.
always_ff: sequential logic only
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) q <= 1'b0;
else q <= d;
end
always_ff lets the tool check the rules you have followed by discipline since Section 5.1: assignments inside should be non-blocking, the sensitivity list must describe edge-triggered behaviour, and a variable assigned here must not be assigned anywhere else. Put a blocking assignment in an always_ff by accident and the tool flags it, where plain always @(posedge clk) would compile silently and show up later as a race.
always_comb: combinational logic only
always_comb begin
if (sel)
y = b;
else
y = a;
end
Two real improvements over always @(*). Tools check that the block uses blocking assignment and that its outputs are not driven from anywhere else, the mirror image of the always_ff checks. And always_comb executes once at time zero even before any input changes, where always @(*) only triggers on an actual transition and can show a stale value until the first input event. That closes a small but real gap that occasionally causes a spurious mismatch at the start of a simulation.
always_latch: intentional latches only
always_latch begin
if (latch_enable)
q_latched <= data_in;
// no else: deliberately level-sensitive
end
This serves the one deliberate-latch case in the course, operand isolation in Section 7.6. always_latch tells the tool and the reader “yes, this is meant to be a latch”, which suppresses the generic latch warning from Section 8.4 (the tool now knows the inference is intentional) and documents the intent in the source. That is exactly the distinction the latch article in Section 8.4 asked for: a waived, deliberate latch, clearly separated from an unexamined warning.
The check that catches a real bug
Recall the latch checklist in Section 8.4: an incomplete case silently infers a latch, and in classic Verilog the only way to find out is a synthesis warning, well after the RTL was written and simulated. Because an always_comb block has declared itself combinational, tools can flag an incomplete assignment path inside it as an error at compile or lint time rather than a downstream synthesis warning, sometimes before a single simulation cycle has run.
None of the three constructs change what hardware is synthesized. An always_ff block infers the same flip-flop a correct always @(posedge clk) always did. What changes is when a mistake gets caught. Plain always carries your intent only implicitly, through what you happened to write, and the tool has to guess whether a block was meant to be sequential, combinational or a latch. The SystemVerilog variants make you state it, which is what lets the tool check your code against your own intent rather than only against the language’s syntax rules.
Practice Exercise
The block below was meant to be purely combinational but has a bug. What would always_comb catch that plain always @(*) would not necessarily flag as clearly?
always_comb begin
if (mode)
result <= a + b; // bug: non-blocking assignment
else
result = a - b;
end
Solution
The <= in the if branch is a non-blocking assignment inside a block declared combinational, mixed with a blocking assignment in the else branch. That is the mixed-assignment mistake from the Section 5.1 rules, and it is the class of error always_comb is meant to catch, since a combinational block should not contain a non-blocking assignment at all. A plain always @(*) with the same mistake compiles without complaint in many toolchains and can produce a subtle simulation/synthesis difference (the non-blocking assignment schedules its update differently within the time step), surfacing as a hard-to-trace behavioural discrepancy rather than an immediate error naming the problem.
always_ff, always_comb and always_latch do not change the hardware; they change when a mistake is caught, by making you state your intent explicitly enough for the compiler to check against it. In the next article we look at typedef, struct and enum, moving past loose bit vectors and localparam values into named, self-documenting types.