Verilog · lesson 5

Combinational Logic

assign versus always @*, a 4-to-1 multiplexer built both ways, and a real simulated demonstration of the classic incomplete-sensitivity-list bug.

10 min read · Free, no login

Two ways to describe the same logic

Combinational logic โ€“ the circuits from the Digital Electronics path's fourth lesson, whose output depends only on the current inputs โ€“ can be written in Verilog two different ways, and this course uses both, for different reasons. assign describes a single continuous expression driving one wire, exactly as lesson 2's half adder did. An always @* block describes the same kind of logic procedurally, with if/case and multiple statements, driving a reg. Neither is more "correct" than the other; assign reads better for a short expression, always @* reads better once the logic needs branching or a multi-way choice.

A 4-to-1 multiplexer, written procedurally

Recall from the Digital Electronics path's fourth lesson that a multiplexer routes one of several data inputs to a single output based on select lines. A 2-input mux is a one-line job for the conditional operator from the previous lesson; a 4-input mux reads far more clearly as a case statement.

mux4.v
module mux4 (
    input  wire [3:0] d0, d1, d2, d3,
    input  wire [1:0] sel,
    output reg  [3:0] y
);
    always @* begin
        case (sel)
            2'b00: y = d0;
            2'b01: y = d1;
            2'b10: y = d2;
            2'b11: y = d3;
        endcase
    end
endmodule
sel=00 y=1
sel=01 y=2
sel=10 y=3
sel=11 y=4

Every select value correctly routes its corresponding input through, matching the mux truth table exactly. Running this module through Yosys's synthesis flow (read_verilog; synth; stat) confirms what the code is meant to describe: the resulting netlist is built entirely from ordinary combinational gates (AND, NAND, NOR, and similar), with no flip-flop and no latch anywhere in it โ€” pure combinational logic, exactly as intended.

Why always @*, and not a hand-written list

Older Verilog required a procedural block's trigger list to be written out by hand: always @(d0 or d1 or d2 or d3 or sel). This works, but only if the list is complete โ€” and a list a designer edits by hand is a list a designer eventually forgets to update when a new input is added to the logic above it. always @* (standard since Verilog-2001) tells the tool to compute the sensitivity list automatically from every signal the block actually reads, which is both shorter to write and immune to this specific mistake by construction. Every example in this course uses always @* for combinational logic; a hand-written sensitivity list appears only in lesson 7, for sequential logic, where the list is deliberately short (just the clock, and sometimes a reset) rather than exhaustive.

What an incomplete sensitivity list actually does

It is worth seeing the old-style mistake happen once, in a real simulation, rather than only being told to avoid it. Here is a two-input mux with a deliberately broken, hand-written sensitivity list that is missing its two data inputs.

bad_mux2.v -- do not write this
module bad_mux2 (input wire a, input wire b, input wire sel, output reg y);
    always @(sel) begin   // BUG: a and b are missing from the list
        y = sel ? a : b;
    end
endmodule
a=0 b=1 sel=1 ybad=0 (correct)
a changed to 1, sel unchanged, ybad=0 (STALE -- should now be 1)

With sel = 1, the mux should be passing a straight through to y. The first line is correct by coincidence, because a happened to equal 0 at the moment sel last changed. When a then changes to 1 with sel held steady, a real combinational circuit would update y immediately โ€” but this always block only re-evaluates when sel itself changes, since that is the only signal in its trigger list. The simulated output of y is frozen at its old, now-wrong value, which is exactly the kind of bug that can simulate correctly for an entire test suite that never happens to change a while sel is held constant, and then disagree with the real, always-reactive hardware the moment it is synthesized. This is precisely why always @* exists, and precisely why this course never writes a sensitivity list by hand for combinational logic.

Common mistakes

Declaring the output of an always @* block as wire instead of reg is the first โ€” the fix is always to change the declaration, per lesson 3's rule, never the logic. The second, covered in full in the very next lesson, is leaving a case or if without a branch for every possible input value โ€” mux4's case (sel) above only avoids this because sel is a 2-bit signal with all four possible values listed explicitly; leaving even one combination unhandled changes what gets synthesized. The third is assuming assign and always @* can target the same signal โ€” they cannot; a given wire is driven by exactly one continuous assignment or one procedural block, never a mix, and a synthesis tool will reject a signal driven both ways.

Interview questions

When would you choose always @* over assign for combinational logic? assign reads most clearly for a single continuous expression. always @* reads more clearly once the logic needs a case statement, an if/else if chain, or more than one statement โ€” both describe pure combinational logic when written correctly; the choice is about readability, not about what gets synthesized.

What goes wrong with an incomplete, hand-written sensitivity list? The procedural block only re-evaluates when one of the listed signals changes. Any signal the block reads but that was left out of the list can change without the block re-running, so the output holds a stale value until some listed signal happens to change too โ€” a mismatch between simulation and the real, always-reactive combinational hardware. always @* removes the problem by computing the list automatically.

Can the same signal be driven by both an assign statement and an always block? No. Each signal needs exactly one driver in that sense โ€” one continuous assignment, or one procedural block โ€” and most tools flag multiple drivers of this kind as an error.

Key takeaways

  • assign and always @* both describe combinational logic; choose based on whether the logic is a single expression or needs branching, not based on what synthesizes.
  • A signal assigned inside always @* must be declared reg; a given signal is driven by exactly one of assign or a procedural block, never both.
  • always @* computes its own sensitivity list from every signal the block reads โ€” this is why this course never writes one by hand for combinational logic.
  • A hand-written sensitivity list missing a signal produces a block that only re-evaluates on the listed signals, leaving the output stale when an unlisted input changes โ€” correct behavior by coincidence in some tests, wrong behavior, and a simulation/synthesis mismatch, in general.