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

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations35
    • SystemVerilog Data Types
    • Arrays: Packed, Dynamic, Queues & Associative
    • Procedural Code & Scheduling
      • always_comb, always_ff, always_latch
      • Blocking vs Nonblocking Assignments
      • Tasks vs Functions
      • The Event Scheduler & Stratified Regions
      • initial, final & Startup Ordering
      • Loops & Flow Control
    • Operators, Expressions & Width Rules
    • Packages, Typedefs & Compilation
    • Interfaces, Modports & Clocking Blocks
  • Object-Oriented SystemVerilog for Verification35
  • Constraint Randomization — Interview-Critical Deep Dive66
  • SystemVerilog Assertions — Interview-Critical Deep Dive63
  • Functional Coverage in SystemVerilog28
  • Testbench Architecture in SystemVerilog38
  • Advanced Language & Integration30
  • Senior & Interview Preparation34
  • Principal-Level Verification28
Loading lesson

‹Procedural Code & Scheduling
  1. Topics
  2. SystemVerilog
  3. SystemVerilog Language Foundations
  4. Procedural Code & Scheduling
  5. always_comb, always_ff, always_latch

Part 1 · Language Foundations · Intermediate

always_comb, always_ff, always_latch

Intent-checked always blocks, latch inference with the default-first fix, and a testbench.

On this page

    Why this matters

    Synthesis built a latch that nobody wrote, and simulation never showed it. One case statement missed a value, so the output had to remember its old value on that path.

    After this lesson you can: pick always_comb, always_ff or always_latch for a block, explain what each checks, and write combinational logic that cannot infer a latch.

    Contracts instead of plain always

    This lesson builds on Verilog's always @* and case; RTL Foundations covers them if they are new.

    A plain always @(...) block carries no declared intent. The same construct models flops, latches, and combinational logic, so a forgotten signal or a missing else silently mismatches synthesis.

    The specialized blocks declare intent that tools verify. always_comb infers sensitivity from every variable read in the body, including reads inside called functions, and it executes once at time zero. always_ff takes one edge-sensitive event-control statement, which may list two edges such as posedge clk or negedge rst_n. always_latch marks an intentional latch so reviewers know it is deliberate.

    Block

    Models

    Rule (and lint guideline)

    always_comb

    Combinational logic

    Full sensitivity including functions, runs once at time zero; blocking assignments (guideline, lint-checked)

    always_ff

    Flip-flops

    One edge-sensitive event control; nonblocking assignments (guideline, lint-checked)

    always_latch

    Intentional latches

    Declares the latch is wanted; blocking assignments (guideline, lint-checked)

    Latch inference and the default-first fix

    Combinational logic must assign its outputs on every path through the block. A missed branch forces the output to hold its previous value, which is unclocked storage: a latch.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // snippet: missing branch holds state, which is a latch
    always @* begin
      case (state)
        2'b00: grant = req[0];
        2'b01: grant = req[1];
        2'b10: grant = req[2];
      endcase
    end

    Assigning a default value first closes every path. A default arm inside the case works equally well.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // snippet: default first, so every path assigns grant
    always_comb begin
      grant = 1'b0;
      case (state)
        2'b00: grant = req[0];
        2'b01: grant = req[1];
        2'b10: grant = req[2];
      endcase
    end

    What tools enforce

    Some checks are mandatory and some are lint quality. Required by the LRM: variables written in always_comb, always_latch or always_ff may not be written by any other process, and always_ff allows exactly one edge-sensitive event control. Not every simulator enforces the first rule, so lint for it too.

    Latch detection inside always_comb is commonly flagged but not guaranteed everywhere. always_comb does not prevent latches; it makes them detectable, and the default-assignment habit is what prevents them.

    Common pitfall

    Writing an always_comb output from a second process is illegal under the LRM, even where a tool lets it through. Driving a latch with a nonblocking assignment, or combinational logic with mixed assignment kinds, also draws tool complaints.

    Interview

    “Does always_comb prevent latches?”

    No. It makes incomplete assignments detectable and reportable. The default-assign-first habit is what prevents the latch.

    Check yourself A case inside always_comb covers three of four state values with no default and no prior assignment. What does synthesis build for the missing value?

    A latch. The output must hold its old value on that path, which is unclocked storage.

    Check yourself An always @* block calls a function that reads a signal. What can go wrong in simulation?

    The signal is missing from the inferred sensitivity list. always_comb includes function-internal reads; always @* does not.

    Testbench

    Predict first In the bench below, req is 3'b101 and state steps 0, 1, 2, 3. At state 3, what do grant_bad and grant_good show?

    grant_bad still shows 1, the value it took at state 2 (req[2]): no branch assigns it, so it holds, which is a latch. grant_good shows 0, its default.

    A latch-prone decoder next to its default-first fix, checked for all four state values.

    latch_demo.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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    module latch_demo (
      input logic [1:0] state,
      input logic [2:0] req,
      output logic grant_bad,
      output logic grant_good
    );
      always @* begin
        case (state)
          2'b00: grant_bad = req[0];
          2'b01: grant_bad = req[1];
          2'b10: grant_bad = req[2];
        endcase
      end
      always_comb begin
        grant_good = 1'b0;
        case (state)
          2'b00: grant_good = req[0];
          2'b01: grant_good = req[1];
          2'b10: grant_good = req[2];
        endcase
      end
    endmodule
    
    module tb_latch_demo;
    
      timeunit 1ns; timeprecision 1ps;
      logic [1:0] state;
      logic [2:0] req;
      logic grant_bad, grant_good;
      latch_demo dut (.*);
      int errors = 0;
      initial begin
        req = 3'b101;
        for (int s = 0; s < 4; s++) begin
          state = s[1:0]; #1;
          if (s < 3) begin
            if (grant_good !== req[s]) errors++;
          end else begin
            if (grant_good !== 1'b0) errors++;
          end
        end
        if (grant_bad !== req[2]) errors++;
        if (errors != 0) $fatal(1, "FAIL: grant outputs look wrong for some state");
        $display("PASS: default-first grant is right for all 4 states and the bare case holds its old value");
        $finish;
      end
    endmodule
    Terminal
    PASS: default-first grant is right for all 4 states and the bare case holds its old value

    Remember

    • always_comb is sensitive to everything it reads, including inside functions, and runs once at time zero.

    • A path that does not assign an output makes a latch; always_comb only makes it visible.

    • Assign a default first, or give every case a default arm.

    Rule of thumb: every path through combinational logic assigns every output.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. Icarus 13 (-g2012) runs them too, with the same output.

    Not every simulator rejects a second process writing an always_ff variable: Verilator 5.048 accepts an initial block doing it.

    Related topics

    • Next: Blocking vs Nonblocking Assignments

    Previous

    19 / 363

    Next: Blocking vs Nonblocking Assignments

    Up next

    Up nextBlocking vs Nonblocking Assignments

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Tasks vs Functions
    • The Event Scheduler & Stratified Regions
    • initial, final & Startup Ordering