Writing RTL that synthesizes into the hardware you actually intended is one of the most important skills for a digital design engineer. A few lines of incomplete Verilog can be enough to create hardware that was never part of the original design plan.
One common example is unintended latch inference.
Latches are not inherently bad. They are legitimate storage elements and are used deliberately in certain designs. The problem occurs when a latch appears in RTL accidentally because a combinational block does not assign an output under every possible condition.
For beginners, latch inference can seem confusing because the RTL may look perfectly reasonable during simulation. The synthesis tool, however, interprets the missing assignment as a requirement to preserve the previous value. That behavior requires storage, so a latch is inferred.
This article explains why inferred latches occur, how to recognize the coding patterns that cause them, and practical ways to write cleaner, predictable RTL.
A latch is a level-sensitive storage element. Unlike a flip-flop, which normally updates at a clock edge, a latch can allow its output to follow its input while an enable condition is active and retain its previous value when that condition is inactive.
Consider this simple RTL:
always_comb begin
if (en)
y = data;
end
At first glance, this appears to mean:
When en is high, assign data to y.
But what should happen when en is low?
There is no assignment to y.
Therefore, the hardware must somehow remember the previous value of y. Synthesis can implement that behavior using a latch.
The corrected combinational version could be:
always_comb begin
if (en)
y = data;
else
y = 1’b0;
end
Now y receives a value whether en is 1 or 0. No storage is required.
Another common solution is to provide a default assignment:
always_comb begin
y = 1’b0;
if (en)
y = data;
end
The second style becomes particularly useful when a combinational block contains several conditions.
The basic reason is simple: an output is not assigned on every possible execution path.
In combinational logic, the output should be completely determined by the current inputs. If the RTL says that the output should change under some conditions but does not say what should happen under other conditions, the synthesizer has to preserve the previous value.
This is why incomplete if, if-else, and case statements are common sources of inferred latches. Current FPGA synthesis documentation also specifically identifies incomplete IF and CASE coverage as a common cause of unintended latch generation.
The easiest question to ask during RTL review is:
“For every possible combination of inputs, does every combinational output get a value?”
If the answer is no, investigate for latch inference.
Consider:
always @(*) begin
if (sel)
out = a;
end
When sel is 1:
out = a
But when sel is 0, out is not assigned.
The synthesizer interprets the missing assignment as:
if sel = 1 → update out
if sel = 0 → keep previous out
That “keep the previous value” behavior is storage behavior.
Use an explicit else:
always @(*) begin
if (sel)
out = a;
else
out = b;
end
Or use a default assignment:
always @(*) begin
out = b;
if (sel)
out = a;
end
Both describe combinational behavior.
The default-assignment approach is often easier to maintain because adding another condition later is less likely to introduce an uncovered path.
Latch problems become harder to spot when conditions are nested.
For example:
always_comb begin
if (mode) begin
if (enable)
out = data;
end
else begin
out = 1’b0;
end
end
The outer if has an else, so the code may initially appear complete.
But consider:
mode = 1
enable = 0
There is still no assignment to out.
Therefore, a latch can be inferred.
A safer coding style is:
always_comb begin
out = 1’b0;
if (mode) begin
if (enable)
out = data;
end
end
The default value covers the path where mode and enable do not activate the assignment.
This is an important lesson: do not judge completeness only by looking for an else. Trace every possible path through the logic.
Incomplete case statements can also produce latches.
For example:
always_comb begin
case (sel)
2’b00: out = a;
2’b01: out = b;
2’b10: out = c;
endcase
end
What happens when:
sel = 2’b11
There is no assignment to out.
The synthesizer may therefore infer storage.
A safer implementation is:
always_comb begin
case (sel)
2’b00: out = a;
2’b01: out = b;
2’b10: out = c;
default: out = 1’b0;
endcase
end
Alternatively, assign a default value before the case:
always_comb begin
out = 1’b0;
case (sel)
2’b00: out = a;
2’b01: out = b;
2’b10: out = c;
endcase
end
The default-assignment technique is particularly useful when several outputs are controlled by the same combinational block.
If the design uses SystemVerilog, always_comb is generally preferable to manually maintaining sensitivity lists.
For example:
always_comb begin
y = a & b;
end
The construct explicitly communicates that the block is intended to represent combinational logic. SystemVerilog tools can also perform additional checks associated with always_comb, including identifying behavior that does not satisfy the intended combinational semantics.
Compare that with older Verilog-style coding:
always @(a or b) begin
y = a & b;
end
The manual sensitivity list becomes another potential maintenance problem if the block later starts depending on another signal.
Using:
always @(*)
is much safer than manually listing signals, but when SystemVerilog is available, always_comb makes design intent clearer.
A very practical RTL coding habit is to assign default values at the beginning of every combinational process.
For example:
always_comb begin
next_state = IDLE;
ready = 1’b0;
valid = 1’b0;
if (condition) begin
next_state = ACTIVE;
valid = 1’b1;
end
end
The defaults establish the behavior for every path. Specific conditions then override those values.
This style has another advantage: it makes the designer’s intent visible during code review.
Instead of trying to mentally identify every missing branch, a reviewer can first see the baseline behavior and then examine which conditions modify it.
Latch inference is not limited to a single output.
Consider:
always_comb begin
if (sel) begin
y = a;
valid = 1’b1;
end
else begin
y = b;
end
end
y is fully assigned, but valid is not assigned when sel is 0.
Therefore, valid can require storage.
A better implementation is:
always_comb begin
y = b;
valid = 1’b0;
if (sel) begin
y = a;
valid = 1’b1;
end
end
When checking a combinational block, review every left-hand-side signal independently.
One correctly assigned output does not make the entire block latch-free.
Finite-state machines are another area where accidental latches commonly appear.
Consider:
always_comb begin
case (state)
IDLE: begin
if (start)
next_state = RUN;
end
RUN: begin
if (done)
next_state = IDLE;
end
endcase
end
There are multiple missing assignments.
What should next_state be when:
state = IDLE
start = 0
Or:
state = RUN
done = 0
The intended behavior is often to remain in the current state.
Instead of relying on incomplete assignments to imply that behavior, make it explicit:
always_comb begin
next_state = state;
case (state)
IDLE: begin
if (start)
next_state = RUN;
end
RUN: begin
if (done)
next_state = IDLE;
end
default: begin
next_state = IDLE;
end
endcase
end
This is much easier to understand and review.
The default:
next_state = state;
clearly expresses the “stay in the current state unless a transition condition is met” behavior.
A latch and a flip-flop are both storage elements, but they are not interchangeable.
A flip-flop is normally inferred from edge-triggered sequential RTL:
always_ff @(posedge clk) begin
q <= d;
end
A latch is level-sensitive.
For example, intentional latch behavior may be described using:
always_latch begin
if (enable)
q <= d;
end
SystemVerilog provides always_ff, always_comb, and always_latch to make the intended hardware model clearer to tools and other engineers.
The goal is therefore not:
“Never use latches.”
The better rule is:
Never allow a latch to appear unintentionally.
If a latch is genuinely required, describe it explicitly and document why it exists.
An accidental latch can create several downstream problems.
Latch-based timing analysis is different from straightforward edge-triggered flip-flop timing. The design team must account for transparency windows and related timing relationships.
An output that retains its previous value may look unexpected during RTL simulation, especially when the designer thought the block was purely combinational.
A latch introduces state into a block that may have been intended to be stateless.
That can make waveforms difficult to interpret because an output is no longer determined solely by the current input values.
Unintended storage elements affect synthesis, timing, area, power, and downstream implementation decisions.
A latch warning is sometimes more than a coding issue. It can reveal that the specification itself does not clearly define what the output should do under certain conditions.
Good RTL development should not depend entirely on manual inspection.
RTL lint tools can identify incomplete assignments and other coding problems before the design reaches synthesis. Synthesis tools also report inferred storage elements, and those warnings should be investigated rather than ignored.
A practical development cycle is:
Write RTL
↓
Run simulation
↓
Run RTL lint
↓
Review latch warnings
↓
Run synthesis
↓
Review inferred hardware
↓
Fix unintended storage
If a combinational block unexpectedly produces a latch in the synthesis report, go back to the RTL and identify which signal lacks a complete assignment path.
Before committing a combinational RTL block, ask:
This checklist catches a large percentage of accidental latch problems before they become more expensive to debug.
Inferred latches are rarely caused by complicated hardware concepts. More often, they come from a small gap between what the designer intended and what the RTL actually specified.
An incomplete if, a missing default in a case, or an overlooked output assignment can be enough to turn apparently simple combinational logic into storage.
The most reliable approach is to make combinational behavior explicit. Use complete assignments, establish sensible defaults, write FSM next-state logic carefully, prefer always_comb for SystemVerilog combinational logic, and treat lint and synthesis warnings as part of the design process rather than as optional cleanup.
Most importantly, remember the core rule:
If a combinational output must never remember its previous value, make sure the RTL assigns it a value for every possible path.
That simple discipline can prevent many RTL bugs and help produce cleaner, more predictable synthesis results.