How to Avoid Inferred Latches in RTL Design

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.

What Is an Inferred Latch?

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.

Why Do Latches Get Inferred?

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.

The Most Common Cause: Incomplete if Statements

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.

Better Approach

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.

Nested Conditions Can Hide Latches

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.

Case Statements Are Another Major Source

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.

Use always_comb for Combinational RTL

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.

Default Assignments: One of the Simplest Latch Prevention Techniques

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.

Be Careful With Multiple Outputs

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.

Latches in FSM Next-State Logic

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.

Do Not Confuse Latches With Flip-Flops

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.

Why Unintended Latches Are a Problem

An accidental latch can create several downstream problems.

1. Timing becomes more complicated

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.

2. Simulation and hardware behavior can become confusing

An output that retains its previous value may look unexpected during RTL simulation, especially when the designer thought the block was purely combinational.

3. Debugging becomes harder

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.

4. Physical implementation can become more complicated

Unintended storage elements affect synthesis, timing, area, power, and downstream implementation decisions.

5. It can indicate a deeper RTL specification problem

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.

Use Lint and Synthesis Reports

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.

A Practical Latch-Prevention Checklist

Before committing a combinational RTL block, ask:

  • Does every output receive a value on every possible path?
  • Does every if statement have an intentional else or a safe default assignment?
  • Does every case statement handle uncovered conditions appropriately?
  • Are default assignments placed at the beginning of complex combinational blocks?
  • Is always_comb being used where SystemVerilog is available?
  • Are FSM next-state signals assigned sensible defaults?
  • Are all outputs checked individually?
  • Are latch warnings from lint being reviewed?
  • Are synthesis reports showing any unexpected latch cells?
  • If a latch is intentional, is it explicitly coded and documented?

This checklist catches a large percentage of accidental latch problems before they become more expensive to debug.

Final Thoughts

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.

Leave a Reply

Your email address will not be published. Required fields are marked *