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. X and Reset Bugs

Part 5 · Bug Classes · Working (W)

X and Reset Bugs

X and reset bugs: a register with no reset, a reset value that changed, and a reset that never acted because the clock was stopped. How each one differs between four-state, two-state and random-start simulation, the first three checks, and a reset-value check that catches two dbglab bugs at cycle 5.

On this page

    Why this matters

    Reset bugs are written once and paid for at every power-up: a register that starts with the wrong value or with whatever the silicon gives it. Each kind looks different in four-state, two-state and random-start simulation, and running all three tells you which kind you have in minutes.

    After this lesson you can: tell a missing reset, a wrong reset value and a reset that never acted apart from how they behave in three kinds of simulation, check every register against its documented reset value, and fix the cause.

    Note

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

    Before this: X in a Waveform, Logic Bugs

    Two reset bugs, two different faces

    Monday's regression has two failures that both mention reset in the triage notes. irq_w1c fails at cycle 84 on every seed, in Icarus and in Verilator. random seed 17 reads xxxxxxxx in Icarus and passes in Verilator. One engineer calls both "X problems"; another says the second is a simulator quirk.

    Are they the same class, and is there one check that finds both before any test fails?

    What people usually do, and why it fails

    They treat every reset bug as an X bug, and every X as something to waive or initialise away. A wrong reset value never shows an X, so X checks miss it; a missing reset shows X only in four-state simulation, so a two-state run hides it. Both are found late, by whichever test happens to read the register first.

    The signature

    Run the failing test three ways: four-state (Icarus), two-state (Verilator, flops start at 0) and two-state with random start values (dbglab's XRAND=1). The pattern of results names the kind.

    Kind

    Four-state

    Two-state

    Random start

    Shows

    No reset (and read before written)

    X

    often passes (0 happens to be right)

    fails on some seeds, with a random value

    when the value is first used

    Wrong reset value

    fails

    fails, same cycle

    fails, same cycle

    on the first read or use after reset

    Reset never acted (sync reset, no clock edge)

    X

    often passes

    fails on some seeds

    from the moment reset ends

    X hidden by an if/else

    clean output from an X input

    clean

    depends on the value

    X in a waveform

    On the debug loop this is Observe: the three runs are an observation, not a fix, and they cost three commands.

    The first three checks

    1. Run four-state, two-state and random start. Same test, same seed. Fails everywhere at the same cycle: a wrong value. Fails only where the start is unknown: a register that was never reset.

    2. Check every register one cycle after reset. Compare each one with its documented reset value, as a probe or a directed test. This finds both kinds at once, at the first cycle.

    3. List the flops without a reset, with Yosys (the localisation checklist shows the query). For each one, show that a write always comes before the first read. Then check the clock runs while reset is held.

    One real reproduction: a reset that never acted

    This fresh case is the third kind. A busy flag uses a synchronous reset. The bench holds reset while the clock is still stopped, as it is before a PLL locks, releases it, and only then starts the clock. FIX = 0 is the synchronous version; FIX = 1 (the default) resets asynchronously.

    busy_flag.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
    40
    41
    42
    // syncrst: a busy flag whose reset needs a clock edge. The bench holds reset while the clock is
    // still stopped (as before a PLL locks), releases it, then starts the clock. FIX = 0 builds the
    // flag with a synchronous reset, which never sees an edge during reset; FIX = 1 (the default)
    // uses an asynchronous reset.
    `timescale 1ns/1ps
    module busy_flag #(parameter bit FIX = 1) (input logic clk, rst_n, set, clr, output logic busy);
      if (FIX) begin : g_async
        always_ff @(posedge clk or negedge rst_n)
          if (!rst_n)   busy <= 1'b0;
          else if (set) busy <= 1'b1;
          else if (clr) busy <= 1'b0;
      end else begin : g_sync
        always_ff @(posedge clk)                    // bad: reset only acts on a clock edge
          if (!rst_n)   busy <= 1'b0;
          else if (set) busy <= 1'b1;
          else if (clr) busy <= 1'b0;
      end
    endmodule
    
    module tb;
      parameter bit FIX = 1;
      logic clk = 1'b0, clk_on = 1'b0, rst_n, set = 1'b0, clr = 1'b0, busy;
      int bad = 0;
      busy_flag #(.FIX(FIX)) u (.clk, .rst_n, .set, .clr, .busy);
      always #5 if (clk_on) clk = ~clk; else clk = 1'b0;
    
      initial begin
        rst_n = 1'b0;                               // reset from time 0, clock stopped
        #30 rst_n = 1'b1;                           // reset released at 30 ns, still no clock
        #1  if (busy !== 1'b0) begin bad++; $display("at %0d ns, after reset, before the clock: busy=%b", $time, busy); end
        #20 clk_on = 1'b1;                          // clock starts at 51 ns
        repeat (3) @(posedge clk);
        #1  if (busy !== 1'b0) begin bad++; $display("at %0d ns, three idle cycles later: busy=%b", $time, busy); end
        set = 1'b1; @(posedge clk) #1 set = 1'b0;
        if (busy !== 1'b1) bad++;
        clr = 1'b1; @(posedge clk) #1 clr = 1'b0;
        if (busy !== 1'b0) bad++;
        if (bad) $fatal(1, "busy was not 0 after reset in %0d checks", bad);
        $display("PASS: busy is 0 after reset with no clock, then sets and clears (FIX=%0d)", FIX);
        $finish;
      end
    endmodule
    Terminal
    $ iverilog -g2012 -P tb.FIX=0 -o s0.vvp syncrst.sv && vvp -n s0.vvp
    at 31 ns, after reset, before the clock: busy=x
    at 76 ns, three idle cycles later: busy=x
    FATAL: syncrst.sv:38: busy was not 0 after reset in 2 checks
           Time: 96000  Scope: tb
    $ verilator --binary --timing --assert -Wno-fatal --top-module tb -GFIX=0 -Mdir vl0 syncrst.sv > /dev/null 2>&1 && vl0/Vtb
    PASS: busy is 0 after reset with no clock, then sets and clears (FIX=0)
    ...
    $ verilator --binary --timing --assert -Wno-fatal --x-assign unique --x-initial unique --top-module tb -GFIX=0 -Mdir vlx syncrst.sv > /dev/null 2>&1
    $ vlx/Vtb +verilator+rand+reset+2 +verilator+seed+1
    at 31 ns, after reset, before the clock: busy=1
    at 76 ns, three idle cycles later: busy=1
    [96000] %Fatal: syncrst.sv:38: Assertion failed in tb: busy was not 0 after reset in 2 checks
    ...
    $ vlx/Vtb +verilator+rand+reset+2 +verilator+seed+2
    PASS: busy is 0 after reset with no clock, then sets and clears (FIX=0)
    ...
    $ iverilog -g2012 -P tb.FIX=1 -o s1.vvp syncrst.sv && vvp -n s1.vvp
    PASS: busy is 0 after reset with no clock, then sets and clears (FIX=1)

    Read the four runs as check 1 says. Four-state: busy is X after reset and still X three idle cycles later, because nothing writes it until set. Two-state: a pass, because the flop starts at 0, which is the reset value. Random start: seeds 1, 3, 5 and 7 power the flag up as 1 and fail, seeds 2, 4, 6 and 8 pass. That is the signature of a register that was never reset, although the RTL has a reset branch. The reset was there; no clock edge ever came while it was held. The asynchronous version passes in all three.

    The same class in dbglab: B1 and B4 in one check

    dbglab has one bug of each of the first two kinds. B1 (commit 9) changed CTRL's reset value from 0 to 1, and bisection in history found the commit. B4 (commit 14) removed FILL's reset, and X in a waveform traced its X. Their runs show the two signatures from the table:

    Terminal
    $ tb/run.sh irq_w1c 1; tb/run.sh irq_w1c 1 verilator
    dbglab: test=irq_w1c seed=1
    FAIL test=irq_w1c seed=1 cycle=84: IRQ: irq=1 while CTRL=00 enables no interrupt
    dbglab: test=irq_w1c seed=1
    FAIL test=irq_w1c seed=1 cycle=84: IRQ: irq=1 while CTRL=00 enables no interrupt
    $ tb/run.sh random 17; tb/run.sh random 17 verilator; XRAND=1 tb/run.sh random 17 verilator
    dbglab: test=random seed=17
    FAIL test=random seed=17 cycle=7949: SCOREBOARD: RAM word 816 (address 00001cc0) read xxxxxxxx, expected 00000000 (last written by DMA run #4)
    dbglab: test=random seed=17
    PASS test=random seed=17 cycles=18442 time=184416 ns
    dbglab: test=random seed=17
    FAIL test=random seed=17 cycle=7949: SCOREBOARD: RAM word 816 (address 00001cc0) read 5d88e2dd, expected 00000000 (last written by DMA run #4)

    B1 fails identically in both simulators: a wrong value. B4 is X, then a pass, then a random value: no reset. Now the second check, a probe that reads every register with a documented reset value one cycle after reset and compares it with the register map in dbglab's README:

    reset_check.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // not-run: needs dbglab's sources and bench; compile it next to them (command in the Evidence box)
    // reset_check: one cycle after reset release, compare every software-visible register that the
    // register map gives a reset value with that value. It only reads the design.
    `timescale 1ns/1ps
    module reset_check;
      task automatic chk(string name, logic [31:0] got, logic [31:0] want);
        if (got !== want) $display("RSTCHK cycle=%0d: %s = %h after reset, register map says %h",
                                   dbglab_tb.cycle, name, got, want);
        else              $display("RSTCHK cycle=%0d: %s = %h ok", dbglab_tb.cycle, name, got);
      endtask
      initial begin
        @(posedge dbglab_tb.rst_n);
        @(posedge dbglab_tb.clk) #1;
        chk("CSR SCRATCH", dbglab_tb.dut.u_csr.scratch_q, 32'h0);
        chk("CSR CTRL",    {24'h0, dbglab_tb.dut.u_csr.ctrl_q}, 32'h0);
        chk("CSR STATUS",  {29'h0, dbglab_tb.dut.u_csr.err_q, dbglab_tb.dut.u_csr.done_q, 1'b0}, 32'h0);
        chk("CSR FILL",    dbglab_tb.dut.u_csr.fill_q, 32'h0);
        chk("PER CTRL",    {24'h0, dbglab_tb.dut.u_per.pctrl_q}, 32'h5);
      end
    endmodule
    Terminal
    $ ../probes/run-probe.sh ../probes/reset_check.sv irq_w1c 1
    dbglab: test=irq_w1c seed=1
    RSTCHK cycle=5: CSR SCRATCH = 00000000 ok
    RSTCHK cycle=5: CSR CTRL = 00000001 after reset, register map says 00000000
    RSTCHK cycle=5: CSR STATUS = 00000000 ok
    RSTCHK cycle=5: CSR FILL = xxxxxxxx after reset, register map says 00000000
    RSTCHK cycle=5: PER CTRL = 00000005 ok
    FAIL test=irq_w1c seed=1 cycle=84: IRQ: irq=1 while CTRL=00 enables no interrupt

    Both bugs, at cycle 5, in a test that only ever sees one of them. B1's test fails 79 cycles later; B4's first failure, on seed 17, comes 7,944 cycles later and only on a seed that draws a zero fill. At commit 03ebdc1, before FILL lost its reset, the same probe reports CTRL alone. The register map is the reference, not the package of reset values: a check that imported the package would agree with B1.

    The fix pattern

    • No reset: give the register its reset back, or prove and assert that every read is preceded by a write. B4's fix puts FILL into the reset block.

    • Wrong value: fix the constant against the specification, and make the check read the specification's value, not the RTL's.

    • Reset never acted: use an asynchronous reset for state that must be valid before the clock runs, or keep reset asserted for a few cycles after the clock starts. Reset sequencing is a system rule; write it down.

    Preventing it

    • A reset-value test that reads every register after reset against the register map. It would have caught B1 and B4 on the day they were committed.

    • Random start values in regression: run some seeds two-state with random initial values, so a missing reset fails somewhere even without a four-state simulator.

    • An assertion after reset: !$isunknown on every output and state register from the first clock after reset release.

    • Review questions: "What does this register hold before its first write, and who reads it before then?" and "Is the clock running while reset is held?"

    Traps

    Common pitfall

    A bench reset that never happens. Setting rst_n to 0 in its declaration creates no falling edge in Icarus, so even an asynchronous reset stays X; Verilator passes. Drive reset with an assignment at time 0, or start it at X, so negedge rst_n really fires.

    Common pitfall

    Initialising the X away. Adding initial busy = 0; makes every simulator pass. Synthesis at best keeps it as an initial-value attribute, which an FPGA can load and an ASIC flip-flop cannot. Fix the reset, not the simulation.

    Why this matters

    Principal lens. Removing resets from wide datapath registers saves area and is often right, but only under a written rule ("written before first use") with an assertion that checks it. B4's commit relied on that rule for SRC, DST and LEN, where it held, and for FILL, where it did not.

    Make power-up behaviour part of sign-off: a reset-value test, random-start seeds, and a check of the reset sequence against clock start-up. These cost little before tape-out, and a reset bug in silicon fails at every power-up.

    Interview

    Asked: "A test fails with an X in one simulator and passes in another. Another test fails identically in both, right after reset. Are these the same bug?"

    Weak answer: Both are reset problems, so I would initialise all the registers in the testbench and rerun.

    Strong answer: Probably not. Failing identically in both simulators means a wrong value, such as a reset constant that does not match the specification. Failing with X in four-state and passing in two-state means a register that was never reset, either because it has no reset or because the reset never acted. I confirm the second with random start values. Then one check covers both: read every register one cycle after reset and compare it with the register map. I fix the reset itself, never with initial blocks, which an ASIC cannot honour.

    Follow-ups: (1) How can a register with a reset branch still power up unknown? (2) Why compare with the register map rather than the RTL's reset package?

    Check yourself A flag with a synchronous reset is X after reset in Icarus, although the reset was held for 30 ns. What do you check first?

    Whether the clock was running while reset was held. A synchronous reset acts only on a clock edge, so a reset that ends before the first edge does nothing. Look at the clock during reset in the same run; if it is stopped, either reset asynchronously or hold reset for several cycles after the clock starts.

    Check yourself Your reset-value probe reports every register as correct, yet a two-state run with random start values still fails on some seeds. What is left?

    A register the probe does not cover: one with no documented reset value (a datapath register, a FIFO entry, an internal state) that is read before it is written. List the flops without reset with Yosys, and for each one show a write before the first read, or give it a reset.

    Remember

    • Missing reset: X in four-state, a pass in two-state, a seed-dependent failure with random start values. Wrong reset value: the same failure everywhere.

    • A synchronous reset needs a clock edge while reset is held. If the clock starts after reset ends, the flop was never reset.

    • Read every register one cycle after reset and compare it with the register map. It is the cheapest test in the plan.

    Rule of thumb: for every register, know its value one cycle after reset and who reads it before its first write.

    How this was checked

    syncrst.sv as shown, 2026-10-18: iverilog -g2012 -P tb.FIX=0 then vvp -n (Icarus Verilog 13.0) fails with busy=x; verilator --binary --timing --assert -Wno-fatal --top-module tb -GFIX=0 (Verilator 5.048) passes; built with --x-assign unique --x-initial unique and run with +verilator+rand+reset+2 +verilator+seed+N, seeds 1, 3, 5, 7 fail with busy=1 and 2, 4, 6, 8 pass. FIX=1 passes in all three, random seeds 1, 3, 5 checked.

    dbglab at HEAD 49c5f04: reset_check.sv compiled next to the bench (iverilog -g2012 -Wno-timescale -I tb -o build/probe.vvp <rtl/files.f> tb/bus_rules.sv tb/dbglab_tb.sv reset_check.sv, vvp -n build/probe.vvp +TEST=irq_w1c +SEED=1) reports CTRL and FILL at cycle 5; at commit 03ebdc1 only CTRL. tb/run.sh irq_w1c 1 and 2 fail at cycle 84 in both simulators; tb/run.sh random 17 fails with an X, passes in Verilator, and fails with a random value with XRAND=1.

    Limits: simulation only. Random start values are one model of power-up, not all of them. Reset synchronisers and reset-domain crossings are a separate class, covered in reset-domain crossing bugs.

    Keep going

    • Practice: Reset strategies in RTL: sync, async, gated

    • Practice: X and Z in simulation and the mismatches they cause

    • Practice: Reset value testing of registers

    • Code lab: DFF with asynchronous reset

    • Interview: run a mock interview on this topic

    Related topics

    • Part 5 · Bug Classes

    • RTL: X-propagation discipline

    • X in a waveform

    • RTL: clock and reset best practices

    Previous

    42 / 107

    Next: Races and Simulation-Synthesis Mismatches

    Up next

    Up nextRaces and Simulation-Synthesis Mismatches

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Logic Bugs: Off-by-One, Width and Sign
    • Latches and Combinational Loops
    • CDC Bugs