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 always block leaves tools guessing about your intent
  • How always_comb infers its sensitivity list, and how that improves on @*
  • When to use always_latch and always_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:

Output
xcelium> run
xmsim: *W,RNQUIE: Simulation is complete.
xcelium> exit

Synthesis tools, however, report errors and warnings:

Output
# 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.
Output
[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_combalways_latchalways_ff
Intended hardwareCombinational logicLatchesFlip-flops
Sensitivity listInferred automaticallyInferred automaticallyWritten explicitly, one event control at the start
Runs once at time 0?YesYesNo, only on its events
Delays or waits inside?Not allowedNot allowedOnly the single event control at the start
Variables written here driven elsewhere?Not allowedNot allowedNot 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 initial and always blocks have started. Its outputs therefore reflect the inputs from the start. An always @* 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 posedge or negedge, 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_latch and always_ff state your hardware intent so tools can check it
  • always_comb infers a complete sensitivity list, including signals read inside called functions, which @* misses
  • always_comb and always_latch both run once at time zero; always_ff runs only on its edge events
  • Variables written in any of the three blocks cannot be driven by another process
  • always_ff takes exactly one event control at the start, with edge-qualified signals for synthesis
  • Assign defaults in always_comb to avoid unintended latches
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5