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.
- Cycle 3
The last cycle of the test:
enis 1,selis 3,yis 1000. Everything looks right. - Cycle 4
enfalls. Theifis false and no statement assignsy, soykeeps 1000. Peripheral 3 stays selected. - Cycle 5
selchanges to 1 whileenis still 0. Nothing changes: a held value does not follow its inputs. That is the definition of a latch. - Cycle 6
enrises andyfollowsselagain: 0010. - Cycle 7
enfalls andyholds 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.
- 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
- data 1000: 1 << sel → latch x4
- data 1000: latch x4 → y[3:0]
- 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
- call 1000: en → latch x4
- 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.
| Design | Cells | Of them latches | Levels of logic |
|---|---|---|---|
the decoder with no value for en = 0 (always @*) | 8 | 4 | 1 |
the same block as always_comb | rejected: the check pass reports a logic loop on every bit of y (next section) | ||
the decoder with else y = 0 | 6 | 0 | 2 |
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.
| Block | What it claims | Yosys 0.70 on the decoder with no else | Verilator 5.052 lint |
|---|---|---|---|
always @* | nothing | builds the four latches; the Gates tab reports 4 latch cell(s) inferred: $_DLATCH_P_ | the same %Warning-LATCH for y |
always_comb | no memory | rejected: 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_latch | a latch | builds the four latches; the Gates tab counts them and says so | no 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
Assign first, then decide
Give the output a default at the top of the block (
y = 4'b0000;) and let theiforcaseoverwrite it. Every path now assigns y, and the default is the value a reader expects when nothing else applies.Or complete the decision
Add the
elsebranch, or thedefaultarm of acase. This reads well when the two outcomes are equally important.Check what you did not write
A
caseon a 2-bit value with three arms has a fourth path. Acaseon an enumerated state has the states the enum does not name.defaultcovers all of them.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.
| Port | Direction | Width | Meaning |
|---|---|---|---|
| en | input | 1 | enable |
| sel | input | [1:0] | select |
| y | output | [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
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.
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.
Do
always @*andalways_combbehave 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.
Synthesize the design to measure its cells and inspect the mapped gates.
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 outputOriginal 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
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 @*fromalways_combhere: both hold the value. Synthesis builds the latch foralways @*and refuses thealways_combversion; 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_latchwhen a latch is what you want, so Lint stays quiet and the next reader knows; the Gates tab still counts every latch.