Verilog's general-purpose always block can model many kinds of hardware. It also models algorithmic behavior, such as implicit state machines spanning multiple cycles, and is used in testbenches for clock generation and repeated verification processes. SystemVerilog adds three special-purpose blocks, always_comb, always_latch and always_ff, that state exactly which kind of hardware you mean. Simulators, synthesis tools and lint checkers can then verify that your code matches that intent.
8 min read | Intermediate Level
What You'll Learn
- Why the general-purpose
alwaysblock leaves tools guessing about your intent - How
always_combinfers its sensitivity list, and how that improves on@* - When to use
always_latchandalways_ff, and the rules each one enforces - The coding mistakes these blocks help you catch
Why do we need a special always block?
A synthesis tool cannot simply execute an always block's statements. It must also work out whether the block describes combinational logic, a latch or flip-flops, by analyzing its statements and event controls. In the example below, combinational and sequential coding styles are mixed in the same always block.
module my_design (
input logic clk,
input logic reset_n, // Active-low reset
input logic d1,
input logic d2,
output logic q
);
logic temp;
always @(posedge clk) begin
if (!reset_n)
q <= 0;
else begin
// Blocking assignment (combinational style) mixed inside sequential block
temp = d1 & d2;
q <= temp | (q << 1);
// Incrementing q again creates ambiguity in synthesis
q = q + 1;
end
end
endmodule
The syntax and semantics are legal, so there are no compile or run-time errors in simulation:
xcelium> run xmsim: *W,RNQUIE: Simulation is complete. xcelium> exit
Synthesis tools, however, report errors and warnings:
# Siemens Precision #------------------- # Info: [44522]: Root Module my_design: Pre-processing... # Error: [46778]: "/home/runner/design.sv", line 21: Blocking assignment on a non-blocking signal(q). Skipping compilation of this module. Continuing ... # Warning: [45383]: Module my_design: This module cannot be compiled because it contains non-RTL constructs. # Info: [44536]: No modules were compiled in this run of RTLC, please check the logs for blackboxes or non-rtl constructs in the design. # Info: [44513]: Overall running time for compilation: 1.0 secs. # Error: [46259]: Design compilation failed, unsupported or non-rtl constructs detected in the following modules : # Info: [40000]: my_design # Info: [40000]: Please check the log for details pertaining to unsupported or non-rtl construct(s) # Error: [685]: Unable to elaborate design work.my_design in verilog.
[Synth 8-6157] synthesizing module 'my_design' [Synth 8-6090] variable 'q' is written by both blocking and non-blocking assignments, entire logic could be removed
The design simulated "fine" but cannot be built. SystemVerilog addresses this gap by adding hardware-specific procedural blocks that make intent explicit. The new blocks do not make the code above legal. They give every tool in the flow the same statement of intent, so problems like this are flagged consistently and earlier.
Syntax
always_comb begin // combinational logic, no sensitivity list
// blocking assignments (=)
end
always_latch begin // level-sensitive latch, no sensitivity list
if (enable) q = d;
end
always_ff @(posedge clk or negedge rst_n) begin // flip-flops, explicit edge events
// non-blocking assignments (<=)
end
always_comb | always_latch | always_ff | |
|---|---|---|---|
| Intended hardware | Combinational logic | Latches | Flip-flops |
| Sensitivity list | Inferred automatically | Inferred automatically | Written explicitly, one event control at the start |
| Runs once at time 0? | Yes | Yes | No, only on its events |
| Delays or waits inside? | Not allowed | Not allowed | Only the single event control at the start |
| Variables written here driven elsewhere? | Not allowed | Not allowed | Not allowed |
always_comb
The always_comb block states that the logic is combinational. You don't write a sensitivity list: the tool infers it from every signal the block reads. Temporary variables that are written before they are read inside the block are left out.
module alu_4_bit (
input logic [3:0] a,
input logic [3:0] b,
input logic [1:0] op_code,
output logic [3:0] result
);
// This is a combinational always block
always_comb begin
case (op_code)
2'b00: begin
result = a + b; // Addition
end
2'b01: begin
result = a - b; // Subtraction
end
2'b10: begin
result = a & b; // Bitwise AND
end
2'b11: begin
result = a | b; // Bitwise OR
end
endcase
end
endmodule
All four values of op_code are covered, so result is assigned on every path and the block is purely combinational. If one case item were missing, result would have to hold its old value for that code, which means a latch. Tools flag that as a mismatch with the always_comb intent.
Sensitivity to signals read inside functions
In Verilog, always @* includes the signals read directly in the block, including expressions passed as function arguments. It does not include signals that a called function reads internally, such as a module-level variable used inside the function body. So all signals a function uses must be passed in as arguments. If a function's signal usage changes, its definition and every call site must be updated together.
logic a, b, en, z;
// 'en' is read inside the function body; it is not passed as an argument
function automatic logic and_en (input logic x, y);
return x & y & en;
endfunction
always @* begin // sensitive to a and b only: a change on en is missed
z = and_en(a, b);
end
When en changes while a and b stay the same, the @* block does not wake up, and z keeps a stale value in simulation. Synthesis builds the correct AND gate anyway, so simulation and gates disagree. SystemVerilog's always_comb removes this limitation. It is sensitive to signals read directly in the block and to signals read inside the functions it calls:
always_comb begin // sensitive to a, b and en (read inside the function)
z = and_en(a, b);
end
always_comb adds two more guarantees that always @* lacks:
- It runs once at time zero, after all
initialandalwaysblocks have started. Its outputs therefore reflect the inputs from the start. Analways @*block waits for an input to change, so its outputs can stay X if the inputs never change. - Single driver: variables on the left-hand side cannot be written by any other process. This prevents shared variables that would break the intended combinational behavior.
Quick Check
Q: A block assigns y = sel ? a : b;. Its inputs are set by an initial block at time 0 and never change afterwards. Why might always @* leave y at X while always_comb does not?
Show Answer
If the initial block assigns the inputs before the @* block has started waiting, the change is missed and the block never runs. always_comb runs once at time zero regardless, so y is computed from the initial values.
always_latch
The always_latch block is for logic that is meant to be a latch. It follows the same rules as always_comb: the sensitivity list is inferred, it runs once at time zero, and the variables it assigns cannot be driven by any other process.
module data_selector_with_latch (
input wire a_in,
input wire b_in,
input wire sel,
input wire enable,
output reg data_out
);
// This is the combinational part of the logic, a 2-to-1 mux
wire mux_out;
assign mux_out = sel ? a_in : b_in;
// This is the sequential part (the latch) that will hold the value
always_latch begin
if (enable) begin
data_out = mux_out;
end
end
endmodule
The difference from always_comb is the intent. Tools know you want a storage element, so they apply different checks. In latch logic, it is fine for an output not to be assigned on every path: data_out simply holds its value while enable is low. Written with always_comb, the same code would get a warning or error for the missing else, because combinational logic must define every output for every input.
always_ff
The always_ff block models synthesizable sequential logic such as flip-flops. Unlike always_comb and always_latch, it requires an explicit sensitivity list. The signals you list decide whether set and reset are synchronous or asynchronous.
module sync_counter_3bit (
input wire clk,
input wire reset_n, // Active-low reset
output reg [2:0] count
);
// This is the sequential always block, triggered on a clock edge
always_ff @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
// Asynchronous reset: sets the count to 0 when reset_n is low
count <= 3'b000;
end else begin
// On the positive edge of the clock, increment the count
count <= count + 1'b1;
end
end
endmodule
Because reset_n is in the event list, the reset takes effect as soon as it falls, without waiting for a clock edge. That makes it asynchronous, despite the module name. A synchronous reset would list only posedge clk. The counter wraps from 7 back to 0.
always_ff enforces stricter rules to stay consistent with synthesis:
- It must contain exactly one event control, at the start of the block, and no other delays or waits.
- Variables assigned in it cannot be written by any other process.
- Synthesis tools also expect every signal in the event list to be qualified with
posedgeornegedge, and tools may warn if the body does not follow flip-flop coding practice.
Where are these blocks used?
Most RTL coding guidelines in industry now require always_ff for every flip-flop and always_comb for every block of combinational logic, and forbid plain always in synthesizable code. Lint and synthesis tools then flag accidental latches and multiple drivers automatically, which catches many bugs before simulation even starts.
Using These Blocks in Verification
These blocks describe hardware, so testbench stimulus still uses initial, always and forever. always_comb is still handy in verification code for small combinational models, for example deriving an expected parity bit inside an interface or a reference model. Its time-zero evaluation and complete sensitivity list keep the model in step with the DUT from the first cycle.
Common Beginner Mistakes
Mistake #1: Incomplete assignments in always_comb
Wrong: always_comb if (en) y = a;
Right: always_comb begin y = '0; if (en) y = a; end
Why? When en is 0, y must keep its old value, which needs a latch. That contradicts always_comb, and tools report it. Assign a default at the top of the block. If you really want a latch, use always_latch.
Mistake #2: Driving one variable from two blocks
Assigning state in two different always_ff blocks, such as one for reset and one for normal operation, is a compile error. Why? Each variable written by always_comb, always_latch or always_ff must have exactly one driving process. With plain always, this mistake would only show up as a race in simulation or as a synthesis error.
Mistake #3: A level-sensitive event list in always_ff
Wrong: always_ff @(clk or rst_n)
Right: always_ff @(posedge clk or negedge rst_n)
Why? Flip-flops respond to edges. Without posedge or negedge, synthesis cannot map the block to flip-flops and reports an error.
Practice Exercise
Challenge: Counter with enable
Write a 4-bit counter with an active-low asynchronous reset rst_n and an enable en. Compute the next count value in an always_comb block and register it in an always_ff block.
Hint
Give the next-value signal a default of "hold the current count" in always_comb, so it is assigned on every path. The always_ff block only resets or copies the next value.
Solution
module counter_en (
input logic clk,
input logic rst_n,
input logic en,
output logic [3:0] count
);
logic [3:0] count_next;
// Next-state logic: purely combinational
always_comb begin
count_next = count; // default: hold (no latch)
if (en)
count_next = count + 4'd1; // increment when enabled
end
// State register: flip-flops with asynchronous active-low reset
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= '0;
else count <= count_next;
end
endmodule
Each variable has exactly one driver: count_next is driven by always_comb and count by always_ff. This split between next-state logic and the register is the standard RTL style for counters and FSMs.
Summary
In this tutorial, you learned:
always_comb,always_latchandalways_ffstate your hardware intent so tools can check italways_combinfers a complete sensitivity list, including signals read inside called functions, which@*missesalways_combandalways_latchboth run once at time zero;always_ffruns only on its edge events- Variables written in any of the three blocks cannot be driven by another process
always_fftakes exactly one event control at the start, with edge-qualified signals for synthesis- Assign defaults in
always_combto avoid unintended latches