RTL Design — Synthesis Pitfalls

Latch Inference &
RTL Coding Guidelines

An unintentional latch is one of the most dangerous bugs in RTL design — it silently passes simulation but breaks timing analysis and can fail in silicon. This guide shows exactly when and why synthesis infers a latch, how to detect it, how to eliminate it, and the complete set of RTL coding guidelines every ASIC designer must follow.

Latch vs Flip-Flop
RTL Best Practices
Verilog & SystemVerilog
Synthesis-Ready
1

Default-Assign Every Output First

At the top of every always @(*) block, assign every output to its safe default value before any if/case logic. This guarantees full coverage with zero latches.

2

Every case Needs a default Branch

Even if the synthesizer never reaches it, always add a default: out = '0; branch to every case statement to prevent latch inference on uncovered encodings.

3

One Driver Per Signal

Each reg/wire must be driven by exactly one always block or assign statement. Two drivers produce X in simulation and a short circuit in synthesis — both are silent killers.

4

Never Mix = and <= in One Block

Use blocking = only in combinational blocks; non-blocking <= only in sequential blocks. Mixing them in one always block causes sim/synth mismatches that are nearly impossible to debug.

The Problem

What Is a Latch and Why Is It Dangerous?

A latch is a level-sensitive storage element: its output is transparent (follows input) when the enable is active, and holds its value when the enable is inactive. Unlike a flip-flop, it has no clock — it changes output whenever the enable and data change, creating a window that STA tools cannot fully time.

⚠ LATCH (Level-Sensitive) LATCH D Q EN D EN Q Transparent when EN=1 Q changes any time D or EN changes ⚠ STA cannot fully time this path Glitch on EN → glitch on Q Hard to scan-test in DFT ✓ FLIP-FLOP (Edge-Triggered) D FF D Q CLK▲ D CLK Q Updates only on rising clock edge Q stable between edges — easy to time ✓ STA fully analyzes setup & hold paths Glitch on D before edge is ignored Scan-chain compatible
🚫

The Silent Killer: Simulation Passes, Silicon Fails

An unintentionally inferred latch will often pass RTL simulation because the test vectors happen to trigger the output on the intended path. But in silicon, the latch enable glitches due to combinational hazards, or the transparent window causes hold violations that STA missed. Most ASIC projects require zero unintentional latches — enforced by lint tools like Spyglass or Mentor's HDL Designer.

Root Causes

The Three Ways Synthesis Infers a Latch

Every latch inference comes down to one root cause: the synthesizer cannot find a combinational assignment for an output in every possible code path, so it inserts a storage element to hold the previous value.

❌ Case 1: if without else
always @(*) begin
  if (load)
    data_out = data_in;
  // No else: when load=0,
  // data_out must HOLD → LATCH
end
✅ Fix: default first, then if
always @(*) begin
  data_out = 8'h00; // default
  if (load)
    data_out = data_in;
  // load=0 → data_out = 8'h00
  // No latch. Pure combinational.
end
❌ Case 2: case without default
always @(*) begin
  case (state)
    IDLE:  out = 2'b00;
    READ:  out = 2'b01;
    WRITE: out = 2'b10;
    // DONE state → out holds → LATCH
  endcase
end
✅ Fix: default assignment + default branch
always @(*) begin
  out = 2'b00; // default — prevents latch
  case (state)
    IDLE:  out = 2'b00;
    READ:  out = 2'b01;
    WRITE: out = 2'b10;
    default: out = 2'b00; // covers DONE
  endcase
end
❌ Case 3: Only some branches assign all outputs
always @(*) begin
  if (sel) begin
    y = a;
    z = b;       // z assigned here
  end else begin
    y = c;
    // z NOT assigned when sel=0 → LATCH on z!
  end
end
✅ Fix: assign ALL outputs in ALL branches
always @(*) begin
  y = 1'b0; z = 1'b0; // defaults
  if (sel) begin
    y = a;
    z = b;
  end else begin
    y = c;
    z = 1'b0; // explicit, or rely on default
  end
end
RTL Coding Guidelines

10 Rules Every RTL Designer Must Follow

These guidelines prevent the majority of synthesis bugs, sim/synth mismatches, and DFT issues encountered in production ASIC flows. Follow all of them on every RTL block.

01
DO

Default-assign all outputs at the top of every combinational always block

The first statement(s) inside always @(*) should assign every output to its safe inactive value. This guarantees zero latches regardless of how complex the if/case logic becomes, and makes code easier to review.

02
DO

Use @(*) — never manual sensitivity lists

Manual sensitivity lists (always @(a or b or sel)) become stale when signals are added. A missed signal means the simulator does not re-evaluate the block when that input changes — a silent sim/synth divergence. Always use @(*) or always_comb.

03
AVOID

Never mix blocking (=) and non-blocking (<=) in one always block

Blocking assignments in a clocked block cause ordering-dependent behavior in simulation that synthesis ignores. Non-blocking assignments in a combinational block cause zero-delay feedback loops. Keep the rule absolute: = in combinational, <= in sequential — never mixed.

04
AVOID

One always block per output signal — no multi-drivers

Each reg must be driven by exactly one always block. Two always blocks both driving the same variable produce X in simulation (last-executed wins non-deterministically) and are a synthesis error. Use a single always block with internal mux logic if multiple conditions must update the same signal.

05
DO

Add a default branch to every case statement

Even if the full encoding is theoretically covered by named cases, always add default: out = '0;. This documents intent, prevents lint warnings, covers X-propagation in simulation, and prevents latches on any encoding the tool considers "reachable."

06
DO

Always include a synchronous reset in clocked always blocks

A flip-flop without reset comes up in an unknown state after power-on. Always include a reset condition in every sequential always block. Prefer synchronous reset (if (!rst_n) q <= '0;) for ASIC flows; use asynchronous reset only when the design specifically requires it.

07
AVOID

Do not infer combinational feedback loops

If a signal feeds itself through combinational logic (assign a = a & b;), the simulator will oscillate or produce X, and synthesis may produce a ring oscillator or error. Every combinational path must have a registered cut point (flip-flop) to break feedback.

08
DO

Use full-width arithmetic — avoid implicit truncation

Verilog expressions evaluate at the width of the widest operand, but assignments truncate. Always explicitly size intermediate wires: wire [8:0] sum = {1'b0, a} + {1'b0, b}; to capture carry. Implicit truncation discards the MSB silently.

09
DO

Prefer always_comb and always_ff in SystemVerilog

always_comb causes a compile error if a latch would be inferred. always_ff restricts the block to flip-flop inference and catches incorrect usage. These typed always blocks make synthesis intent explicit and eliminate entire categories of bugs at compile time rather than silicon time.

10
DO

Run RTL lint before synthesis — fix all warnings

Tools like Spyglass, Mentor Questa Lint, or Synopsys Leda catch latch inference, multi-drivers, incomplete sensitivity lists, and sim/synth mismatches before a single gate is mapped. Treat lint warnings as errors. A design that lint-clean synthesizes predictably.

Intentional Latches

When a Latch Is Actually Correct

Latches are not universally wrong — they appear intentionally in clock-gating cells and specific low-power design patterns. The key is that the intent must be explicit.

Intentional Latch Patterns
// ── Integrated Clock Gating (ICG) latch ─────────────────
// Standard cell in ASIC libs — latch holds the enable.
// ICG cell is instantiated, not inferred, in production RTL.
module icg_cell (
  input  clk, enable, test_en,
  output gated_clk
);
  reg en_latch;
  // Latch: transparent on LOW phase of clk
  always @(*) begin
    if (!clk) en_latch = enable | test_en;
  end
  assign gated_clk = clk & en_latch;
endmodule

// ── SystemVerilog: always_latch (explicit intent) ────────
// Use always_latch when a latch is truly intended.
// Compiler will error if your block infers a FF instead.
always_latch begin
  if (en) q_latch <= d; // transparent when en=1
end

// For all other RTL: use always_comb (errors on latch inference)
always_comb begin
  out = '0;           // default
  if (sel) out = a;   // no latch — default covers sel=0
end
Quick Reference

RTL Coding Guidelines Summary

RuleCombinational BlockSequential BlockConsequence if Broken
Assignment type Blocking = Non-blocking <= Sim/synth mismatch, race condition
Sensitivity list @(*) or always_comb @(posedge clk [or negedge rst_n]) Sim divergence from missed signals
All outputs assigned Yes — default first N/A (holds value by FF nature) Latch inferred → timing failure
case completeness default branch required default branch required Latch on outputs not in all branches
Reset N/A Always include reset Unknown state after power-on
Drivers per signal Exactly one Exactly one X in sim / short circuit in silicon
SV preferred form always_comb always_ff Missing compile-time latch check
FAQ

Frequently Asked Questions

What causes latch inference in Verilog?
A latch is inferred when a combinational always @(*) block does not assign an output variable in every possible code path. The synthesizer must insert a storage element to hold the previous value when the unassigned path is taken. The three patterns that cause this: (1) an if without an else, (2) a case without a default branch, (3) an output that is only assigned in some branches of an if/case but not all. The fix for all three is the same: add a default assignment at the top of the block that covers every output.
What is the difference between a latch and a flip-flop?
A latch is level-sensitive: its output is transparent (follows input) whenever the enable is active. It has no clock — it can change output at any time, creating combinational paths that STA cannot fully constrain. A flip-flop is edge-triggered: its output only changes on a clock edge (posedge or negedge). The flip-flop is the fundamental register element of synchronous design. All intentional storage in RTL should use flip-flops (via always @(posedge clk)), never unintentional latches.
Are latches ever correct in RTL design?
Yes — intentional latches are used in two specific contexts: (1) Integrated Clock Gating (ICG) cells: a latch is used to hold the clock-enable signal during the active clock phase, preventing glitches from propagating to the gated clock. This is a standard low-power technique in ASIC design, but the latch is instantiated as a specific standard cell — not inferred by general RTL. (2) Explicit latch storage in some high-performance datapaths. In SystemVerilog, use always_latch to declare the intent explicitly. Any latch that appears without intentional use of always_latch is an unintentional latch — a bug.
How does always_comb prevent latch inference?
always_comb in SystemVerilog causes the elaborator (and most simulators) to issue a compile-time error if the block would infer a latch. This turns what was a silent synthesis issue — discovered only during gate-level netlist review or STA — into an error that stops compilation immediately. Additionally, always_comb executes once at time zero before simulation begins, ensuring outputs are not left in an unknown state at startup. Use always_comb in any flow that supports SystemVerilog; it is strictly superior to always @(*).
What is a multi-driver and how do I fix it?
A multi-driver occurs when two or more always blocks (or an always block and an assign statement) both drive the same net. In simulation, both drivers "write" the net — if they disagree, the result is X (unknown). In synthesis, it produces a short circuit that will physically short power to ground. The fix: consolidate all driving logic for a signal into a single always block using an internal mux (always @(*) begin out = sel ? a : b; end). Lint tools catch multi-drivers at elaboration. If you are seeing X propagation through a net, a multi-driver is a likely cause.
What RTL lint warnings should I never ignore?
Treat these lint warnings as errors — they indicate real synthesis bugs: (1) Latch inferred — any unintentional latch. (2) Multi-driver — two sources driving the same net. (3) Incomplete sensitivity list — signal read but not in @(...). (4) Blocking in sequential block — using = inside posedge clk. (5) Combinational loop — a net feeds itself without a register. (6) Width mismatch — assigning a wider expression to a narrower net (truncation). (7) Unconnected output port — a module output left floating. These seven categories catch the majority of RTL bugs before synthesis runs.