Processes — always_comb, always_ff, always_latch & Continuous Assignments
The three specialised always blocks that communicate synthesis intent to tools, their differences from Verilog-2001 always @*, sensitivity list rules, and the relaxed continuous assignment rules that let assign drive variables as well as nets.
💡 Why Specialised always Blocks?
In Verilog-2001, all logic — combinational, latched, and sequential — is described with the same always keyword. The design intent (combinational? latched? flip-flop?) is communicated only by the sensitivity list and the structure of the code inside. Tools have to reverse-engineer the intent, which leads to two problems:
- Unintended latches: forget an
elsein a combinational block and synthesis infers a latch silently. The RTL passes simulation but generates unexpected hardware. - Simulation/synthesis mismatch: a Verilog
always @*sensitivity list is automatically derived but can differ subtly from what synthesis infers.
SystemVerilog adds three specialised always variants that make intent explicit. Both the simulator and synthesis tool can then verify that the code matches the declared intent and issue warnings when it does not.
always_comb
- Intent: combinational logic
- Auto sensitivity list
- Executes once at time 0
- No timing controls allowed
- Single writer enforced
always_latch
- Intent: latched logic
- Same sensitivity as always_comb
- Executes once at time 0
- Tool warns if not latch-like
- Single writer enforced
always_ff
- Intent: flip-flop / register
- Explicit sensitivity list required
- Exactly one event control
- No blocking timing controls
- Single writer enforced
▶ always_comb — Combinational Logic
Use always_comb for purely combinational logic — no memory, no clocks. The tool generates the sensitivity list automatically from the expressions read inside the block. You never write @(*).
// Basic: a simple combinational function always_comb y = a & b; // With begin...end for multiple statements always_comb begin sum = a + b; carry = (a & b) | (a & cin) | (b & cin); end // Intra-assignment delay is allowed (not a blocking timing control) always_comb d <= #1ns b & c; // legal — delta delay, nonblocking // FSM next-state logic typedef enum logic[1:0] {IDLE,FETCH,EXEC,DONE} state_t; state_t state, next_state; always_comb begin next_state = IDLE; // default — prevents latches unique case(state) IDLE: next_state = FETCH; FETCH: next_state = EXEC; EXEC: next_state = DONE; DONE: next_state = IDLE; endcase end
Four rules of always_comb
- Automatic sensitivity list — computed from the expressions read inside the block (and inside any functions called from the block). You never write
@(...). - Automatic time-0 execution — the block runs once before any simulation events, ensuring outputs are consistent with inputs from the start.
- Single writer — variables written on the left-hand side of assignments inside the block (including inside called functions) must not be written by any other process.
- No blocking timing controls —
#N,@event, andwaitare illegal. Intra-assignment delays (nonblocking form) and delta delays are allowed.
🔎 always_comb Sensitivity Rules
The implicit sensitivity list is built from the longest static prefix of each variable or select expression that is read within the block or in any called function. Two categories of reads are excluded:
- Variables declared within the block or within called functions (local variables, not signals).
- Any expression that is also written within the block (the LHS values don’t sensitise the block to their own writes).
always_comb begin int temp = 0; // local — NOT in sensitivity list temp = a + b; // reads a, b → in list; writes temp → excluded y = temp & c; // reads temp (local, excluded), reads c → in list z = y | d; // reads y (written here, excluded), reads d → in list end // Resulting implicit sensitivity list: @(a, b, c, d)
a, b, c, d — signals read that are not locally declared or locally writtentemp (locally declared), y (also written in this block), z (written here)Array indexing and sensitivity
reg [7:0] m [5:1][5:1]; integer i; // variable index localparam p = 3; always_comb out1 = m[1][i]; // longest static prefix = m[1] (because i is variable) // sensitivity: ALL of m[1][0..5] — the entire second dimension of row 1 always_comb out2 = m[p][1]; // longest static prefix = m[p][1] = m[3][1] (both constant) // sensitivity: ONLY m[3][1] — exactly one element always_comb out3 = m[i][1]; // longest static prefix = m (because i is variable at the first index) // sensitivity: ALL of m — the entire array
⇄ always_comb vs always @(*)
Both derive their sensitivity list automatically, but there are four important differences.
| Feature | always_comb | always @(*) |
|---|---|---|
| Auto-runs at time 0 | ✓ Yes — outputs consistent from t=0 | ✗ No — waits for first signal change |
| Sensitivity to function internals | ✓ Sensitive to signals read inside called functions | ✗ Only sensitive to function arguments, not internals |
| Multiple writers | ✗ Compile error — only one process may write each LHS variable | ✓ Permitted (though not recommended) |
| Tool checks behavior | ✓ Tool warns if behavior is not combinational (e.g. latch inferred) | ✗ No behavioral intent — no warnings |
always @(*) — function internals NOT in sensitivity list
function int get_val(int sel); return sel ? sig_a : sig_b; // sig_a, sig_b inside function endfunction always @(*) y = get_val(sel); // Sensitivity: @(sel) only! // Changes to sig_a or sig_b are NOT detected // → simulation/synthesis mismatch risk
always_comb — function internals IN sensitivity list
function int get_val(int sel); return sel ? sig_a : sig_b; // sig_a, sig_b inside function endfunction always_comb y = get_val(sel); // Sensitivity: @(sel, sig_a, sig_b) // Changes inside the function are properly tracked
always @(*) list manually, simulation would not re-execute when that signal changed — a classic source of RTL bugs that only manifest after synthesis. always_comb prevents this entirely.
📌 always_latch — Latched Logic
always_latch communicates that the block intentionally infers a latch. It behaves identically to always_comb in terms of sensitivity and time-0 execution — the only difference is that the tool checks for latched (not combinational) behavior and warns if the block looks purely combinational.
// Transparent latch: passes d through when ck is high, holds when ck is low always_latch if(ck) q <= d; // Sensitivity: @(ck, d) — auto-derived, same as always_comb // Latch inferred: q is only assigned when ck=1, holds when ck=0 // Tool CONFIRMS this is latch-like and does NOT issue a warning // Multiple latches in one block always_latch begin if(en) begin q1 <= d1; q2 <= d2; end end // Both q1 and q2 are latched on the same enable signal
always_latch is to signal to tools “yes, I know this infers a latch, and I want it to”. In contrast, a latch inferred inside always_comb is almost certainly a bug (a missing else clause), and the tool will warn. Real latches — SR latches, level-sensitive flip-flops — should use always_latch.
⏱ always_ff — Sequential Logic
always_ff is for synthesisable flip-flop (register) logic. Unlike always_comb and always_latch, you must provide an explicit event control list. The tool verifies the block represents sequential logic.
// Standard clocked register with synchronous reset always_ff @(posedge clk) begin if(rst) q <= '0; else q <= d; end // Asynchronous reset — both posedge clk AND posedge rst in sensitivity always_ff @(posedge clk or posedge rst_n) begin if(!rst_n) q <= '0; else q <= d; end // With iff: conditional clock gating expressed in the sensitivity always_ff @(posedge clk iff reset==0 or posedge reset) begin r1 <= reset ? 0 : r2 + 1; end // posedge clk only when reset==0; OR posedge reset (unconditional) // Multiple registers in one always_ff always_ff @(posedge clk) begin pipe_a <= data_in; // stage 1 pipe_b <= pipe_a; // stage 2 pipe_c <= pipe_b; // stage 3 end
always_ff rules
- Exactly one event control — one
@(...)in the block body, placed at the beginning. Multiple event controls are illegal. - No blocking timing controls — no
#Ninside the body. Intra-assignment delays (nonblocking form) are legal. - Single writer — variables assigned within the block (or in called functions) must not be written by any other process.
- Sensitivity is explicit — you write the clock and reset edges manually. No automatic derivation.
<=) for all assignments inside always_ff — this is the correct synthesis idiom for registered logic. Mixing blocking (=) assignments for registered outputs inside always_ff is a common mistake and will cause synthesis/simulation mismatches.
Common mistake — blocking in always_ff
// WRONG: blocking assignment inside always_ff always_ff @(posedge clk) begin pipe_a = data_in; // = instead of <= : simulation sees immediate update pipe_b = pipe_a; // pipe_b gets NEW pipe_a value — wrong pipeline behaviour! end // CORRECT: non-blocking assignments — all read old values, write simultaneously always_ff @(posedge clk) begin pipe_a <= data_in; // reads current data_in pipe_b <= pipe_a; // reads CURRENT pipe_a (before update) — correct! end
🔄 Three-Way Comparison
| Feature | always_comb | always_latch | always_ff |
|---|---|---|---|
| Design intent | Combinational logic | Level-sensitive latch | Edge-triggered register |
| Sensitivity list | Auto-derived (implicit) | Auto-derived (implicit) | Explicit (you write @(…)) |
| Runs at time 0 | Yes — always | Yes — always | No — waits for first edge |
| Number of event controls | Zero (none allowed) | Zero (none allowed) | Exactly one (required) |
| Blocking timing controls | Illegal | Illegal | Illegal |
| fork…join inside | Illegal | Illegal | Generally avoided |
| Multiple writers to same variable | Illegal | Illegal | Illegal |
| Tool warns if behavior doesn’t match | Yes — latch-like code warns | Yes — pure combinational warns | Yes — non-sequential code warns |
| Typical LHS assignment | Blocking (=) or nonblocking (<=) | Nonblocking (<=) preferred | Nonblocking (<=) required |
⚡ Continuous Assignments — SV Relaxation
In Verilog-2001, assign statements could only drive nets (wire, tri, etc.). SystemVerilog removes this restriction: assign can now also drive variables (logic, bit, reg, etc.).
Verilog-2001 — assign only drives nets
wire w; assign w = a & b; // OK — net reg r; // assign r = a & b; // ERROR in Verilog-2001 always @(*) // must use always for variables r = a & b;
SystemVerilog — assign drives nets AND variables
logic v; assign v = a & b; // OK — logic variable real r; assign r = 2.0 * PI * R; // OK — real variable! int i; assign i = counter[7:0]; // OK — int variable
Rules for driving variables with assign
- A net can be driven by multiple continuous assignments or a mixture of primitives and assignments (same as Verilog-2001).
- A variable can only be driven by one continuous assignment or one primitive output. Multiple assigns to the same variable are illegal.
- A variable being driven by
assignmust not have an initialiser in its declaration. - A variable being driven by
assignmust not have any procedural assignments to it in any always block or initial block.
// LEGAL: one assign to a logic variable logic v; assign v = a | b; // OK // ILLEGAL: two assigns to same variable // assign v = a; // ERROR — v already has one assign // ILLEGAL: assign + procedural to same variable logic u; assign u = x; // always @(posedge clk) u <= y; // ERROR — u driven by assign AND procedural // ILLEGAL: assign with initialiser in declaration // logic w = 1; // ERROR if w is later assigned with assign logic w; // OK — no initialiser assign w = y; // assign to real (very useful for analogue/mixed-signal modelling) real voltage; assign voltage = current * resistance; // continuously updated real
assign v = a & b and always_comb v = a & b produce the same simulation result for simple combinational expressions. The difference is that always_comb gets behavioral checking (tool warns if it doesn’t look combinational) and handles function-internal sensitivity. Use assign for simple wire-like expressions; use always_comb for anything with conditions, case statements, or function calls.
📋 Quick Reference
always block selection guide
- Purely combinational logic with no memory →
always_comb - Level-sensitive latch (intentional) →
always_latch - Edge-triggered flip-flop / register →
always_ff @(...) - Testbench behavioural code with timing →
always @(...)orinitial
always_comb sensitivity: what’s included vs excluded
| Signal type | In sensitivity list? |
|---|---|
| External signal read in the block | ✓ Yes |
| Signal read inside a called function | ✓ Yes (unlike always @*) |
| Variable declared inside the block (local) | ✗ No |
| Variable written inside the block | ✗ No |
Continuous assignment rules for variables
- SV allows
assign var = exprfor any variable type (logic, bit, real, int, etc.). - Only one
assignper variable — multiple assigns to the same variable are illegal. - No procedural assignments to a variable that has a continuous assign.
- No initialiser in the declaration of a variable that has a continuous assign.
join_any, join_none), automatic variables inside fork-join, wait fork, disable fork, and the process built-in class for fine-grained process control (sections 9.6–9.9).
