Module 8: RTL Coding for Quality & Synthesis ยท 8.4 Common RTL Bugs & Debugging

Finding a Latch That Got Into Your Design

The previous article chased an X. This one chases a latch.

Section 5.2 explained why an incomplete if or case infers a latch, down to the mechanism. This article assumes you know that and asks the practical question: a colleague hands you a 2,000-line module and says “there’s a latch warning in here somewhere”. How do you find it quickly?

The synthesis warning is the first clue, so read it literally

Every synthesis tool reports inferred latches as a warning, and the warning almost always names the signal:

Warning: Inferring latch for variable 'grant' in always block at
  arbiter.v line 47. Signal 'grant' is not assigned in all
  branches of the conditional statement.

This is a gift most engineers under-use. It gives the exact signal and the exact line. The instinct to scan the whole file for problems is slower and less reliable than going to line 47 and asking one question: on every path through this always block, does grant get assigned?

The two patterns from Section 5.2 as a checklist

Rather than re-deriving the theory, use it as a scanning checklist against the flagged block.

Pattern 1, a missing else:

always @(*) begin
    if (req)
        grant = 1'b1;
    // Missing: what happens to grant when req is 0?
end

Pattern 2, incomplete case coverage:

always @(*) begin
    case (state)
        IDLE:   grant = 1'b0;
        ACTIVE: grant = 1'b1;
        // Missing: DONE, ERROR, and any other state value, including
        // encodings that were never given a name at all, per Section
        // 5.6's illegal-state discussion
    endcase
end

In both cases the fix from Section 5.2 is the same: a default assignment at the top of the block, before any conditional, so every path assigns the signal something.

always @(*) begin
    grant = 1'b0;   // default: now EVERY path assigns grant something
    case (state)
        IDLE:   grant = 1'b0;
        ACTIVE: grant = 1'b1;
        // any unlisted state now correctly falls through to grant = 1'b0
    endcase
end

The multi-signal trap

The case that catches people even after they have internalised the single-signal fix: an always block assigning several signals, where only some get a default.

always @(*) begin
    result = 8'h00;   // default given: result is safe
    // valid has NO default: still latch-prone
    if (op_valid) begin
        result = a + b;
        valid  = 1'b1;
    end
end

result is latch-free; every path assigns it. valid is not; it is only assigned inside the if, with no default and no else. A warning naming valid specifically, not result, is telling you exactly this: the fix is per signal, not per block, and “I already added a default in this block” is not the same as “I added a default for every signal this block assigns”.

Confirming the fix worked

After adding a default, do not assume the warning is gone. Re-run synthesis, or a lint pass, which usually catches this without a full run, and confirm the warning for that specific signal has disappeared. It is common to add the default to the wrong signal in a multi-signal block, or to put it inside a nested if rather than at the top of the block where an earlier branch can still bypass it. The re-checked warning is the proof the fix landed.

The one intentional latch in this course, operand isolation in Section 7.6, is the legitimate exception to “eliminate every latch warning”. When you infer a latch on purpose, most tools let you acknowledge and waive that specific warning, tied to that signal and that line, rather than ignoring warnings in general. A design review should always be able to distinguish “this latch is intentional, documented and waived” from “this warning was never looked at”. The second is an unexamined risk; the first is a reviewed decision.

Practice Exercise

A synthesis tool reports Warning: Inferring latch for variable 'busy' in always block at controller.v line 112. You open the file and find a case with four named states, each of which assigns busy, and no default. The state register is 3 bits wide. Explain why the warning fires anyway.

Solution

A 3-bit state register has $2^3 = 8$ values, and the case names 4 of them. This is the illegal-state scenario from the safe-FSM article in Section 5.6: the other 4 encodings are reachable by hardware (a glitch, an uninitialised reset, anything that puts the register into a value nobody assigned) but have no branch, and with no default either, busy is unassigned on those 4 paths, which is what infers the latch. The fix is the default branch from Section 5.6, doing double duty: it routes an illegal state to a safe recovery state and guarantees busy is assigned on every value, removing the warning.

A latch warning names the signal and line; use it directly rather than re-scanning a file. The fix is one of the two patterns from Section 5.2, applied per signal in a multi-assignment block and confirmed by re-running the tool. The one legitimate exception, operand isolation’s deliberate latch, should always be an explicitly waived exception, never a warning nobody classified. In the next article we look at reset domain problems, the bug class that shows up only at integration, when individually correct modules are wired together.

โ† X-Propagation in Practice Reset Domain Problems โ†’