SYSTEMVERILOG SERIES · SV-09A

SystemVerilog Series — SV-09a: Processes — always_comb, always_ff, always_latch & Continuous Assignments — VLSI Trainers
SystemVerilog Series · SV-09a

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 else in 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

  1. Automatic sensitivity list — computed from the expressions read inside the block (and inside any functions called from the block). You never write @(...).
  2. Automatic time-0 execution — the block runs once before any simulation events, ensuring outputs are consistent with inputs from the start.
  3. 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.
  4. No blocking timing controls#N, @event, and wait are illegal. Intra-assignment delays (nonblocking form) and delta delays are allowed.
fork…join is also illegal inside always_comb. The block must be purely sequential — no concurrent sub-processes. For the same reason, no task calls that contain timing controls 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:

  1. Variables declared within the block or within called functions (local variables, not signals).
  2. Any expression that is also written within the block (the LHS values don’t sensitise the block to their own writes).
Sensitivity list construction — example
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)
In the list: a, b, c, d — signals read that are not locally declared or locally written
Excluded: temp (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.

Featurealways_combalways @(*)
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
Prefer always_comb over always @(*) for all new RTL. The function-internal sensitivity fix alone is worth the switch. Before SV, if a function read a global signal and you forgot to add it to your 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
Latches are intentional here, not accidental. The whole point of 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 #N inside 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.
always_ff and nonblocking assignments: Use non-blocking (<=) 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

Featurealways_combalways_latchalways_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 assign must not have an initialiser in its declaration.
  • A variable being driven by assign must 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 to logic vs always_comb: Both 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 @(...) or initial

always_comb sensitivity: what’s included vs excluded

Signal typeIn 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 = expr for any variable type (logic, bit, real, int, etc.).
  • Only one assign per 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.
Coming next: SV-09b covers the rest of Section 9 — fork-join variants (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).

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top