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
    • The Debug Loop
    • Symptom, Fault and Cause
    • The Cost of a Bug by Stage
    • Guessing vs Searching
    • Meet dbglab
    • A Bug Report Others Can Act On
  • 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
  • Part 9 · Fix, Prove, Prevent7
  • Part 10 · Case Studies12
  • Part 11 · Debug as a Team5
  • Part 12 · Interview Prep5
Loading lesson

‹Part 0 · What Debugging Is
  1. Topics
  2. The Art of Hardware Debug
  3. Part 0 · What Debugging Is
  4. Symptom, Fault and Cause

Part 0 · What Debugging Is · Foundation (F)

Symptom, Fault and Cause

Symptom, fault and cause defined precisely on a real dbglab failure where the wrong value is read 7,461 cycles after it was written, and a real symptom patch that passes every Icarus run and still fails when the hardware powers up at random.

On this page

    Why this matters

    The place a test reports a failure is rarely the place the hardware went wrong, and a fix aimed at the report makes the test pass while the bug ships. Naming the symptom, the fault and the cause separately keeps you walking back until you reach the line that must change.

    After this lesson you can: separate a failure into its symptom, its fault and its cause, measure how far apart they are in time, and tell a fix that removes the cause from a patch that only hides the symptom.

    Note

    Level: Foundation (F) · Debug-loop step: Observe and Localise (what you see versus where it went wrong)

    Before this: The Debug Loop

    A read that fails 8,000 cycles in

    dbglab's random test fails on seed 17. The scoreboard read RAM word 816 at cycle 7949 and got xxxxxxxx, an unknown value, where it expected zero. The message even names the last writer: DMA run #4. A colleague has a fix ready: "the RAM should never store an X; clamp it to zero on the way in." The regression goes green. Is the bug fixed?

    What people usually do, and why it fails

    Most debugging starts where the failure is reported, which is natural: the FAIL line names a module, a cycle and a value. The trap is to stop there. The scoreboard is a reporter. It checks at certain moments, and it reports whatever was wrong by then. The RAM is a carrier: it stored the value it was given. Neither is necessarily where the design went wrong. A change at the reporter or the carrier makes the message go away, and in a four-state simulator that can look exactly like a fix. It is not, and the run below shows how you find out.

    The method: three words, kept apart

    Use three words, each for one thing, and do not let them blur:

    Word

    What it is

    In this failure

    Found by

    Symptom

    Where and when a check, a log or a user first sees something wrong

    Cycle 7949: the scoreboard reads word 816 as X

    Report and Reproduce: the FAIL line

    Fault

    The first wrong value inside the design: one signal, one cycle, one module

    Cycle 5: the FILL register is X where the specification says 0

    Observe and Localise: walk back

    Cause

    The code, decision or assumption that produced the fault; the thing the fix changes

    Commit 14 moved FILL out of the reset block

    Hypothesise and Test, then Fix

    Between the fault and the symptom lies a chain of wrong values, each produced by the one before it. Every link in that chain is a consequence, not a cause. Four rules follow:

    • Write the symptom down exactly and do not edit it into a theory. "Word 816 read X at 7949", not "the RAM is corrupt".

    • Walk back, one link at a time, asking who wrote this value, and was their input right? Stop when a block's inputs are right and its output is wrong. That output is the fault.

    • The cause is in the code or the decision, not in time. Ask why the fault happened: a line, a missing reset, a wrong assumption in a commit message.

    • Test a fix where symptom and cause would disagree. A fix that only hides the symptom must fail somewhere the real fix passes. Find that place before you believe either.

    The steps this lesson sharpens: Observe and Localise walk from the symptom back to the fault, and Fix changes the cause.

    The picture: how far apart they are

    Seed 17 on a linear cycle axis: the fault exists from reset, the bad value reaches the RAM from cycle 488, and the symptom appears at cycle 7949. The cause is a line of code and has no time.

    Almost all of the axis is empty. The design went wrong in the first 500 cycles and said nothing for the next 7,461. A waveform opened at the symptom would show a perfectly healthy RAM read.

    Do it: walk back from the symptom

    The symptom, reproduced

    Terminal
    $ tb/run.sh random 17
    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)

    Read it as a symptom, nothing more: one word, one cycle, one check. The message gives one useful pointer back: the model believes DMA run #4 wrote this word last, so that run is the next link to look at.

    The chain, link by link

    A probe module, compiled next to the design as in the debug loop, prints the FILL register at reset release, every DMA run in fill mode, the first write of an unknown value into the RAM, and the read of word 816. It only reads signals; it changes nothing.

    x_probe.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
    // not-run: needs dbglab's sources and bench; compile it next to them (command in the Evidence box)
    // x_probe: follows an unknown value from the register that holds it to the RAM. It only reads the design.
    `timescale 1ns/1ps
    module x_probe;
      int unsigned run = 0, xwrites = 0;
      bit seen_rst = 1'b0;
      always @(posedge dbglab_tb.clk) begin
        #0.5;                                                     // just after the edge
        if (!seen_rst && dbglab_tb.rst_n) begin
          seen_rst = 1'b1;
          $display("cycle %0d: reset released, FILL register (u_csr.fill_q) = %h", dbglab_tb.cycle, dbglab_tb.dut.u_csr.fill_q);
        end
        if (dbglab_tb.dut.u_dma.start && !dbglab_tb.dut.u_dma.busy) begin
          run++;
          if (dbglab_tb.dut.u_dma.cfg_ctrl[4])
            $display("cycle %0d: DMA run #%0d start, fill mode, LEN=%0d DST=%h FILL=%h", dbglab_tb.cycle, run,
                     dbglab_tb.dut.u_dma.cfg_len, dbglab_tb.dut.u_dma.cfg_dst, dbglab_tb.dut.u_dma.cfg_fill);
        end
        if (dbglab_tb.dut.u_ram.req && dbglab_tb.dut.u_ram.write && $isunknown(dbglab_tb.dut.u_ram.wdata)) begin
          xwrites++;
          if (xwrites == 1)
            $display("cycle %0d: first write of an unknown value: RAM word %0d (address %h) <= %h",
                     dbglab_tb.cycle, dbglab_tb.dut.u_ram.addr[11:2], dbglab_tb.dut.u_ram.addr, dbglab_tb.dut.u_ram.wdata);
        end
        if (dbglab_tb.dut.u_ram.req && !dbglab_tb.dut.u_ram.write && dbglab_tb.dut.u_ram.addr[11:2] == 816)
          $display("cycle %0d: CPU reads RAM word 816 (unknown writes so far: %0d)", dbglab_tb.cycle, xwrites);
      end
    endmodule
    Terminal
    $ ../probes/run-probe.sh ../probes/x_probe.sv random 17
    dbglab: test=random seed=17
    cycle 5: reset released, FILL register (u_csr.fill_q) = xxxxxxxx
    cycle 483: DMA run #4 start, fill mode, LEN=9 DST=00001ca8 FILL=xxxxxxxx
    cycle 488: first write of an unknown value: RAM word 810 (address 00001ca8) <= xxxxxxxx
    cycle 823: DMA run #6 start, fill mode, LEN=17 DST=00001ea4 FILL=3578a189
    ...
    cycle 7947: CPU reads RAM word 816 (unknown writes so far: 9)
    FAIL test=random seed=17 cycle=7949: SCOREBOARD: RAM word 816 (address 00001cc0) read xxxxxxxx, expected 00000000 (last written by DMA run #4)

    Read it backwards, from the symptom to the fault:

    • Cycle 7947: the CPU reads word 816, which the bench reports two cycles later. The RAM returns what it holds. The RAM is a carrier, so keep walking.

    • Cycle 488: DMA run #4 writes X into word 810, the start of a 9-word fill from 0x1ca8. Words 810 to 818 all receive X by cycle 524 (the nine unknown writes the probe counts, word 816 among them). The DMA wrote what it was told to write, so keep walking.

    • Cycle 483: run #4 starts in fill mode with FILL=xxxxxxxx. Fill mode writes the FILL register's value to every word. A zero fill relies on FILL holding its documented reset value of 0, and the bench's model expects zeros for that reason.

    • Cycle 5: reset is released and FILL is already X. The register map says FILL resets to 0. This is the first wrong value: the fault. Nothing before it is wrong, because nothing came before it.

    Notice run #6 at cycle 823: also a fill, with a real value. Software wrote FILL before that run, so it was fine. Only a fill that comes before any write to FILL exposes the fault, which is why this needs a particular seed.

    From the fault to the cause

    Why is FILL unknown after reset? Its register is written here:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet: rtl/csr_block.sv at HEAD, lines 55-64
      // Datapath registers: always programmed before START, so they need no reset.
      always_ff @(posedge clk)
        if (wr) begin
          case (off)
            CSR_SRC:  src_q  <= wdata;
            CSR_DST:  dst_q  <= wdata;
            CSR_LEN:  len_q  <= wdata[9:0];
            CSR_FILL: fill_q <= wdata;
            default: ;
          endcase

    No reset. The comment states the assumption: these registers are always programmed before START. That is true of SRC, DST and LEN, and false of FILL, whose specification promises 0. The commit that made the change, b357bd8 ("csr: no reset on the DMA address and data registers"), is the cause: a wrong assumption written into line 62. How you would find that commit without reading it is bisection in history.

    What the evidence says: test both fixes where they disagree

    Two candidate fixes. The symptom patch makes the RAM store 0 whenever it is handed an unknown value. The cause fix gives FILL its reset back. In Icarus both look identical:

    design.sv
    // snippet: the symptom patch, rtl/ram_slave.sv line 14
            if (write) mem[addr[11:2]] <= $isunknown(wdata) ? 32'h0 : wdata;  // symptom patch
    design.sv
    1
    2
    3
    4
    // snippet: the cause fix, rtl/csr_block.sv: FILL joins the registers with a reset
          err_q <= 1'b0; err_addr_q <= '0; fill_q <= FILL_RST;      // in the reset branch
              CSR_FILL:    fill_q    <= wdata;                         // in the write case, which has a reset
          // ...and the CSR_FILL line is removed from the block without reset

    Run random with seeds 1 to 60 for HEAD and each fix. Then run them again with XRAND=1 in Verilator, which starts every flip-flop without a reset at a random value, the way silicon powers up, instead of at X:

    Version

    Icarus: failing seeds of 60

    Verilator, random power-up: failing seeds of 60

    HEAD

    19

    19

    Symptom patch (RAM stores 0 for X)

    12

    19

    Cause fix (FILL reset to 0)

    12

    12

    In Icarus the two fixes give exactly the same twelve failures, line for line; those twelve are a different bug, the one from the debug-loop lesson. A regression run only in a four-state simulator cannot tell the fixes apart. With random power-up values the patch fixes nothing:

    Terminal
    $ cd symfix && 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 5d88e2dd, expected 00000000 (last written by DMA run #4)
    $ cd ../causefix && XRAND=1 tb/run.sh random 17 verilator
    dbglab: test=random seed=17
    PASS test=random seed=17 cycles=18442 time=184416 ns

    Same cycle, same word, same DMA run, but the value is now 5d88e2dd instead of X. In silicon there is no X: an unreset register powers up at whatever value it lands on. The patch only catches the simulator's picture of the fault. The real chip would fill the buffer with junk, and no check in the RAM can know that the junk is wrong. The cause fix passes, because the register now starts at the value the specification promises.

    Traps

    Common pitfall

    The last writer is not the culprit. The message says last written by DMA run #4, and it is tempting to debug the DMA. The DMA did its job perfectly: it wrote what FILL held. "Who wrote this?" is the right question to walk back with, but the answer is the next place to look, not the verdict.

    Common pitfall

    An X is not always a fault. At reset release, 20 signal names in dbglab are X: the SRC, DST, LEN and FILL registers, the wires that carry them, and the peripheral FIFO's read data. SRC, DST and LEN are never used before software writes them. An unknown that nothing consumes is harmless. The fault is the first unknown that reaches a result, and you find it by walking back from the symptom, not by listing every X.

    Common pitfall

    Words vary by team. The reliability literature says failure for what this course calls a symptom, error for the fault, and fault for the cause. In a bug meeting, define your terms before you argue about which one you found.

    Why this matters

    Senior lens. The distance between fault and symptom is a property of your checks, not of the bug. Here a check on the bus write data (the bench's protocol rules check the address and write flag for X, not the data) would have fired at cycle 488 instead of 7949. When you close a bug, ask what check would have moved the symptom next to the fault, and add it if it is cheap.

    In review, ask the author of a fix one question: "which line made the first wrong value, and does your change touch it?" A fix that lives in a different module from the fault needs a very good reason.

    Interview

    Asked: "What is the difference between a symptom and a root cause? Give an example from your own work."

    Weak answer: The symptom is the error message and the root cause is the bug. You fix the root cause instead of the symptom.

    Strong answer: I separate three things. The symptom is where a check first sees the problem; the fault is the first wrong value inside the design; the cause is the code or decision that produced it. For example, a scoreboard read an X from RAM thousands of cycles in. Walking back, the RAM had been written with X by a DMA fill, which had used a fill register that came out of reset unknown. The cause was a commit that removed that register's reset on the assumption that software always programs it. Clamping X in the RAM made the four-state regression pass, but with random power-up values the test still failed, so the fix had to restore the reset.

    Follow-ups: (1) How do you know you have reached the fault and not just another link in the chain? (2) How would you have caught this earlier?

    Check yourself A stream test fails with STREAM: word #41 = xxxxxxxx, expected 00000000 (dbglab seed 23). Which of the three words does this message give you, and what is your next question?

    Only the symptom: the stream checker saw an unknown word on the output pins. The next question is who put that word into the stream: which bus write sent it to the peripheral, and where did that write's data come from. In this seed the word was copied from a buffer that an earlier zero fill had filled with X, so the chain leads back to the same fault, the FILL register at reset, and the same cause.

    Check yourself A colleague proposes making the scoreboard ignore X values "because they are a simulation artefact". The regression would pass. What would you say?

    That it moves the fix even further from the cause: from the RAM to the checker. The X is the simulator's honest picture of a register with no reset. In silicon that register holds some value, often not 0, and the chip writes it. Ignoring X in the checker hides the only warning you get. Run with random power-up values (Verilator's XRAND=1 here) and the same test fails with a real wrong value.

    Remember

    • Symptom: where a check or a user first sees something wrong. Fault: the first wrong value inside the design. Cause: the code or decision that made it.

    • Walk back from the symptom to the first wrong value. Here that is 7,944 cycles, from the scoreboard at cycle 7949 to an X in the FILL register at reset.

    • A fix at the symptom can pass every test you have and still fail in silicon. Fix the cause, then check the fix in a run that could tell the two apart.

    Rule of thumb: the line you change must be the line that made the first wrong value, not the line that reported it.

    How this was checked

    dbglab at HEAD 49c5f04 (rebuilt with dbglab/build-history.sh), Icarus Verilog 13.0, 2026-10-17. tb/run.sh random 17 fails at cycle 7949 reading RAM word 816 as xxxxxxxx. x_probe.sv, compiled next to dbglab as in the debug-loop lesson (iverilog -g2012 -Wno-timescale -I tb ..., then vvp -n build/probe.vvp +TEST=random +SEED=17), printed the lines shown.

    Fixes, on copies of HEAD: the RAM patch ($isunknown(wdata) ? 32'h0 : wdata in ram_slave.sv) and the cause fix (FILL moved back into the reset always_ff with FILL_RST). Each ran random seeds 1 to 60 in Icarus and with XRAND=1 tb/run.sh random <seed> verilator (Verilator 5.048, --x-initial unique): HEAD 19 and 19 failures, patch 12 and 19, cause fix 12 and 12. The 12 left are another bug's signature.

    Limits: one bug in one small design, in simulation. Verilator's random start values stand in for silicon power-up; real flip-flops are not uniformly random. The guessing in the scene is illustrative.

    Keep going

    • Practice: Reset strategies in RTL

    • Practice: Flip-flop with asynchronous reset

    • Practice: Inline checkers: X/Z detection

    • Code lab: D flip-flop with asynchronous reset

    • Interview: run a mock interview on this topic

    Related topics

    • Part 0 · What Debugging Is

    • The debug loop

    • Guessing vs searching: finding cycle 488 without knowing where to look

    • Part 2: Observe, including X in a waveform

    • RTL: reset architecture

    Previous

    2 / 107

    Next: The Cost of a Bug by Stage

    Up next

    Up nextThe Cost of a Bug by Stage

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Guessing vs Searching
    • Meet dbglab
    • A Bug Report Others Can Act On