always_comb and the latch nobody asked for

Spot the incomplete assignment that turns combinational logic into a latch, see why one tool stops you and another does not, and write the default that removes it.

Lesson
R1.3
Time
25 min
Runs on
Your browser
Check
5 questions

A 2-to-4 decoder drove the chip-select lines of four peripherals, and its tests walked through the four select codes with the enable high. In the lab the enable went low and a peripheral stayed selected. Nothing in the code said what the outputs should be when the enable was 0, so synthesis built four latches to hold the old values. The testbench never dropped the enable, so it never saw them.

The decoder with the enable lowOne (en, sel) pair per cycle. The first four cycles are the test the team ran. From cycle 4 the enable is 0 and y should be 0000, but it keeps the last value it was given.
01234567clkensel01231y00010010010010000010
  1. Cycle 3

    The last cycle of the test: en is 1, sel is 3, y is 1000. Everything looks right.

  2. Cycle 4

    en falls. The if is false and no statement assigns y, so y keeps 1000. Peripheral 3 stays selected.

  3. Cycle 5

    sel changes to 1 while en is still 0. Nothing changes: a held value does not follow its inputs. That is the definition of a latch.

  4. Cycle 6

    en rises and y follows sel again: 0010.

  5. Cycle 7

    en falls and y holds 0010. Peripheral 1 stays selected this time. Which line is stuck depends on what came before: the mark of memory.

Measured on the bench. The simulation holds the value in exactly the way the latch would.

An assignment that is missing is a memory

Combinational logic has no memory: the outputs are a function of the inputs now. A procedural block describes combinational logic only if every variable it writes gets a value on every path through the block. On a path that assigns nothing, the language says the variable keeps its old value. Keeping a value is memory. Synthesis builds the cheapest memory that fits: a latch, a storage element that follows its input while an enable is high and holds when the enable is low. Here the enable of the latch is en itself.

No value when en is 0

always @* begin
  if (en) y = 4'b0001 << sel;
end

Four latches, enabled by en. While en is 0, y holds its last value.

A value on every path

always_comb begin
  if (en) y = 4'b0001 << sel;
  else    y = 4'b0000;
end

Gates only, no latch. The output is a function of en and sel.

What synthesis built from the incomplete blockThe shift builds the one-hot value from sel. Because y has no value when en is 0, the result goes through a latch whose enable is en. While en is 1 the latch follows the shift; when en falls it holds.
What synthesis built from the incomplete blockThe shift builds the one-hot value from sel. Because y has no value when en is 0, the result goes through a latch whose enable is en. While en is 1 the latch follows the shift; when en falls it holds. manager sel[1:0] input manager en input bus 1 << sel four gates: theone-hot value bridge latch x4 enable = en; holdswhen en is 0 completer y[3:0] output selectdataenableheld or following
  1. Enable high

    en is 1: the latch is open, y follows the shift. Every test cycle looked like this.

    Follow the calls, values and waits
    1. data 1000: 1 << sel → latch x4
    2. data 1000: latch x4 → y[3:0]
  2. Enable falls

    en is 0: the latch closes and keeps the last one-hot value. sel can change; nothing reaches y.

    Follow the calls, values and waits
    1. call 1000: en → latch x4
  3. With the else

    The fixed block has a value for en = 0, so the latch is gone: six gates decode en and sel, and y is 0000.

Read the diagram as text
  • sel[1:0]: input
  • en: input
  • 1 << sel: four gates: the one-hot value
  • latch x4: enable = en; holds when en is 0
  • y[3:0]: output
  • sel[1:0] → 1 << sel: select
  • 1 << sel → latch x4: data
  • en → latch x4: enable
  • latch x4 → y[3:0]: held or following

The latch is the direct picture of "keep the old value". Removing the hold from the code removes the cell from the netlist.

DesignCellsOf them latchesLevels of logic
the decoder with no value for en = 0 (always @*)841
the same block as always_combrejected: the check pass reports a logic loop on every bit of y (next section)
the decoder with else y = 0602

Measured with Yosys 0.70 on this flow. The fixed decoder is smaller: the four latches became gates.

Three process kinds, three answers

SystemVerilog gives a combinational block a name of its own. always_comb says "this block has no memory", and a synthesizer or a lint tool can check the claim. The old spelling always @* makes no claim, so a tool builds the latch and at most warns. always_latch says "this block is a latch", so a tool stays quiet. Simulation treats the first two the same way: it holds the variable when nothing assigns it. The difference exists only in the tools that read your intent. On this site the Lint button names the latch. The Synthesize button refuses the always_comb version: with no memory allowed, "keep the old value" becomes an output that feeds itself, and the check pass reports a logic loop.

BlockWhat it claimsYosys 0.70 on the decoder with no elseVerilator 5.052 lint
always @*nothingbuilds the four latches; the Gates tab reports 4 latch cell(s) inferred: $_DLATCH_P_the same %Warning-LATCH for y
always_combno memoryrejected: found logic loop in module dec2to4 for each bit of y, from the check pass%Warning-LATCH: Latch inferred for signal y (not all control paths of combinational always assign a value)
always_latcha latchbuilds the four latches; the Gates tab counts them and says sono warning

Measured. The same three lines of code, three different reactions.

So always_comb is not a style choice. It turns the mistake of this lesson from a latch that synthesis builds into a design that synthesis refuses, before any testbench runs. The bench below passed five of its eight cycles with the latch in place; Lint would have named y in under a second.

The default that removes it

  1. Assign first, then decide

    Give the output a default at the top of the block (y = 4'b0000;) and let the if or case overwrite it. Every path now assigns y, and the default is the value a reader expects when nothing else applies.

  2. Or complete the decision

    Add the else branch, or the default arm of a case. This reads well when the two outcomes are equally important.

  3. Check what you did not write

    A case on a 2-bit value with three arms has a fourth path. A case on an enumerated state has the states the enum does not name. default covers all of them.

  4. When you do want a latch

    Write always_latch. Lint stays quiet, the Gates tab still counts the latches, and the next reader knows the memory is there on purpose.

Design brief
PortDirectionWidthMeaning
eninput1enable
selinput[1:0]select
youtput[3:0]y[k] = 1 when en = 1 and sel = k; y = 0000 when en = 0
Latency
combinational: y follows en and sel in the same cycle
Parameters
Memory: none: no latch, no flop
Corner cases
en falling while sel holds; sel changing while en is 0

The lab below ships the decoder as the team wrote it. Run it and count the stale cycles. Then change always @* to always_comb and run again. The simulation prints the same lines, because it cannot tell the two apart. Press Lint and read %Warning-LATCH for y; press Synthesize and the flow refuses the design with the logic loops from the table. Then add the else: the stale count goes to 0, Lint is clean and the Gates tab shows six cells and no latch.

Asked in interviews
  1. What is latch inference and how do you avoid it?

    A combinational block that leaves a variable unassigned on some path keeps the old value, so synthesis builds a latch. Assign every output on every path: a default at the top, an else, a default arm. Write the block as always_comb so lint names the latch and synthesis refuses the block instead of building it.

  2. Does a missing else always make a latch?

    Only when the variable is not assigned on that path. If the variable got a default earlier in the same block, the missing else is harmless. The rule is about paths, not about the else keyword.

  3. Do always @* and always_comb behave differently in simulation?

    Almost none for synthesizable code: both run when an input changes. always_comb also runs once at time zero and lets tools check that the block has no memory. The difference that matters is in lint and synthesis, not in the waveform.

Try it

The first four lines are the test the team ran, and they are all correct. The bench then drops the enable: y should be 0000 and instead keeps 1000, then keeps it through a change of sel, then keeps 0010 after the second drop. Three stale cycles, each holding whatever came last.

Change always @* to always_comb and run: the same nine lines. Lint: %Warning-LATCH on y. Synthesize: refused, found logic loop in module dec2to4 for each bit of y. Now add else y = 4'b0000; after the if: zero stale cycles, Lint clean, and the Gates tab shows six cells and no latch.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: e0 en=1 sel=0 y=0001 e1 en=1 sel=1 y=0010 e2 en=1 sel=2 y=0100 e3 en=1 sel=3 y=1000 e4 en=0 sel=3 y=1000 stale e5 en=0 sel=1 y=1000 stale e6 en=1 sel=1 y=0010 e7 en=0 sel=1 y=0010 stale 3 cycles with a stale output

Original design · 8 cells · 0 flip-flops · 4 latches · 1 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic       en = 0;
  logic [1:0] sel = 0;
  logic [3:0] y;
  logic [3:0] want;
  int stale = 0;
  dec2to4 dut (.en(en), .sel(sel), .y(y));
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    want = en ? 4'b0001 << sel : 4'b0000;
    if (y !== want) stale++;
    $display("e%0d en=%b sel=%0d y=%b%s", edge_n, en, sel, y, y === want ? "" : "  stale");
  endtask
  initial begin
    // the test the team ran: enable high, every select code
    en = 1; sel = 0; tick();
    sel = 1; tick();
    sel = 2; tick();
    sel = 3; tick();
    // the cycles the team never ran: enable low
    en = 0;          tick();
    sel = 1;         tick();
    en = 1;          tick();
    en = 0;          tick();
    $display("%0d cycles with a stale output", stale);
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule

Check yourself

Predict the outputThe case statement names three of the four values of sel. The program sets sel to 2, then to 3, then prints y. What is the output line?
module tb;
  logic [1:0] sel;
  logic [3:0] y;
  always_comb
    case (sel)
      2'd0: y = 4'b0001;
      2'd1: y = 4'b0010;
      2'd2: y = 4'b0100;
    endcase
  initial begin
    sel = 2; #1;
    sel = 3; #1;
    $display("y=%b", y);
    $finish;
  end
endmodule
Spot the bugEach option is the body of an always_comb block for the decoder. The bench sets en = 1 and sel = 2, then drops en and checks that y is 0000. Which body fails?
ConceptThe decoder was changed from always @* to always_comb and the testbench still passed every test it passed before. Why?
Read the waveformIn the measured trace of the hook, in which cycle does y first show a value that the inputs do not justify?
The decoder, eight cycles
01234567ensel01231y00010010010010000010

One value per cycle, from cycle 0.

ConceptA designer wants a real level-sensitive latch, the enable latch of a clock-gating cell. Which block says so to the tools?

Remember

  • Combinational logic must assign every output on every path; a path that assigns nothing keeps the old value, and that memory becomes a latch in synthesis.
  • Simulation cannot tell always @* from always_comb here: both hold the value. Synthesis builds the latch for always @* and refuses the always_comb version; Lint names the latch either way.
  • Remove the latch with a default assignment at the top of the block, an else, or a default arm; the fixed decoder is no bigger than the broken one.
  • Write always_latch when a latch is what you want, so Lint stays quiet and the next reader knows; the Gates tab still counts every latch.