Topic 3 · Procedural Combinational Logic

The Latch Problem

Video 3 of 4 · ~12 minutes

Dr. Mike Borowczak · Electrical & Computer Engineering · CECS · UCF

always @(*)if/else & caseLatch ProblemBlock vs Nonblock

🌍 Where This Lives

In Industry

“Zero inferred latches” is a line item on virtually every FPGA/ASIC design checklist. Synopsys DC, Xilinx Vivado, and Intel Quartus all emit latch warnings — which senior engineers treat as errors. Static timing analysis tools can't reliably analyze a design with latches, so they break your sign-off flow.

In This Course

Every Topic 3+ lab checks for latch warnings during make stat. If Yosys reports “inferred latch,” your grader sees it. This video is your insurance policy: three independent techniques, each of which alone prevents the bug.

⚠️ When Memory Sneaks In

❌ Wrong Model

“I didn't write posedge clk, so there's no memory in my always @(*) block. Whatever I forgot to assign just stays undefined.”

✓ Right Model

Any signal that must retain a value through some path of your logic needs storage. If your always @(*) doesn't assign the output on some path, the synthesizer infers a level-sensitive latch to hold the prior value. No clock required — the latch watches your condition signal.

The failure mode: Latches work in simulation. They pass many testbenches. But they create unclocked storage — glitches on the condition signal propagate to the output, and static timing analyzers refuse to sign off. The bug hides until tapeout.

The Recipe for an Inferred Latch

// Missing else → latch on y
always @(*) begin
    if (sel) y = a;
    // when sel=0: y must "hold" its previous value → LATCH
end
// Missing default + missing case → latch
always @(*) begin
    case (opcode)
        2'b00: y = a;
        2'b01: y = b;
        // 2'b10 and 2'b11 not assigned → LATCH
    endcase
end
The rule: If there exists any combination of inputs where your output is not assigned, the synthesizer must infer storage — it has no choice. Your “intent” doesn't matter; the tool can't read minds.

👁️ I Do — Three Ways to Prevent Latches

1. Default at top

always @(*) begin
    y = 4'b0;   // default FIRST
    if (sel) y = a;
    // no latch!
end

2. Complete if/else

always @(*) begin
    if (sel) y = a;
    else     y = 4'b0;
end

3. default in case

always @(*) begin
    case (opcode)
        2'b00: y = a;
        2'b01: y = b;
        default: y = 4'b0;
    endcase
end
My preference: Option 1 (default at top). It's the most defensive — it covers both if-without-else and missing-case in one line. I put it in every single always @(*) block I write.

🤝 We Do — Latch Hunt

Which of these always @(*) blocks will infer a latch?

// Block A
always @(*) begin
    if (en) y = a;
    else    y = 0;
end
// Block B
always @(*) begin
    case (sel)
        2'b00: y = a;
        2'b11: y = b;
    endcase
end
// Block C
always @(*) begin
    y = 0;
    if (en) y = a;
end
Answers: A: NO latch (complete if/else). B: YES latch (cases 2'b01 and 2'b10 not covered → y holds). C: NO latch (default at top).

🧪 You Do — Predict the Warning

You synthesize this code. What exact warning do you expect from Yosys?

always @(*) begin
    case (opcode)
        2'b00: result = a + b;
        2'b01: result = a - b;
    endcase
end
Expected: Warning: Latch inferred for signal \result\ in process... Followed by: list of conditions under which the signal is not assigned (opcodes 2'b10 and 2'b11). Some synthesizers also report the estimated latch gate count.
Habit: grep your synthesis logs for “latch” or “inferred” every run. One hit = one bug to fix.
▶ LIVE DEMO

Inducing & Fixing a Latch

~5 minutes

▸ COMMANDS

# 1. Buggy version (latch warnings):
cd lecture_examples/week1_day03/d03_s3_ex2/
make stat 2>&1 | grep -i 'latch\|infer'

# 2. Fixed version (default + complete case):
cd ../d03_s3_ex3/
make stat 2>&1 | grep -i 'latch\|infer'

# Diff the two source files to see the one-line fixes:
diff ../d03_s3_ex2/day03_ex01_latch_demo.v \
     ../d03_s3_ex3/day03_ex02_latch_fixed.v

▸ EXPECTED OUTPUT (Yosys 0.47)

# d03_s3_ex2 (buggy):
Latch inferred for signal
  `\o_bug1' from process ...
Latch inferred for signal
  `\o_bug2' from process ...
SCC pass: Found 8 SCCs   ← combo loops!
=== latch_demo ===
   SB_LUT4   12

# d03_s3_ex3 (fixed):
(no latch warnings, 0 SCCs)
=== latch_fixed ===
   SB_LUT4    8

▸ KEY OBSERVATION — THE BUG IS HIDING

stat shows only SB_LUT4 in both versions — no latch cell appears. The buggy build just has 4 more LUTs (12 vs 8) wired into 8 combinational feedback loops (one per bit of o_bug1/o_bug2). Read the synthesis log for Latch inferred warnings and the SCC count — make stat alone won't save you.

🔧 What Did the Tool Build?

Same module, buggy vs fixed (Yosys 0.47, synth_ice40):

Buggy (latch inferred)

Latch inferred for `\o_bug1'
Latch inferred for `\o_bug2'
SCC pass: Found 8 SCCs
SB_LUT4     12

No DFF, no SB_DFFE, no SB_DLATCH. The latch becomes 12 LUTs wired into 8 combinational feedback loops — one per output bit.

Fixed (pure combinational)

(no latch warnings)
SCC pass: Found 0 SCCs
SB_LUT4      8

Pure combinational. 8 LUTs, no feedback loops, clean STA.

The trap on FPGAs: iCE40 has no latch primitive. Yosys lowers $_DLATCH_P_ through latches_map.v into a mux + inverter, which ABC packs into LUTs with feedback. stat just shows SB_LUT4 — exactly what a combinational design looks like. The bug is hiding inside the LUT fabric.
How to actually catch it: grep the Yosys log for Latch inferred and check the SCC count. A latch-free combinational module has 0 SCCs; every inferred latch bit adds one feedback loop.

Where the Latch Actually Lives

Same Verilog, two silicon targets — radically different gates.

ASIC — dedicated cell

Standard-cell libraries ship a real DLATCH primitive (transmission-gate or NAND-SR internally). Synthesis maps the inferred latch to one characterized cell. STA still flags it — latches break sign-off — but the gate is real and visible in the netlist.

D EN DLATCH stdcell D G Q Q

netlist: DLATCH_X1 — one cell, level-sensitive

FPGA (iCE40) — LUT + feedback

No latch primitive in the fabric. Yosys lowers $_DLATCH_P_ via latches_map.v into Q = EN ? D : Q — a mux whose own output feeds an input. ABC packs that into an SB_LUT4. The latch is the combinational loop.

D EN SB_LUT4 EN ? D : Q Q combinational feedback → SCC

netlist: SB_LUT4 + 1 SCC per latched bit

Why this matters: on ASIC the bug is at least visible in the cell list (a DLATCH shouldn't be there). On iCE40 it disappears into LUT statistics that look identical to a clean combinational design — the only fingerprints left are the “Latch inferred” warning and the non-zero SCC count.

🤖 Check the Machine

Paste this buggy code and ask AI: “Will this infer a latch on iCE40? If so, give me three different fixes.”

always @(*)
    if (enable) out = data;

TASK

Ask AI: latch? What fixes?

BEFORE

Predict: 3 fixes — default at top, explicit else, rewrite with assign.

AFTER

Good AI identifies the latch and suggests all 3. Weak AI may miss the default-at-top pattern.

TAKEAWAY

Latch-hunting is a well-solved problem; AI is actually reliable here. Use it as a second pair of eyes.

Key Takeaways

 A latch appears when any input combination leaves an output unassigned.

 Three fixes: default at top, complete if/else, case with default.

 Default at top is universal — use it always.

 Grep synthesis logs for “latch” every session. Zero tolerance.

Every output, every path, every time. If you don't assign it, the tool stores it.

🔗 Transfer

Blocking vs. Nonblocking

Video 4 of 4 · ~6 minutes

▸ WHY THIS MATTERS NEXT

You've seen = (blocking) inside every always @(*) so far. Tomorrow we introduce <= (nonblocking) for sequential logic. Video 4 previews the rule: = for combinational, <= for sequential, never mix. Topic 4 will prove why, but you need the rule in memory before you get there.