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
  • Part 6 · Debug by Stage9
  • Part 7 · Hangs and Deadlocks7
  • Part 8 · Intermittent Failures7
    • Why Intermittents Happen
    • Making the Rate Go Up
    • Capturing the Rare Event
    • Statistics of Rare Failures
    • Ordering-Dependent Bugs
    • Heisenbugs
    • An Intermittent on dbglab
  • Part 9 · Fix, Prove, Prevent7
  • Part 10 · Case Studies12
  • Part 11 · Debug as a Team5
  • Part 12 · Interview Prep5
Loading lesson

‹Part 8 · Intermittent Failures
  1. Topics
  2. The Art of Hardware Debug
  3. Part 8 · Intermittent Failures
  4. Why Intermittents Happen

Part 8 · Intermittent Failures · Working (W)

Why Intermittents Happen

An intermittent failure is a deterministic bug with an input nobody controls. The five usual hidden inputs, how each shows in a failure rate, and two real runs: a clock ratio that sets a loss rate, and a power-up value that decides pass or fail.

On this page

    Why this matters

    An intermittent failure that is chased as "random" can take weeks and is often closed unexplained. Naming the input that differs between a passing and a failing run turns it back into an ordinary bug with a repro.

    After this lesson you can: name the five usual hidden inputs behind an intermittent failure, tell from its rate and pattern which one is likely, and make that input explicit in a simulation so it fails on demand.

    Note

    Level: Working (W) · Debug-loop step: Reproduce

    Before this: Making a Flaky Test Fail, Determinism and Seeds

    Ninety-nine green nights and one red one

    A block-level regression has run the same 400 tests every night for three months. Last Tuesday one test failed: an event counter in a slow peripheral was one short. Wednesday it passed again, with the same seed, the same commit and the same simulator. In the lab the same counter is one short about once an hour, and never on the same event twice.

    Everyone calls it random. It is not: something was different on Tuesday. What could it be, and how do you find out which?

    What people usually do, and why it fails

    The usual moves are to rerun until it passes, mark the test as flaky, or wait for it to happen again with more logging turned on. Each treats the failure as noise. Rerunning tells you nothing if the input that matters is not the one you control. Waiting is slow, and as a later lesson shows, extra logging can change the very thing you are waiting for.

    The deeper mistake is to treat every intermittent as one kind of problem. A rare stimulus path, two events that collide, two clocks that drift against each other and a flop that powers up wrong all look the same in a pass-fail summary. They need different experiments.

    The method: find the hidden input

    An intermittent failure is a deterministic failure of an input you are not controlling. Logic computes the same answer from the same inputs every time. So list the inputs that can differ between two runs, and find the one that does. Five cover almost every case:

    Hidden input

    What differs between runs

    How it shows

    First experiment

    Stimulus path

    The seed picks a rare sequence

    Same seed fails every time; the rate depends on the stimulus weights

    Measure and bias the rate

    Ordering

    Two independent events land in a different order or the same cycle

    Fails only when latencies or gaps line up

    Control the order

    Clock ratio and phase

    Where the edges of unrelated clocks fall against each other

    Rate set by the frequencies; which event fails moves with the phase

    Sweep the ratio and the phase

    Start value

    What an unreset flop or memory holds at power-up

    X in four-state simulation, passes in two-state, varies on silicon

    Run with random start values

    Marginal timing and environment

    Voltage, temperature, process, a slow path

    Only on silicon or in timed simulation; moves with frequency and voltage

    Move the clock, then the lab

    1. Prove the logic is deterministic in your run. Rerun the same seed, commit and command. If it always fails, the hidden input is the stimulus: go to Part 1's method.

    2. If the same command sometimes passes, list what was not pinned: start values, clock phases, the simulator and its options, files from a previous run, the machine.

    3. Read the rate. A rate that moves with stimulus weights points at a path; one set by two frequencies points at a crossing; one near 50% with a single unreset bit points at a start value.

    4. Make the suspect input explicit. Turn it into a plusarg or a parameter, so a run that failed can be repeated exactly.

    5. Sweep it. A curve that matches a prediction tells you the mechanism before you open a waveform.

    The picture: a slow clock steps over a short pulse

    A one-cycle event of 10 ns and the edges of a 13 ns destination clock, to scale. An event with no destination edge inside it is never sampled; here the event at 35 ns is lost.

    Every 13 ns edge samples whatever is on the wire at that instant. A pulse 10 ns wide contains an edge only if one happens to fall inside it, so the chance of losing it is the share of time not covered: 1 − 10/13, about 23%. The ratio sets the rate. The phase, where the first edge falls, decides which pulses are lost.

    Do it: two hidden inputs, made explicit

    Clock ratio and phase

    The design below takes a one-cycle event from a 10 ns domain into a slower one through two flops. The bench sends 1,000 events at seeded random times and counts how many the slow side saw. The fix, a toggle crossing, turns each event into a change of level, which no edge can step over. The destination period and its phase are plusargs, so both inputs are explicit.

    pulse_to_slow.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
    // pulse_to_slow: a one-cycle event in the src domain, seen in the dst domain through two flops.
    // The bug: the pulse is one src cycle wide, so a slower dst clock can step over it.
    module pulse_to_slow (
      input  logic dst_clk, rst_n, ev,
      output logic seen                           // one dst-cycle pulse per event that got through
    );
      logic s1, s2, s3;
      always_ff @(posedge dst_clk or negedge rst_n)
        if (!rst_n) {s1, s2, s3} <= '0;
        else        {s1, s2, s3} <= {ev, s1, s2};
      assign seen = s2 && !s3;
    endmodule
    
    // The fix: the src side turns each event into a toggle of a level, which dst cannot step over.
    module toggle_to_slow (
      input  logic src_clk, dst_clk, rst_n, ev,
      output logic seen
    );
      logic t, s1, s2, s3;
      always_ff @(posedge src_clk or negedge rst_n)
        if (!rst_n) t <= 1'b0; else if (ev) t <= ~t;
      always_ff @(posedge dst_clk or negedge rst_n)
        if (!rst_n) {s1, s2, s3} <= '0;
        else        {s1, s2, s3} <= {t, s1, s2};
      assign seen = s2 ^ s3;
    endmodule
    
    // ratio_tb: the src clock is 10 ns. Each set sends 1000 events, 8 to 23 src cycles apart (seeded),
    // and counts the events each crossing lost. +DST=<ps> runs one dst period and +PHASE=<ps> delays its first edge; with no plusargs it sweeps the dst period and checks the lesson's claims.
    `timescale 1ns/1ps
    Terminal
    $ iverilog -g2012 -o r.vvp ratio_tb.sv && vvp -n r.vvp
    dst 10000 ps: pulse lost   0 of 1000 ( 0.0%, predicted  0.0%), toggle lost 0
    dst 10100 ps: pulse lost  11 of 1000 ( 1.1%, predicted  1.0%), toggle lost 0
    dst 11000 ps: pulse lost  95 of 1000 ( 9.5%, predicted  9.1%), toggle lost 0
    dst 13000 ps: pulse lost 241 of 1000 (24.1%, predicted 23.1%), toggle lost 0
    dst 16000 ps: pulse lost 381 of 1000 (38.1%, predicted 37.5%), toggle lost 0
    dst 20000 ps: pulse lost 498 of 1000 (49.8%, predicted 50.0%), toggle lost 0
    PASS: the loss rate follows 1 - Tsrc/Tdst, and the toggle crossing loses nothing

    Read the rows as a curve. With equal clocks nothing is lost. A destination only 1% slower loses 1.1% of events: a failure once in a hundred, the kind that survives months of regressions at one fixed ratio. Every row lands within a point or so of the prediction 1 − Tsrc/Tdst, which is the evidence that the mechanism is the one in the picture. Now hold the ratio and move only the phase:

    Terminal
    $ for p in 0 3000 6000 9000; do vvp -n r.vvp +DST=13000 +PHASE=$p; done
    dst 13000 ps, phase 0 ps: pulse lost 208 of 1000 (events 1 7 12 15 18 23 ...), toggle lost 0
    dst 13000 ps, phase 3000 ps: pulse lost 236 of 1000 (events 0 9 10 14 37 39 ...), toggle lost 0
    dst 13000 ps, phase 6000 ps: pulse lost 237 of 1000 (events 3 5 6 8 20 22 ...), toggle lost 0
    dst 13000 ps, phase 9000 ps: pulse lost 246 of 1000 (events 2 11 13 16 19 21 ...), toggle lost 0

    Same stimulus, same ratio, the same rate within sampling noise, and a completely different list of lost events. A chip picks a new phase every time it powers up, so on the bench the lost event moves from run to run and looks random. In simulation the phase is one fixed number unless you make it a knob. Verilator printed identical lines for both commands.

    A start value

    The second design joins pairs of 16-bit halves into words. Its half-select flop ph has no reset, on the argument that it is "just a toggle". Whether it works depends on the value ph powers up with, which no seed controls. The default bench forces both possible values and checks both designs; +ONE leaves ph to the simulator.

    pack2.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
    33
    34
    35
    36
    37
    38
    39
    // pack2: joins two 16-bit halves into one 32-bit word, low half first.
    // The bug: the half-select flop ph has no reset, so the first half lands wherever ph powered up.
    module pack2 (
      input  logic        clk, rst_n, in_valid,
      input  logic [15:0] in_half,
      output logic        out_valid,
      output logic [31:0] out_word
    );
      logic        ph;                            // 0: expecting the low half
      logic [15:0] lo_q;
      always_ff @(posedge clk)                    // no reset: "it is just a toggle"
        if (in_valid) ph <= ~ph;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin out_valid <= 1'b0; lo_q <= '0; out_word <= '0; end
        else begin
          out_valid <= in_valid && ph;
          if (in_valid && !ph) lo_q <= in_half;
          if (in_valid &&  ph) out_word <= {in_half, lo_q};
        end
    endmodule
    
    // The fix: ph is reset with everything else.
    module pack2_fixed (
      input  logic        clk, rst_n, in_valid,
      input  logic [15:0] in_half,
      output logic        out_valid,
      output logic [31:0] out_word
    );
      logic        ph;
      logic [15:0] lo_q;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin ph <= 1'b0; out_valid <= 1'b0; lo_q <= '0; out_word <= '0; end
        else begin
          if (in_valid) ph <= ~ph;
          out_valid <= in_valid && ph;
          if (in_valid && !ph) lo_q <= in_half;
          if (in_valid &&  ph) out_word <= {in_half, lo_q};
        end
    endmodule
    
    Terminal
    $ iverilog -g2012 -o i.vvp init_tb.sv && vvp -n i.vvp
    power-up ph=0: pack2 0 of 8 wrong, pack2_fixed 0 wrong
    power-up ph=1: pack2 8 of 8 wrong, pack2_fixed 0 wrong
    PASS: pack2 works or fails by its power-up state; pack2_fixed works from both
    $ vvp -n i.vvp +ONE
    FAIL: pack2 started with ph=x: 0 words, 0 wrong
    $ vi/Vinit_tb +ONE                          # Verilator, default build
    PASS: pack2 started with ph=0, 8 words right

    Three simulations, three answers. Icarus starts ph at X, and an X toggled stays X, so no word ever comes out: a loud failure. Verilator's default starts it at 0, which happens to be the good value, so the bug is invisible. A chip starts it at whatever the flop settles to. Verilator can model that:

    terminal
    1
    2
    3
    4
    5
    6
    $ verilator --binary --timing --assert -Wno-fatal --x-assign unique --x-initial unique \
        --top-module init_tb init_tb.sv -Mdir vx
    $ for s in $(seq 1 20); do vx/Vinit_tb +ONE +verilator+rand+reset+2 +verilator+seed+$s \
        | grep -E "^(PASS|FAIL)"; done | sort | uniq -c
       8 FAIL: pack2 started with ph=1: 8 words, 8 wrong
      12 PASS: pack2 started with ph=0, 8 words right

    Eight failures in twenty with nothing changed but the start values. That is the bench equivalent of "fails after some power cycles". The +verilator+seed argument makes each start value repeatable, which turns the intermittent back into a failing command: seed 2 fails every time.

    What the evidence says

    Both designs are deterministic. Each failure came from an input that a normal run does not vary: the phase between two clocks, or the value of one flop at time zero. Once that input is a plusarg, the failure repeats on demand, the rate can be predicted, and the fix can be checked against every value of the input, not just the lucky one. The toggle crossing lost nothing at any ratio; the reset ph passed from both start values.

    You are on the Reproduce step of the debug loop: the output is a command that fails every time, for a reason you can name. The rest of this part builds on that: raising a rate on purpose, capturing an event you cannot repeat, and deciding how many clean runs prove a fix.

    Traps

    Common pitfall

    Equal clocks look safe. At exactly 10 ns against 10 ns the bench lost nothing, because the phase never moves. Two oscillators of the same nominal frequency always drift. Sweep a little either side of 1:1 and several phases, or a crossing that is wrong in principle passes in practice.

    Common pitfall

    Two-state simulation hides start values. A pass in Verilator's default mode says the design works when every unreset flop starts at 0, nothing more. Run random start values with several seeds, or check that every flop that matters has a reset (X and reset bugs).

    Common pitfall

    This is not a metastability model. These runs are zero-delay: a flop that samples near an edge gets a clean old or new value. Real synchronisers can also resolve late. The CDC lesson shows a hostile simulation of that; here the point is only how a ratio turns into a rate.

    Why this matters

    Senior lens. In a triage meeting, ask "what input differs between the passing and the failing run?" before anyone opens a waveform. If nobody can answer, the next action is to pin one more input, not to debug.

    Ask for clock ratios, phases and random start values to be plusargs in every bench with more than one clock or any unreset state. They cost a few lines and make whole classes of lab-only failures reproducible in simulation.

    Interview

    Asked: "A counter in a slow clock domain is occasionally one short on the board, never in simulation. What do you suspect and what do you do?"

    Weak answer: It sounds like metastability, so I would add a third synchroniser flop and see if it goes away.

    Strong answer: One short means an event was lost, and the simulation probably never varies the input that loses it. I would first check how events cross: a one-cycle pulse into a slower clock is lost whenever no edge lands inside it, at a rate of about one minus the ratio of the periods. Then I would make the destination period and phase plusargs and sweep them; if the rate matches that prediction I have the mechanism and a repro. The fix is a toggle or handshake crossing, proven at every ratio and phase, not another flop, which only helps with metastability.

    Follow-ups: (1) What rate would you expect with a 10 ns source and a 12 ns destination? (2) Why might the lab see a different rate from your simulation?

    Check yourself A test fails with seed 5 on Monday and passes with seed 5 on Tuesday. Same commit, same simulator version. Which hidden inputs do you check first, and which can you rule out?

    You can rule out the stimulus path: the same seed gives the same stimulus. Check what was not pinned: start values (a two-state run with random initialisation, or an option that changed), the order of processes or a file left by another job, and the simulator options in the run script. A clock phase only differs if the bench draws it from something other than the seed. Compare the full command lines and environments of the two runs before anything else.

    Check yourself With a 10 ns source and a 13 ns destination the bench lost 241 events in the sweep and 208 when run alone with +DST=13000 +PHASE=0. Is one of them wrong?

    Neither. Both are about 23% of 1,000, as predicted, and in the sweep the 13 ns clock starts wherever the previous set left it, so its phase differs and different events fall into the gaps. That is the phase effect the second command shows: 208 to 246 lost depending on the phase. When you compare rates between runs, compare them with their sampling spread, which the statistics lesson makes exact.

    Remember

    • Hardware does not fail at random. Something differs between a passing run and a failing one; the job is to find what.

    • Unrelated clocks: the ratio sets the failure rate and the phase decides which events fail. Silicon picks a new phase at every power-up.

    • An unreset flop takes the value its simulator gives it: X, 0 or random. Only random start values behave like a chip.

    Rule of thumb: before chasing an intermittent, write down which input you think differs between passing and failing runs, and how you will control it.

    How this was checked

    ratio_tb as shown, Icarus Verilog 13.0 (iverilog -g2012 -o r.vvp ratio_tb.sv, vvp -n r.vvp) and Verilator 5.048 (verilator --binary --timing --assert -Wno-fatal --top-module ratio_tb), 2026-10-18, identical lines. With no plusargs: lost 0, 11, 95, 241, 381, 498 of 1000 at dst periods 10.0, 10.1, 11, 13, 16, 20 ns; the toggle crossing lost none. +DST=13000 +PHASE=<ps> for 0, 3000, 6000, 9000: 208, 236, 237, 246 lost, with different first lost events.

    init_tb as shown, same tools: forced power-up ph=0 gives 0 of 8 words wrong, ph=1 gives 8 of 8, the fixed design 0 in both; +ONE in Icarus fails with ph=x; Verilator default passes with ph=0; built with --x-assign unique --x-initial unique and run with +verilator+rand+reset+2 +verilator+seed+<n> for n = 1 to 20: 12 PASS, 8 FAIL.

    Limits: zero-delay simulation with no metastability model, so these show how a clock ratio and a start value set a rate, not how a real synchroniser fails (see the CDC lesson). Marginal timing and the environment are described, not run: they need gate-level timing or silicon.

    Keep going

    • Practice: Waveform debug: missing pulse in CDC due to narrow pulse width

    • Practice: Pulse synchroniser: stretching pulses across clock domains

    • Practice: D flip-flop with asynchronous reset

    • Code lab: CDC pulse stretch

    • Code lab: DFF async reset

    • Interview: run a mock interview on this topic

    Related topics

    • Part 8 · Intermittent Failures

    • Next: making the rate go up

    • CDC bugs: the class

    • CDC/RDC: debug and case studies

    • Bring-up: failure triage in the lab

    Previous

    72 / 107

    Next: Making the Rate Go Up

    Up next

    Up nextMaking the Rate Go Up

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Capturing the Rare Event
    • Statistics of Rare Failures
    • Ordering-Dependent Bugs