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. One Change at a Time

Part 4 · Hypothesise and Test · Foundation (F)

One Change at a Time

Why a run that changes two things answers neither question: a two-change patch makes dbglab's deadlock vanish, and one change at a time shows that neither change fixed it. One does nothing; the other removes the back-pressure the test needs. Four real runs, and the cleaner sequence.

On this page

    Why this matters

    When a run changes two things and the failure goes away, you cannot tell which change mattered, or whether one of them only stopped the test from seeing the bug. Shipping that patch ships the bug with a weaker test.

    After this lesson you can: split a multi-change patch or experiment into single changes, run them against the same baseline, and tell a change that fixes a bug from one that does nothing or one that hides it.

    Note

    Level: Foundation (F) · Debug-loop step: Test and Fix

    Before this: Guessing vs Searching, Case study: the deadlock under back-pressure

    The hang is gone. Which change did it?

    dbglab's dma_burst_backpressure test hangs: the watchdog fires at cycle 2165 with the CPU waiting for the bus. An engineer looks at the hang and sees the peripheral FIFO full, so they make it four times deeper. A colleague suggests the DMA should not hold the bus lock while it waits for FIFO space, so that goes in too. One patch, two changes:

    Terminal
    $ git diff
    ...
    -  periph_slave #(.AW(4)) u_per (
    +  periph_slave #(.AW(6)) u_per (
    ...
    -                || (state == WAIT) || (state == WR_REQ) || (state == WR_RSP)) && !chunk_end;
    +                || (state == WR_REQ) || (state == WR_RSP)) && !chunk_end;
    ...
    $ tb/run.sh dma_burst_backpressure 1
    dbglab: test=dma_burst_backpressure seed=1
    FAIL test=dma_burst_backpressure seed=1 cycle=787: the FIFO filled only 0 times: no back-pressure was tested

    No more watchdog. The test now fails for a new reason that sounds like the bench being fussy. Is the deadlock fixed?

    What people usually do, and why it fails

    They change several things at once, because each run seems expensive and every change seems sensible. When the failure goes away they keep the whole patch. Here, the new message looks like a complaint about the test, so the obvious next step is to relax it. On a copy with that check disabled:

    Terminal
    $ tb/run.sh dma_burst_backpressure 1
    dbglab: test=dma_burst_backpressure seed=1
    PASS test=dma_burst_backpressure seed=1 cycles=787 time=7866 ns

    Green. The patch goes to review with the message fixed the DMA deadlock by not locking in WAIT; deeper FIFO for headroom. Three changes now (two RTL, one bench), and the run that "proves" them answers one question, does this combination pass this test?, when the question that matters is which change removed the cause?

    The method: one change, one run, same baseline

    1. Keep the failing command fixed. The same test, seed, simulator and options as the repro. That command is your baseline.

    2. Change one thing per run, then go back to the baseline. Not cumulatively: each change starts from the same tree.

    3. Count everything as a change. RTL, bench, seed, test length, simulator, a tool option, a rebuilt library. git diff --stat before believing a result.

    4. For a patch with n changes, run each alone first. Two changes: neither, A, B, both. Add combinations only if the single runs do not explain the result.

    5. Read a changed message as a changed measurement. A different failure after a change means the test is now seeing something else. Find out what before you edit the check.

    6. Keep the change that removes the cause, and only that, with the test that failed before and passes after.

    The picture: two changes, four runs

    The two changes from the patch as a 2x2 grid of real runs. Moving down a column (dropping WAIT from the lock) changes nothing. Moving across a row (64 entries) changes the message because the FIFO never fills. The fix that removes the cause, in the fabric, is neither of them.

    Read the grid along each direction. Whatever the lock does, the result in a column is identical, so the lock change does nothing to this failure. Whatever the lock does, 64 entries change the message, so the depth is the only change that does anything. What it does is the next question.

    Do it: split the patch

    Change A alone: drop WAIT from the lock

    Terminal
    $ tb/run.sh dma_burst_backpressure 1        # HEAD with only WAIT removed from m_lock
    dbglab: test=dma_burst_backpressure seed=1
    FAIL test=dma_burst_backpressure seed=1 cycle=2165: WATCHDOG: no bus or stream progress for 2000 cycles; cpu_valid=1 cpu_ready=0 cpu_addr=00003008

    The same message at the same cycle as HEAD. This change has no effect on the hang. The case study explains why: the fabric decides whether to keep the grant at the response before WAIT, so what the lock does during WAIT is never looked at.

    Change B alone: 64 entries

    Terminal
    $ tb/run.sh dma_burst_backpressure 1        # HEAD with only the FIFO deepened
    dbglab: test=dma_burst_backpressure seed=1
    FAIL test=dma_burst_backpressure seed=1 cycle=787: the FIFO filled only 0 times: no back-pressure was tested

    This is the change that made the watchdog go away, and the message says how. The test copies 48 words into the peripheral and exists to fill its FIFO so the DMA has to wait. With 64 entries the FIFO never fills, the DMA never waits, and the deadlock, which needs a waiting DMA, never gets a chance. The bench's own guard noticed that its stimulus no longer does its job. Disabling that guard is what turned "the test can no longer see the bug" into PASS.

    Is the bug still there? Make the test reach it again

    A prediction: if the deeper FIFO only hides the deadlock, a copy longer than 64 words will fill it and hang again, with both changes applied. On a copy of the bench with len = 120:

    Terminal
    $ tb/run.sh dma_burst_backpressure 1        # both changes, 120-word copy
    dbglab: test=dma_burst_backpressure seed=1
    FAIL test=dma_burst_backpressure seed=1 cycle=2549: WATCHDOG: no bus or stream progress for 2000 cycles; cpu_valid=1 cpu_ready=0 cpu_addr=00003008

    The same hang, the same waiting CPU write to 0x3008, only later. The patch under review would have shipped the deadlock, a FIFO four times bigger, and a test that can no longer find it.

    The cleaner sequence

    Start from the repro and one hypothesis at a time. The case study's hypothesis is that the fabric keeps the grant for a locked owner at every response, so a DMA that locks and then waits for an event that needs the bus holds everyone off. Its fix is one line, applied alone:

    Terminal
    $ tb/run.sh dma_burst_backpressure 1        # HEAD with only accept = rsp_fire in bus_fabric.sv
    dbglab: test=dma_burst_backpressure seed=1
    PASS test=dma_burst_backpressure seed=1 cycles=772 time=7716 ns
    $ tb/run.sh dma_burst_backpressure 1        # the same fix, 120-word copy
    dbglab: test=dma_burst_backpressure seed=1
    PASS test=dma_burst_backpressure seed=1 cycles=1690 time=16896 ns

    It passes with the original 16-entry FIFO, with the back-pressure check intact (seeds 2 and 3 pass too), and with the longer copy that hangs HEAD at cycle 2165. One change, one run, and the result means something.

    Run

    Changes from HEAD

    Result

    What it tells you

    HEAD

    none

    WATCHDOG 2165

    the baseline

    A

    WAIT dropped

    WATCHDOG 2165

    A has no effect

    B

    64 entries

    FIFO filled 0 times

    B removes the stimulus, not the bug

    A + B, 120 words

    both, longer copy

    WATCHDOG 2549

    the bug survives the patch

    fabric fix

    accept = rsp_fire

    PASS (48 and 120 words)

    the cause

    What the evidence says

    Split into single changes, the patch explained itself in four runs: one change was inert, one moved the test away from the bug, and the real cause was elsewhere. Together they produced a convincing green run. This is the Test step done badly and then well, and the Fix step depends on it: a fix is only the change that removed the cause, proven alone.

    Traps

    Common pitfall

    A seed change is a change. "I applied the fix and the regression is clean" often means the fix and a new seed list, or a shorter run. Rerun the original failing command with the fix first; widen afterwards.

    Common pitfall

    Relaxing a check in the same patch as a fix. A check that starts failing after your change is telling you the test measures something different now. Change the check only in its own commit, with its own reason.

    Common pitfall

    Changes you did not make on purpose. A different simulator version, a bench rebuilt from another branch, a local edit you forgot. Look at git status and the tool versions before trusting a surprising result.

    Why this matters

    Senior lens. Review fixes as experiments. A fix commit should contain the change that removes the cause and the test that failed before and passes after, and nothing else: no refactor, no extra headroom, no relaxed check. Anything else goes in its own commit, so each can be reverted, bisected and understood alone.

    When a teammate reports "it passes now", the useful question is "what changed between the failing run and the passing one?" If the answer is a list, ask for the runs that take its items one at a time.

    Interview

    Asked: "You applied a fix and the failing test now passes. How do you know it was your fix that made it pass?"

    Weak answer: If the test failed before and passes now, the fix worked. I would run the full regression to make sure nothing else broke.

    Strong answer: I check that the passing run differs from the failing one by exactly my change: same test, seed, simulator and bench, with git diff showing only the fix. If my patch has more than one change, I run each alone against the failing command. I also make sure the test still exercises the failing condition, because a change can make a test pass by removing its stimulus. Then I check the fix against a harder version of the same condition, like a longer run, and only then widen to the regression.

    Follow-ups: (1) How would you test two changes that only work together? (2) What if a run takes a day?

    Check yourself Your patch has three changes and the failure is gone. What is the smallest set of runs that tells you which change mattered, and when do you need more?

    Three runs, each change alone against the failing command, plus the baseline you already have. If exactly one of them passes, and the test still exercises the failing condition (check its guards or coverage, and push the stimulus further), that is your fix, and the combined run adds nothing. You need combinations only if no single change passes, which means the effect needs two changes together (an interaction); then try the pairs.

    Check yourself After a fix, the test fails with a new message: "no back-pressure was tested". A reviewer says "the check is too strict, just delete it". What do you reply?

    The check passed before the change and fails after it, so the change altered what the test does: here the FIFO no longer fills, so the condition the test exists for never happens. Deleting the check would hide that. First find what the change did to the stimulus; then either keep the change out of the fix, or adjust the test so it still reaches the condition (a longer copy), in a separate commit.

    Remember

    • One change per run, against the same baseline command. A seed, a simulator, a test length or a bench edit is a change too.

    • A new failure message after a change means the change altered what the test measures. Find out why before you relax a check.

    • Two changes need four runs: neither, each alone, both. It costs minutes and tells you what each one did.

    Rule of thumb: if a run changed two things, it answered no question: split it before you believe it.

    How this was checked

    dbglab at HEAD 49c5f04 (rebuilt with dbglab/build-history.sh), Icarus Verilog 13.0, 2026-10-17, test dma_burst_backpressure. Each change on its own copy of HEAD: periph_slave #(.AW(6)) in dbglab_top.sv (64 entries); WAIT removed from m_lock in dma_engine.sv; both; assign accept = rsp_fire; in bus_fabric.sv. tb/run.sh dma_burst_backpressure 1 gives WATCHDOG at 2165 (HEAD, WAIT only), "filled only 0 times" at 787 (depth only, both), PASS in 772 cycles (fabric fix; seeds 2 and 3: 809 and 772).

    Bench edits on copies: with the bursts < 2 check disabled, both changes PASS (seeds 1 and 2, 787 cycles). With len = 120: both changes WATCHDOG at 2549, HEAD WATCHDOG at 2165, fabric fix PASS in 1690 cycles.

    Limits: one bug, one test, Icarus only (the case study shows the hang is identical in Verilator). The scene's patch is illustrative; every run in it is real.

    Keep going

    • Practice: Round-robin arbiter

    • Practice: FSM deadlock and livelock debugging

    • Code lab: Round-robin arbiter

    • Interview: run a mock interview on this topic

    Related topics

    • Part 4 · Hypothesise and Test

    • Case study: the deadlock under back-pressure

    • Fix the cause, not the symptom

    • Next: the debug log

    Previous

    38 / 107

    Next: The Debug Log

    Up next

    Up nextThe Debug Log

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • A Hypothesis Must Predict
    • The Experiment That Splits Suspects
    • Keeping Several Hypotheses Alive