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
assignfor a combinational signal that is one expression. - Use
always @(*)for combinational logic that wantsif/elseorcase. - 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;
endPoints 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 —
regin Verilog-2001,logicin SystemVerilog.reghere 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
wirefrom analwaysblock. A procedural block can only assign a variable. Either change the declaration or useassign. - Listing signals manually in the sensitivity list.
always @(a or b)that forgetscsimulates differently from the hardware it synthesises to. Use@(*). - Reading
regas "register". It describes how the signal is assigned, not what hardware it becomes. Aregassigned insidealways @(*)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.