Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
    • Digital Logic for RTL Engineers
    • Number Systems for RTL Engineers
    • Verilog Fundamentals
      • Modules and Ports
      • Wire vs reg
      • assign vs always
      • The Two Process Templates
      • Blocking vs Non-blocking
      • From Code to Hardware
    • Verilog Operators and Types
    • SystemVerilog for RTL
    • Simulation and Testbenches
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

←Verilog Fundamentals
  1. Topics
  2. RTL Design
  3. RTL Foundations
  4. Verilog Fundamentals
  5. assign vs always
On this page

    Part 1 of 9, lesson 15 of 36Part 1 of 9/Foundations/Lesson 15 of 36Beginner

    assign vs always

    Continuous assignments suit single expressions, procedural blocks suit decisions, and the sensitivity list must stay complete.

    A hand-written sensitivity list that leaves out one input shows stale values in simulation, while synthesis ignores the list and builds a normal mux. So the simulation and the chip disagree.

    After this lesson you can: choose between assign and a combinational always block, and write a complete combinational block with @(*) or always_comb.

    Two ways to describe combinational logic

    Continuous assignment (assign) means the net always equals the expression. A procedural block (always @(*)) computes a variable with if, case, and temporaries.

    Both can describe the same hardware. The table summarizes when each fits.

    mux_assign.v
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    module mux_assign (input wire a, b, sel, output wire y);
      assign y = sel ? b : a;
    endmodule
    
    module mux_always (input wire a, b, sel, output reg y);
      always @(*) begin
        if (sel) y = b;
        else     y = a;
      end
    endmodule

    Use assign when…

    Use always @(*) when…

    The logic is a single expression or a short chain.

    You need if/else, case, or loops to express the logic readably.

    You are driving a wire or logic output.

    You compute several related outputs together from one decision.

    You want the dependency to be obvious.

    You use temporary variables that make the logic clearer.

    The sensitivity list

    A procedural block runs when a signal in its sensitivity list changes. Combinational logic must list every signal the block reads.

    Hand-written lists are error-prone. For the simple mux above, @(*) includes a, b and sel automatically. First see what happens when b is missing from a hand-written list; the function caveat comes after that example.

    The listing pairs a bad-sensitivity mux with a testbench that exposes it. The full code sits under Testbench below.

    With an incomplete list, simulation shows stale values but synthesis builds a normal mux, because synthesis ignores the list for combinational logic. Use always_comb, or @(*) with all dependencies visible.

    Rules for combinational blocks

    1. Use always @(*) (Verilog) or always_comb (SystemVerilog).

    2. Use blocking assignments (=).

    3. Assign every output on every path, or you infer a latch (next lesson).

    4. One block should drive each signal; do not assign the same signal from two blocks.

    Check yourself assign y = a & b; and always @(*) y = a & b; are equivalent. Which one declares y as wire and which as reg?

    assign drives a wire (or logic); the always block assigns a reg (or logic). The hardware is the same AND gate.

    Testbench

    A bad-sensitivity mux and the testbench that exposes its stale output.

    mux_bad_sens.v
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // bad-example: 'b' is missing from the sensitivity list
    module mux_bad_sens (input wire a, b, sel, output reg y);
      always @(a or sel) y = sel ? b : a;
    endmodule
    
    module tb_bad_sens;
      reg a, b, sel;
      wire y;
      mux_bad_sens dut (.a(a), .b(b), .sel(sel), .y(y));
      initial begin
        a = 0; b = 0; sel = 1; #1;
        $display("b=0  y=%b  (correct)", y);
        b = 1; #1;                       // b changes, but the block is not sensitive to b
        $display("b=1  y=%b  (should be 1, stale value kept)", y);
        $finish;
      end
    endmodule
    Diagram
    b=0  y=0  (correct)
    b=1  y=0  (should be 1, stale value kept)

    Advanced: function reads and sensitivity

    Verilog-2001 @(*) includes directly read expressions and function arguments, but not a module-level signal read only inside a function. SystemVerilog always_comb includes reads inside called functions too. Pass every dependency explicitly when using @(*). In the test below, changing only mask leaves y_hidden stale; the two fixes update. Return to this caveat after learning functions if the syntax is new.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    module tb_function_sensitivity;
      logic a = 0, mask = 1;
      logic y_hidden, y_comb, y_explicit;
      int errors = 0;
    
      function automatic logic hidden_mask(input logic x);
        return x & mask;
      endfunction
      function automatic logic explicit_mask(input logic x, m);
        return x & m;
      endfunction
    
      always @(*) y_hidden = hidden_mask(a); // intentional contrast: mask is not an argument
      always_comb y_comb = hidden_mask(a);
      always @(*) y_explicit = explicit_mask(a, mask);
    
      initial begin
        #1 a = 1; #1;
        if ({y_hidden, y_comb, y_explicit} !== 3'b111) errors++;
        mask = 0; #1;
        if (y_hidden !== 1'b1) errors++;       // stale until a changes
        if ({y_comb, y_explicit} !== 2'b00) errors++;
        a = 0; #1;
        if ({y_hidden, y_comb, y_explicit} !== 3'b000) errors++;
        mask = 1; #1;
        a = 1; #1;
        if ({y_hidden, y_comb, y_explicit} !== 3'b111) errors++;
        if (errors == 0) $display("PASS: function sensitivity distinguishes hidden and explicit dependencies");
        else $display("FAIL: function-sensitivity errors=%0d", errors);
        $finish;
      end
    endmodule
    • Use @(*) or always_comb, blocking assignments, and assign every output on every path.

    • An incomplete sensitivity list gives stale simulation but ordinary synthesised hardware.

    • @(*) misses a module signal read only inside a called function; always_comb does not.

    Rule of thumb: never write a sensitivity list for combinational logic by hand.

    • Interview: run a mock interview on this topic

    Related topics

    • The two process templates

    • Latch inference

    Previous

    15 / 350

    Next: The Two Process Templates

    Up next

    Up nextThe Two Process Templates

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Modules and Ports
    • Blocking vs Non-blocking
    • From Code to Hardware