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
    • A Hypothesis Must Predict
    • The Experiment That Splits Suspects
    • Keeping Several Hypotheses Alive
    • Confirmation Bias and Blaming the Tool
    • One Change at a Time
    • The Debug Log
  • 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 4 · Hypothesise and Test
  1. Topics
  2. The Art of Hardware Debug
  3. Part 4 · Hypothesise and Test
  4. A Hypothesis Must Predict

Part 4 · Hypothesise and Test · Foundation (F)

A Hypothesis Must Predict

What makes a hypothesis useful: it names a mechanism and predicts something you have not seen yet. A weak hypothesis for a real dbglab failure explains three runs and helps with none; a strong one predicts a fact, a list of seeds and an exact cycle count, and every prediction is checked by a real run.

On this page

    Why this matters

    A hypothesis that names an area ("the DMA", "timing", "traffic") fits every result, so no run can ever rule it out and the hunt never narrows. One that names a mechanism and predicts an unseen cycle, seed or value can be killed by a single run, which is exactly what makes it worth testing.

    After this lesson you can: turn a vague suspicion into a hypothesis that names a mechanism, write down predictions it makes before running anything, and pick the prediction most likely to prove it wrong.

    Note

    Level: Foundation (F) · Debug-loop step: Hypothesise

    Before this: The Debug Loop, Guessing vs Searching

    Two sentences in a bug ticket

    dbglab's random test fails on seed 19: RAM word 984 reads xxxxxxxx at cycle 415, and the bench says DMA run #3 wrote it last. Two engineers add a line to the ticket.

    One writes: the DMA corrupts data when the CPU is busy on the bus at the same time. The other writes: FILL has no reset, and run #3 is a zero fill that uses it before anyone writes it. Both sound reasonable. Which one can you test, and what would you run first?

    What people usually do, and why it fails

    Most first hypotheses name an area: the DMA, the arbiter, "timing", "traffic". They feel like progress because they point somewhere. But look at what happens when the first engineer tests theirs. Every run below is real:

    Terminal
    $ tb/run.sh dma_copy 19
    dbglab: test=dma_copy seed=19
    PASS test=dma_copy seed=19 cycles=1182 time=11816 ns
    $ tb/run.sh random 18
    dbglab: test=random seed=18
    PASS test=random seed=18 cycles=19229 time=192286 ns
    $ tb/run.sh random 19 icarus +OPS=20
    dbglab: test=random seed=19
    FAIL test=random seed=19 cycle=400: SCOREBOARD (final sweep): RAM word 975 (address 00001f3c) holds xxxxxxxx, expected 00000000 (last written by DMA run #3)

    Run

    Result

    Read as

    Had it gone the other way

    DMA alone (dma_copy)

    PASS

    "it needs CPU traffic"

    "the DMA is broken even alone"

    Another seed (18)

    PASS

    "it depends on the traffic pattern"

    "traffic breaks it often"

    Seed 19, only 20 operations

    FAIL

    "even a little traffic does it"

    "it needs heavy traffic"

    Read the last column. Every result, and its opposite, fits the hypothesis. Three runs later the engineer believes it more and knows nothing new. A hypothesis that forbids no outcome cannot be tested, so it cannot be wrong, so it cannot help. This is the trap guessing vs searching warned about, seen from the other side: the runs were not useless because they were cheap, but because nothing was riding on their results.

    The method: name a mechanism, then predict

    On the debug loop this is Hypothesise, and its output is not a suspect but a prediction. The rules:

    1. Name the mechanism. Which signal has which wrong value, and why. "FILL is X because it has no reset, and a zero fill copies it" is a mechanism; "the DMA corrupts data" is a place.

    2. Derive something you have not seen. A cycle, a seed, a value, a count of words. Predicting the failure you already have ("seed 19 will fail at 415") tests nothing: the hypothesis was built to fit it.

    3. Write it down before the run. In the ticket or the debug log. After the run, memory bends towards the result.

    4. Prefer the risky prediction. The best one would be surprising if the hypothesis were false. A precise number beats "it will still fail".

    5. Say what would kill it. If you cannot name a result that proves it wrong, sharpen it until you can.

    6. A failed prediction is progress. It removes a hypothesis or tells you which part of it is wrong. Change the hypothesis only together with a new prediction.

    The picture: what the strong hypothesis predicted, seed by seed

    Before any sweep, the hypothesis named nine seeds (accent squares). The real runs: those nine fail with X, and no other seed does. The ten seeds with a bar fail with a known wrong value, which this hypothesis does not explain and does not claim to.

    A hypothesis that predicts which seeds fail is easy to kill: one unpredicted X failure, or one predicted seed that passes, and it is wrong. That is what makes the match worth something.

    Do it: three predictions, three runs

    The strong hypothesis, in full: commit 14 took FILL out of the reset block. The bench writes FILL only when its own model of FILL is not already 0, so a zero fill that comes before any write to FILL uses the register's power-up value. In Icarus that is X, and every word of the run is written as X. It came from reading the commit and the bench's zero-fill code, after walking back as in symptom, fault and cause. It predicts three things nobody has looked at yet.

    Prediction 1, a fact: run #3 is a fill with no FILL write before it

    A small probe compiled next to the design lists each DMA run and each write to FILL. It reads the stimulus only; it does not look for X.

    run_log.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
    // not-run: needs dbglab's sources and bench; compile it next to them (command in the Evidence box)
    // run_log: one line per DMA run (its number, mode and length) and one per write to the FILL
    // register. It reads the design's ports and changes nothing. +RUN=<n> stops listing after run n.
    `timescale 1ns/1ps
    module run_log;
      int unsigned run = 0, last;
      bit fill_written = 0;
      initial if (!$value$plusargs("RUN=%d", last)) last = 0;     // 0: list every run
      always @(negedge dbglab_tb.clk) begin                         // mid-cycle: values are settled
        if (dbglab_tb.dut.u_csr.wr && dbglab_tb.dut.u_csr.off == dbglab_pkg::CSR_FILL
            && (last == 0 || run < last))
          $display("cycle %0d: CPU writes FILL = %h", dbglab_tb.cycle, dbglab_tb.dut.u_csr.wdata);
        if (dbglab_tb.dut.u_csr.wr && dbglab_tb.dut.u_csr.off == dbglab_pkg::CSR_FILL) fill_written = 1;
        if (dbglab_tb.dut.u_dma.start && !dbglab_tb.dut.u_dma.busy) begin
          run++;
          if (last == 0 || run <= last)
            $display("cycle %0d: DMA run #%0d starts: %0s LEN=%0d%0s", dbglab_tb.cycle, run,
                     dbglab_tb.dut.u_dma.cfg_ctrl[dbglab_pkg::CTRL_FILL]  ? "fill" :
                     dbglab_tb.dut.u_dma.cfg_ctrl[dbglab_pkg::CTRL_PACED] ? "copy to peripheral" : "copy",
                     dbglab_tb.dut.u_dma.cfg_len,
                     dbglab_tb.dut.u_dma.cfg_ctrl[dbglab_pkg::CTRL_FILL] && !fill_written ? " (FILL never written)" :
                     dbglab_tb.dut.u_dma.cfg_len > 255 ? " (longer than 255)" : "");
        end
      end
    endmodule
    Terminal
    $ sh ../probes/run-probe.sh ../probes/run_log.sv random 19 +RUN=3
    dbglab: test=random seed=19
    cycle 49: DMA run #1 starts: copy to peripheral LEN=5
    cycle 128: DMA run #2 starts: copy LEN=16
    cycle 283: DMA run #3 starts: fill LEN=23 (FILL never written)
    FAIL test=random seed=19 cycle=415: SCOREBOARD: RAM word 984 (address 00001f60) read xxxxxxxx, expected 00000000 (last written by DMA run #3)

    Run #3 is a fill of 23 words, and no FILL write appears before it. Had the probe shown a copy, or a FILL write at cycle 200, the hypothesis would be dead.

    Prediction 2, a list of seeds

    If the mechanism is right, the stimulus alone tells you which seeds fail with X: exactly those that start a zero fill before any FILL write. Written in the ticket before the sweep: the X failures among seeds 1 to 60 are exactly the seeds the probe flags, and no others.

    Terminal
    $ for s in $(seq 1 60); do sh ../probes/run-probe.sh ../probes/run_log.sv random $s | grep -q "FILL never written" && printf "%s " $s; done; echo
    16 17 19 23 29 31 41 43 51
    $ for s in $(seq 1 60); do tb/run.sh random $s | grep FAIL; done | grep xxxxxxxx | sed 's/.*seed=\([0-9]*\).*/\1/' | tr '\n' ' '; echo
    16 17 19 23 29 31 41 43 51

    Nine predicted, nine failed with X, no X failure missed. The ten other failing seeds read a wrong known value; this hypothesis says nothing about them, and they are a different bug (the keeping several hypotheses alive lesson separates the two).

    Prediction 3, a number: the exact cycle count

    Two-state Verilator has no X: its FILL starts at 0, which is what the test expects, so it passes seed 19. If FILL's start value is the only thing wrong, then giving FILL a reset to 0 should make Icarus pass seed 19 in exactly the same number of cycles as two-state Verilator does today. Not roughly: the same number. Any other difference between the runs would show up as a different count.

    Terminal
    $ git diff --stat          # a copy of HEAD with FILL moved into the reset block
     rtl/csr_block.sv | 3 ++-
     1 file changed, 2 insertions(+), 1 deletion(-)
    $ tb/run.sh random 19      # the copy, Icarus
    dbglab: test=random seed=19
    PASS test=random seed=19 cycles=16990 time=169896 ns
    $ tb/run.sh random 19 verilator      # HEAD, unchanged, two-state Verilator
    dbglab: test=random seed=19
    PASS test=random seed=19 cycles=16990 time=169896 ns

    16990 and 16990. Seed 17 gives the same kind of match (18442 in both). This is the riskiest of the three predictions, because it would catch any second difference, and it held.

    What the evidence says

    The strong hypothesis made three predictions that could each have failed, and none did: a fact about run #3, a list of nine seeds out of sixty, and a cycle count shared by two simulators. That is far more convincing than a dozen runs that "fit", and it took five commands. The weak hypothesis made none, so its three runs, though real, moved nothing.

    What it does not prove: that FILL is the only unreset register that matters, or anything about the ten wrong-value failures. Next on the loop is Test in earnest: the experiment that splits suspects when there are several.

    Traps

    Common pitfall

    Predicting what you already saw. "If FILL is the cause, seed 19 fails at cycle 415" is not a prediction: the hypothesis was built from that failure. Predict a different seed, cycle or value.

    Common pitfall

    Rescuing a hypothesis after the fact. When a prediction fails, it is tempting to add a clause ("...unless the CPU is also busy") that makes the result fit. Allowed only if the new version makes a new prediction you then test. Otherwise you are back to a hypothesis that forbids nothing.

    Common pitfall

    A test that cannot fail. Running the fix and seeing PASS on seed 19 alone confirms little: many changes would make that one seed pass. The cycle-count match is what made the fix run a real test.

    Why this matters

    Senior lens. In triage, ask one question of every theory in the room: what does it predict that we have not seen? Theories that cannot answer go to the bottom of the list, however senior their owner. It is the cheapest filter a lead has.

    Ask for the prediction in writing before the experiment. It takes a minute, it stops the room re-reading results to fit, and it makes the debug log useful to the next person.

    Interview

    Asked: "A colleague says the failure is "a timing issue in the DMA". How do you respond?"

    Weak answer: I would agree it could be timing and start looking at the DMA waveforms around the failure to see if anything looks off.

    Strong answer: I would ask what it predicts. "Timing" names an area, not a mechanism: which signal is sampled when, and what value would it have instead? Then I would turn it into something testable, like "the DMA samples LEN one cycle before software writes it, so a run started right after a LEN write uses the old length". That predicts a run length I can check in a log, and which seeds should fail. I would write the prediction down, run the cheapest check, and drop or sharpen the idea depending on the result.

    Follow-ups: (1) What if two hypotheses predict the same result? (2) When is it reasonable to test a weak hypothesis anyway?

    Check yourself Someone writes: "Seed 19 fails because the bus is congested when run #3 starts." Make it into a hypothesis that predicts something.

    Name what congestion would do and where: for example, "when the CPU holds the bus during a DMA write, the write data register is overwritten, so the word written is the CPU's data". That predicts the wrong word equals a value the CPU wrote nearby in time, and that runs with no CPU traffic (dma_copy) never fail. Here the read value is X, which no CPU write can produce, so the sharpened version dies on the first look. That is a good outcome: it took one minute.

    Check yourself Suppose prediction 3 had given 16994 cycles with the fix in Icarus and 16990 in two-state Verilator. What would that tell you?

    That FILL's start value is not the only difference between the two runs. The fix may still be right, but something else (another unreset register, or a difference in how the simulators order events) changes the timing. You would not close the bug: you would find the first cycle where the two runs differ, as in comparing a good and a bad run.

    Remember

    • A hypothesis names a mechanism: which signal, which value, which path. "The DMA is flaky" names a place, not a mechanism.

    • Write the prediction before the run: a cycle, a seed, a value or a count you have not seen yet. Predicting what you already saw tests nothing.

    • Ask of every hypothesis: what result would kill it? If no result would, it is not a hypothesis yet.

    Rule of thumb: no experiment without a written prediction that could have come out wrong.

    How this was checked

    dbglab at HEAD 49c5f04 (rebuilt with dbglab/build-history.sh), Icarus Verilog 13.0 and Verilator 5.048, 2026-10-17. Weak hypothesis runs: tb/run.sh dma_copy 19 (PASS), tb/run.sh random 18 (PASS), tb/run.sh random 19 icarus +OPS=20 (FAIL at cycle 400, final sweep); +OPS=30, 300 and 3000 fail at cycle 415 like the full run.

    Prediction 1: sh ../probes/run-probe.sh ../probes/run_log.sv random 19 +RUN=3 (the probe shown, compiled next to dbglab with iverilog -g2012 -Wno-timescale -I tb <rtl/files.f> tb/bus_rules.sv tb/dbglab_tb.sv run_log.sv). Prediction 2: the same probe over seeds 1 to 60 flags 16 17 19 23 29 31 41 43 51; tb/run.sh random <seed> over 1 to 60 fails with xxxxxxxx in exactly those nine. Prediction 3: on a copy of HEAD with FILL moved into the reset block, tb/run.sh random 19 passes with cycles=16990, as does tb/run.sh random 19 verilator at HEAD; seed 17 gives 18442 in both.

    Limits: one bug, one design, simulation only. The predictions confirm the mechanism for these 60 seeds; they do not show the design has no other X source, and the ten failures with a known wrong value are a different bug.

    Keep going

    • Practice: Reset strategies in RTL

    • Code lab: DFF with asynchronous reset

    • Interview: run a mock interview on this topic

    Related topics

    • Part 4 · Hypothesise and Test

    • Symptom, fault and cause: the walk back that led to FILL

    • X in a waveform: why two-state Verilator passes this seed

    • Next: the experiment that splits suspects

    Previous

    34 / 107

    Next: The Experiment That Splits Suspects

    Up next

    Up nextThe Experiment That Splits Suspects

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Keeping Several Hypotheses Alive
    • Confirmation Bias and Blaming the Tool
    • One Change at a Time