Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Part 0 · What Debugging Is6
  • Part 1 · Reproduce8
  • Part 2 · Observe9
  • Part 3 · Localise10
  • Part 4 · Hypothesise and Test6
  • Part 5 · Bug Classes16
    • Logic Bugs: Off-by-One, Width and Sign
    • FSM Bugs
    • X and Reset Bugs
    • Races and Simulation-Synthesis Mismatches
    • Latches and Combinational Loops
    • CDC Bugs
    • Reset-Domain Crossing Bugs
    • Timing-Only Bugs
    • Protocol Violations
    • Hangs and Deadlocks
    • Starvation and Livelock
    • Performance Bugs
    • Power-Intent Bugs
    • DFT-Mode Bugs
    • Configuration and Register-Map Bugs
    • Testbench Bugs That Look Like Design Bugs
  • Part 6 · Debug by Stage9
  • Part 7 · Hangs and Deadlocks7
  • Part 8 · Intermittent Failures7
  • Part 9 · Fix, Prove, Prevent7
  • Part 10 · Case Studies12
  • Part 11 · Debug as a Team5
  • Part 12 · Interview Prep5
Loading lesson

‹Part 5 · Bug Classes
  1. Topics
  2. The Art of Hardware Debug
  3. Part 5 · Bug Classes
  4. Latches and Combinational Loops

Part 5 · Bug Classes · Working (W)

Latches and Combinational Loops

Latches and combinational loops: a combinational output that remembers, and a simulation that hangs or oscillates. Real Yosys, Verilator and Icarus messages for both, a decoder whose missing branch corrupts a register only after one kind of access, and an arbiter and slave that loop only when they are connected.

On this page

    Why this matters

    A latch makes a combinational output depend on history, so it fails only after a particular sequence of inputs; a loop makes simulation hang with no error at all. Both are reported by the tools long before the lab, in a log line that is easy to scroll past.

    After this lesson you can: recognise a latch from history-dependent output and a combinational loop from a hang or oscillation, find each with Yosys and Verilator, and fix them with the complete-assignment pattern and a broken dependency.

    Note

    Level: Working (W) · Debug-loop step: Localise, Hypothesise

    Before this: The First Wrong Cycle, Timeouts and Watchdogs in Benches

    A register that changes when nobody wrote it

    A peripheral's DATA register sometimes holds a value that software never wrote to it. The firmware team has a trace: it happens after a write to an unmapped address, but only sometimes. Most writes to that address are harmless.

    The register's write logic is three lines and looks right. What kind of bug changes behaviour depending on the access before the one that fails?

    What people usually do, and why it fails

    They stare at the register's always_ff, add a test that writes the unmapped address, see it pass (the test wrote CTRL first), and close the report as a firmware problem. Or, for the second bug in this lesson, they watch a simulation sit at 100% CPU and raise the timeout.

    Both bugs were reported by the tools at the first synthesis or lint run. The message was a warning in a log of several hundred lines.

    The signature

    Bug

    In a waveform or log

    In a regression

    Latch

    a combinational output keeps its old value when its inputs change; the same input gives different outputs depending on what came before

    fails only after a particular sequence; a test that changes the order passes

    Combinational loop, odd inversions

    simulation time stops advancing with no message (Icarus); Verilator stops with DIDNOTCONVERGE; with gate delays, a signal toggles every few nanoseconds with steady inputs

    a hang that the bench's timeout or watchdog catches, or a run that never ends

    Combinational loop, even inversions

    simulates fine and holds a value: it is a latch made of gates, with no enable and no timing check

    usually passes; found by tools, or as a power-up state that differs between runs

    On the debug loop this is Localise and Hypothesise: the history dependence or the frozen time tells you which class you are in before you read the RTL.

    The first three checks

    1. Grep the synthesis log for Latch inferred and logic loop, and the lint log for LATCH, CASEINCOMPLETE and UNOPTFLAT. Read every hit on the failing path.

    2. For a latch, find the unassigned path. For each output of the suspect always block, walk every branch of every if and case and check it is assigned. A missing default or else is the usual one.

    3. For a hang, check time. If simulation time has stopped moving, it is a zero-delay loop, not a slow test. Run the design through Yosys check after flatten, or Verilator lint on the top module, and read the loop it prints.

    The picture: a loop that no module contains

    The arbiter grants unless the slave stalls; the slave stalls a grant that arrives while it is busy. Each module is combinational and correct alone. Connected, grant depends on itself through one inversion.

    One real reproduction

    The bench below holds both bugs and their fixes. reg_decode_bad has no branch for address 3, so its write enables keep their last value there. bus_loop is the arbiter and slave of the picture, with a 1 ns delay per gate as real gates have. With no plusargs the bench checks both claims; +BAD and +FIX run one design as an ordinary test.

    reg_decode_bad.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    `timescale 1ns/1ps
    // reg_decode_bad: write enables for a small register block. Address 3 is unmapped,
    // and the case has no branch for it, so both enables keep their last value: a latch.
    module reg_decode_bad (input logic [1:0] addr, input logic we, output logic we_ctrl, we_data);
      always_comb
        case (addr)
          2'd0: begin we_ctrl = we;   we_data = 1'b0; end   // CTRL
          2'd1: begin we_ctrl = 1'b0; we_data = we;   end   // DATA
          2'd2: begin we_ctrl = 1'b0; we_data = 1'b0; end   // STATUS: read-only
        endcase                                             // 3: nothing assigned
    endmodule
    
    // The fix: assign every output first, then override. No path leaves an output unassigned.
    module reg_decode_ok (input logic [1:0] addr, input logic we, output logic we_ctrl, we_data);
      always_comb begin
        we_ctrl = 1'b0;
        we_data = 1'b0;
        case (addr)
          2'd0: we_ctrl = we;
          2'd1: we_data = we;
          default: ;                                        // STATUS and unmapped: no write
        endcase
      end
    endmodule
    
    // The loop: arbiter and slave, each fine alone. Each gate has a 1 ns delay, as real gates do.
    module bus_loop #(parameter bit FIX = 0) (input logic req, busy, output logic grant);
      logic stall;
      assign #1 grant = req & ~stall;                       // grant unless the slave stalls
      if (FIX) assign #1 stall = busy;                      // fix: stall depends on busy only
      else     assign #1 stall = busy & grant;              // bug: stall depends on the grant
    endmodule
    

    The latch: wrong only after a DATA write

    Terminal
    $ iverilog -g2012 -o l.vvp latch_tb.sv
    $ vvp -n l.vvp +BAD
    FAIL bad: DATA after a write to 3 that follows a DATA write is 99, expected 11
    FAIL bad: grant changed 19 times in 40 ns with req and busy steady
    FATAL: latch_tb.sv:101: buggy design: 2 checks failed
           Time: 140000  Scope: latch_tb.$unm_blk_6
    $ vvp -n l.vvp +FIX
    PASS: fixed design, all 3 checks
    latch_tb.sv:111: $finish called at 140000 (1ps)
    $ vvp -n l.vvp
    bad: 2 of 3 checks fail; fix: 0 of 3 fail
    PASS: the latch corrupts DATA only after a DATA write, the loop oscillates; the fixes pass
    latch_tb.sv:111: $finish called at 140000 (1ps)

    Read the first line with the scene in mind. A write to address 3 straight after a DATA write puts 0x99 into DATA. The same write after an idle cycle (check 2) does not fail. That is the history dependence: when the address moves from 1 to 3, we_data keeps the 1 it had, so the unmapped write lands in DATA. After an idle cycle it keeps a 0 instead. The firmware team's "only sometimes" was the access before. Verilator 5.048 prints the same lines.

    The tools said so before any test ran. Yosys stops on always_comb, because that keyword promises no latch; with the older always @* the same code is only a warning, and the cells appear in stat:

    Terminal
    $ yosys -p "read_verilog -sv decode.sv; synth -top reg_decode_bad; stat"
    ...
    ERROR: Latch inferred for signal `\reg_decode_bad.\we_ctrl' from always_comb process `\reg_decode_bad.$proc$decode.sv:4$1'.
    $ yosys -p "read_verilog -sv decode_star.sv; synth -top reg_decode_bad; stat"      # the same code with always @*
    ...
    Warning: Latch inferred for signal `\reg_decode_bad.\we_ctrl' from process `\reg_decode_bad.$proc$decode_star.sv:4$1': $auto$proc_dlatch.cc:543:proc_dlatch$35
    Warning: Latch inferred for signal `\reg_decode_bad.\we_data' from process `\reg_decode_bad.$proc$decode_star.sv:4$1': $auto$proc_dlatch.cc:543:proc_dlatch$68
    ...
            2   $_DLATCH_NP0_
    ...
    Warnings: 2 unique messages, 2 total
    $ verilator --lint-only -Wall --top-module reg_decode_bad decode.sv
    ...
    %Warning-CASEINCOMPLETE: decode.sv:5:5: Case values incompletely covered (example pattern 0x3)
    %Warning-LATCH: decode.sv:4:3: Latch inferred for signal 'we_ctrl' (not all control paths of combinational always assign a value)
    %Warning-LATCH: decode.sv:4:3: Latch inferred for signal 'we_data' (not all control paths of combinational always assign a value)
    ...

    decode.sv is the two decoder modules from the bench. Verilator even names the missing pattern, 0x3. For reg_decode_ok Yosys prints No latch inferred for both signals and Verilator's lint has nothing to say about the logic.

    The loop: a hang, then an oscillator

    The bench gives each gate 1 ns of delay, so the loop oscillates: grant changed 19 times in 40 ns with steady inputs. Without delays, which is how RTL is normally simulated, the loop has no stable value and no time passes. loop_zero.sv is the same arbiter and slave with zero-delay assigns:

    arb_grant.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // bad-example: the arbiter and the slave each look fine; together they form a loop with one inversion.
    module arb_grant (input logic req, stall, output logic grant);
      assign grant = req & ~stall;          // grant unless the slave stalls
    endmodule
    module slave_stall (input logic busy, grant, output logic stall);
      assign stall = busy & grant;          // stall a grant that arrives while busy
    endmodule
    module loop_zero_tb;
      logic req = 1'b0, busy = 1'b0, grant, stall;
      arb_grant   u_arb (.req, .stall, .grant);
      slave_stall u_slv (.busy, .grant, .stall);
      initial begin
        #10 req  = 1'b1;
        #1  $display("t=%0t req=1 busy=0: grant=%b stall=%b", $time, grant, stall); $fflush;
        #10 busy = 1'b1;                    // the slave becomes busy while req is high
        #1  $display("t=%0t req=1 busy=1: grant=%b stall=%b", $time, grant, stall); $fflush;
        #10 $display("PASS: end of test");
        $finish;
      end
    endmodule
    Terminal
    $ iverilog -g2012 -o lz.vvp loop_zero.sv
    $ time ../limit.sh 10 vvp -n lz.vvp; echo "exit=$?"      # limit.sh: kill after 10 s
    t=11 req=1 busy=0: grant=1 stall=0
    ../limit.sh 10 vvp -n lz.vvp  9.94s user 0.03s system 99% cpu 10.009 total
    exit=142
    $ obj_lz/Vloop_zero_tb                                   # Verilator 5.048 build of the same file
    t=11 req=1 busy=0: grant=1 stall=0
    %Error-DIDNOTCONVERGE: loop_zero.sv:8: Active region did not converge after '--converge-limit' of 10000 tries
    Aborting...

    Icarus burns 10 seconds of CPU and never reaches the second print, due at t=22, one time unit after busy rises: it is re-evaluating grant and stall for ever within one time step. Verilator gives up after 10,000 tries and names the file and line. Now the structural view:

    Terminal
    $ yosys -p "read_verilog -sv loop_top.sv; hierarchy -top bus_top; proc; check"
    ...
    Found and reported 0 problems.
    $ yosys -p "read_verilog -sv loop_top.sv; hierarchy -top bus_top; proc; flatten; opt_clean; check"
    ...
    Warning: found logic loop in module bus_top:
        cell $flatten\u_arb.$and$loop_top.sv:2$2 ($and) source: loop_top.sv:2.18-2.30
          B[0] --> Y[0]
        wire \u_slv.grant source: loop_top.sv:4.39-4.44
        cell $flatten\u_slv.$and$loop_top.sv:5$3 ($and) source: loop_top.sv:5.18-5.30
          B[0] --> Y[0]
        wire \u_arb.stall source: loop_top.sv:1.36-1.41
        cell $flatten\u_arb.$not$loop_top.sv:2$1 ($not) source: loop_top.sv:2.24-2.30
          A[0] --> Y[0]
    Found and reported 1 problems.

    The first check looks at one module at a time and finds nothing, because neither module contains a loop. After flatten the loop is printed cell by cell with source lines: the AND in the arbiter, grant into the slave's AND, stall back through the arbiter's NOT. verilator --lint-only -Wall --top-module bus_top also reports it, as UNOPTFLAT: ... Circular combinational logic: 'grant', because Verilator lints the elaborated hierarchy.

    What the evidence says

    The latch is localised by its history dependence: the two checks differ only in the access before, so the output must remember something, and the only memory in a combinational block is an unassigned path. The loop is localised by frozen time and then by the tool, which prints the cycle. Neither needed a waveform. None of dbglab's five bugs is in this class.

    The fix pattern

    • Latch: assign every output of a combinational block at the top, then override in the branches, as reg_decode_ok does. A default branch alone is not enough if a branch still forgets one output. If memory is really meant, use a flop with an enable, not a latch.

    • Loop: break the dependency. Here the slave's stall should depend on its own state (busy), not on the grant it is answering; or register one side, so the loop passes through a flop. Do not add a delay or a buffer: that only turns the hang into an oscillator.

    Preventing it

    • Make the warnings fatal. Use always_comb (Yosys then errors on a latch), and fail the build on Verilator's LATCH, CASEINCOMPLETE and UNOPTFLAT. The synthesis report lesson shows where these lines sit in a full log.

    • Check the flattened design. Run Yosys check -assert after flatten in the build, so a loop that crosses modules fails it.

    • Count latch cells. Fail the flow if stat lists any $_DLATCH_ cell that is not on a reviewed list.

    • Review question: "Can any combinational output of this module come back, through another module, into its own inputs?"

    Traps

    Common pitfall

    A test that passes for the wrong reason. A directed test that writes CTRL and then the unmapped address passes, because we_data holds a 0. The bug needs the access before to set the latch. When a combinational bug is suspected, test each input after each possible previous input, not each input once.

    Common pitfall

    Raising the timeout. A zero-delay loop never finishes, however long you wait. If the simulated time printed by your watchdog has stopped moving, the bench is not slow; the design has no stable state. See timeouts and watchdogs for a bench that reports simulated time.

    Why this matters

    Senior lens. Loops at interfaces are an integration bug: each block passes its own lint and its own unit tests, and the loop exists only in the top level. Make the elaborated-top lint and the flattened check part of the integration build, and give each interface a rule ("ready never depends combinationally on valid" is the common one) that both owners sign.

    A latch is sometimes right, but it is always a timing and test decision for the team, written down, never an accident in a decoder.

    Interview

    Asked: "Your simulation hangs at the same simulated time every run, with the CPU at 100%. What do you suspect and how do you find it?"

    Weak answer: The test might be too long or stuck in a loop in the testbench. I would add a timeout and some prints to see where it stops.

    Strong answer: If simulated time has stopped, I suspect a zero-delay combinational loop with an odd number of inversions: there is no stable value, so the simulator re-evaluates for ever in one time step. Verilator would report DIDNOTCONVERGE, and its lint UNOPTFLAT. I would run lint on the elaborated top and Yosys check after flatten, which prints the cycle with source lines. Then I break the dependency, usually by making one side depend on state instead of the other side's output, or by registering it.

    Follow-ups: (1) Why might each module pass check on its own? (2) What does an even number of inversions round a loop give you instead?

    Check yourself An interrupt line stays high for one cycle after its source clears, but only when the previous interrupt came from a different source. Which class do you suspect first, and which check do you run?

    A latch: the output depends on the previous input, which combinational logic cannot do unless some path leaves it unassigned. Grep the synthesis and lint logs for "Latch inferred" and LATCH on the interrupt logic, then walk every branch of the always block that drives it for an unassigned output. Fix it with defaults at the top of the block.

    Check yourself A colleague fixes the hanging arbiter by adding a buffer in the stall path "to break the race". The simulation now runs to the end. Is it fixed?

    No. A delay turns the zero-delay hang into an oscillator, as the bench shows with 1 ns gates: grant toggles 19 times in 40 ns with steady inputs. The loop is still there and Yosys still reports it after flatten. Fix the dependency (stall from busy, or a register in the loop) and check that check reports no loop.

    Remember

    • A combinational output that depends on what happened before is a latch. Look for a path through an always block that does not assign it.

    • A simulation that stops advancing time without an error is the signature of a zero-delay loop. Verilator calls it DIDNOTCONVERGE.

    • Loops often span two modules. Check them on the elaborated hierarchy: Yosys check after flatten, or Verilator lint on the top.

    Rule of thumb: treat every "Latch inferred" and every logic-loop warning as a bug until someone has written down why it is meant.

    How this was checked

    latch_tb.sv as shown, 2026-10-18: iverilog -g2012 -o l.vvp latch_tb.sv and vvp -n l.vvp (Icarus Verilog 13.0) print bad: 2 of 3 checks fail; fix: 0 of 3 fail and PASS; +BAD fails with the two lines shown, +FIX passes. verilator --binary --timing --assert -Wno-fatal --top-module latch_tb (Verilator 5.048) prints the same lines, plus CASEINCOMPLETE and LATCH warnings at compile time.

    Yosys 0.69: yosys -p "read_verilog -sv decode.sv; synth -top reg_decode_bad; stat" stops with ERROR: Latch inferred for always_comb; with always @* it warns twice and stat lists 2 $_DLATCH_NP0_; reg_decode_ok reports No latch inferred. On loop_top.sv, check after hierarchy; proc reports 0 problems; after flatten; opt_clean it reports the logic loop shown. verilator --lint-only -Wall --top-module bus_top reports UNOPTFLAT.

    loop_zero.sv (zero-delay loop): Icarus prints one line at t=11 and then makes no progress; killed after 10 s by a perl alarm wrapper (exit 142). Verilator: %Error-DIDNOTCONVERGE ... Active region did not converge.

    Limits: small designs; the oscillation needs a delay model (1 ns per gate here), so its rate says nothing about real silicon. No timing analysis tool is installed, so latch timing is described, not shown.

    Keep going

    • Practice: What causes unintended latch inference and how to avoid it

    • Practice: How to avoid latch inference in SystemVerilog

    • Practice: Accidental latch inference: missing else or default

    • Interview: run a mock interview on this topic

    Related topics

    • Part 5 · Bug Classes

    • RTL: latch inference

    • RTL: reading a synthesis report

    • RTL: waveform bug patterns, latch and race

    Previous

    44 / 107

    Next: CDC Bugs

    Up next

    Up nextCDC Bugs

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Logic Bugs: Off-by-One, Width and Sign
    • FSM Bugs
    • X and Reset Bugs