Skip to article

Combinational RTL

assign vs. always: Choosing Between Continuous and Procedural Logic

When to describe combinational hardware with a continuous assign statement and when a procedural always block reads better — plus what each one requires of the signal it drives.

3 min read · Updated


Verilog gives you two ways to describe the same combinational hardware. A continuous assignment (assign) states an expression once and lets it re-evaluate forever. A procedural block (always @(*)) states the same thing as a short sequence of decisions. Both synthesise to gates; neither is more "hardware" than the other.

The choice is about which one a human — you, three months from now, or a reviewer — can read and debug fastest.

The short rule

  • Use assign for a combinational signal that is one expression.
  • Use always @(*) for combinational logic that wants if/else or case.
  • Use always @(posedge clk) when the signal must remember something between clock edges. That is a different topic, not a style preference.

Continuous assignment

wire grant;
assign grant = request & ~busy;

The right-hand side is re-evaluated whenever anything it reads changes. The target must be a net — a wire in Verilog-2001, or logic in SystemVerilog — and the statement lives at module level, never inside a procedural block.

This is the clearest form when the whole decision fits on one line. It is also the form that reads worst when it does not.

Where a single assign stops helping

A single assign is a great fit for a one-line expression. As the number of conditions grows, chaining conditional (?:) operators inside one assign gets harder to read and harder to debug — each added condition nests one level deeper inside the last, and the failing case is buried somewhere in the middle of a line you cannot set a breakpoint on.

A procedural combinational block describes the exact same hardware, just written as separate readable lines: one decision per line instead of one long nested expression.

Procedural combinational block

reg  grant;
always @(*) begin
  grant = 1'b0;
  if (request && !busy)
    grant = 1'b1;
end

Points that trip people up:

  • @(*) means "re-evaluate whenever any signal read inside the block changes". You do not list the signals by hand; letting the tool infer them is what stops a stale sensitivity list from creating a simulation-versus-synthesis mismatch.
  • The target must be a variable — reg in Verilog-2001, logic in SystemVerilog. reg here does not mean a flip-flop. It only means "assigned procedurally".
  • Every output must be assigned on every path through the block, or you get an accidental latch. The default assignment on the first line above is what makes that impossible.

Side by side

Form Re-evaluates Target type Best for
assign continuously wire / logic one-expression combinational signals
always @(*) on any input change reg / logic conditional combinational logic, decoders, priority selection
always @(posedge clk) on the clock edge reg / logic registers, counters, state machines

Common mistakes

  • Driving a wire from an always block. A procedural block can only assign a variable. Either change the declaration or use assign.
  • Listing signals manually in the sensitivity list. always @(a or b) that forgets c simulates differently from the hardware it synthesises to. Use @(*).
  • Reading reg as "register". It describes how the signal is assigned, not what hardware it becomes. A reg assigned inside always @(*) is plain combinational logic.
  • Mixing assignment operators by habit. Blocking (=) belongs in combinational blocks; non-blocking (<=) belongs in clocked blocks. That rule is about assignment semantics, not about style.

There is no hardware difference between the first two forms. Pick the one that makes the decision you are describing obvious on the page.