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
    • The First Wrong Cycle
    • Bisection in Time
    • Bisection in Space
    • Bisection in Stimulus
    • Bisection in History
    • Bisection Across Configurations and Seeds
    • Cone-of-Influence Tracing
    • Differential Debugging
    • When the Bug Is in the Test
    • The Localisation Checklist
  • 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 3 · Localise
  1. Topics
  2. The Art of Hardware Debug
  3. Part 3 · Localise
  4. The Localisation Checklist

Part 3 · Localise · Foundation (F)

The Localisation Checklist

A one-page checklist for the Localise step: nine checks, each with what "done" looks like and the lesson that teaches it, applied end to end to a real dbglab failure from the stream pins back to one unreset register.

On this page

    Why this matters

    Localising is where most debug hours go, and the hours go into the wrong place when one cheap check is skipped: a commit before that passes, a simulator that disagrees, a checker that is wrong. A short checklist makes each of those a minute's work before anyone opens a waveform viewer.

    After this lesson you can: take a reproduced failure through nine localisation checks, say what finishes each one, and hand over one sentence naming the first wrong signal, its cycle, its module and the change that made it.

    Note

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

    Before this: The Reproduction Checklist, The First Wrong Cycle

    A repro in hand, and a meeting at three

    Part 1's checklist left you a solid repro: tb/run.sh random 23 at HEAD fails every time with STREAM: word #41 = xxxxxxxx, expected 00000000 at cycle 2385. It fails in Icarus, passes in default Verilator, and passes at the commit before. At three o'clock someone will ask where the bug is. Which checks get you from the stream pins to one line of RTL, and how do you know when Localise is done?

    What people usually do, and why it fails

    Most people open the waveform at cycle 2385 and scroll back: the stream word came from the peripheral's FIFO, which a DMA run filled from RAM, which some earlier run wrote. Every hop is a new block, and without a plan the search follows whichever looks most suspicious. Meanwhile the two cheapest facts in hand, a passing commit one step back and a simulator that disagrees, sit unused.

    The checklist

    The checklist finishes the Localise step: its output, one signal, cycle and module, is the input to Hypothesise.

    Work down the table. The order is a default: run the cheap reference rows (5, 6, 8) early if you have them, because each can cut the search to one change or one class of bug before you open a dump.

    #

    Check

    Done when

    Taught in

    1

    Find the first wrong value

    one signal and one cycle where the values first go wrong, walked back from the checker's report

    the first wrong cycle

    2

    Bisect in time

    the earliest cycle at which the state is already wrong, found by halving, not scrolling

    bisection in time

    3

    Bisect in space

    the block whose inputs are right and outputs wrong

    bisection in space

    4

    Bisect in stimulus

    the smallest stimulus with the same signature, and which part of it matters

    bisection in stimulus

    5

    Bisect in history

    the first commit that fails, with the test checking this bug's own signature

    bisection in history

    6

    Vary configurations, seeds and tools

    which settings, seeds and simulators fail, and what they have in common

    bisection across configurations

    7

    Trace the cone

    the short list of registers and logic that could have made the first wrong value, pruned by values

    cone-of-influence tracing

    8

    Compare with a reference

    the first difference from a model, a release or a tool that is not on the allowed-differences list

    differential debugging

    9

    Check the checker

    the spec, quoted, says the design is wrong at that cycle, and the expected value's source is independent of the design

    when the bug is in the test

    The output of the table is one sentence in the bug report: signal S in module M is first wrong at cycle C, since commit K, and the spec says it should be V. That sentence is what Part 4 starts from.

    Do it: seed 23, row by row

    Rows 1 to 3: the first wrong value, in time and in space

    This failure is an unknown value, so "wrong" means X, and one script answers three rows at once. first_x.py, from the cone lesson, reads the failing run's dump and prints, for each signal, when it first went unknown. Pick one signal at each block boundary on the path back from the pin: the stream output, the DMA's read buffer, the RAM's read data, the bus write data, the DMA's fill value, and the CSR registers that feed the DMA.

    Terminal
    $ tb/run.sh random 23
    dbglab: test=random seed=23
    FAIL test=random seed=23 cycle=2385: STREAM: word #41 = xxxxxxxx, expected 00000000
    $ DUMP=1 tb/run.sh random 23 > /dev/null && mv dbglab.vcd ../b4-23.vcd
    $ python3 ../tools/first_x.py ../b4-23.vcd --after 50000 --clock dbglab_tb.cycle dut.pstream_data u_dma.f_dout u_ram.rdata dut.m_wdata u_dma.fill_val_q u_csr.fill_q u_csr.src_q u_csr.dst_q u_csr.len_q
    u_csr.dst_q              unknown from cycle     5 (was -), known again at cycle 21
    u_csr.fill_q             unknown from cycle     5 (was -), unknown to the end
    u_csr.len_q              unknown from cycle     5 (was -), known again at cycle 24
    u_csr.src_q              unknown from cycle     5 (was -), known again at cycle 18
    u_dma.fill_val_q         unknown from cycle    28 (was 0), unknown to the end
    dut.m_wdata              unknown from cycle  2227 (was 97b1334d00000000), unknown to the end
    u_ram.rdata              unknown from cycle  2361 (was cfa1f2e1), known again at cycle 2379
    u_dma.f_dout             unknown from cycle  2365 (was 97b1334d), unknown to the end
    dut.pstream_data         unknown from cycle  2381 (was 2b44c4cd), unknown to the end

    Read it bottom to top, against the data path:

    • Stream pin, cycle 2381; DMA buffer, 2365; RAM read data, 2361. A DMA run read an unknown word from RAM and carried it to the peripheral. RAM was not the source; it was holding something already written.

    • Bus write data, 2227. An earlier DMA run put that unknown word on the bus, 134 cycles before it was read back. That is the first wrong value on the bus (row 1), and it puts the source upstream of the DMA's write side (row 3).

    • The DMA's fill value, from cycle 28; CSR `fill_q`, from reset (cycle 5 is where the window opens). The run at 2227 was a fill run, which writes fill_val_q instead of copied data, and the DMA loads fill_val_q from the FILL register at every start. FILL was never written by software, so it is X from reset to the end.

    • SRC, DST and LEN were also X from reset, but each was written before cycle 25 and known from then on. They are cleared: unknown, but never used while unknown.

    Row 2, bisection in time, is answered by the same output: the state was already wrong at reset, and it first did damage at 2227. A halving search over the dump would land on the same two cycles; here one script read them off directly.

    Row 4: stimulus

    Terminal
    $ tb/run.sh random 23 icarus +OPS=185
    dbglab: test=random seed=23
    FAIL test=random seed=23 cycle=2385: STREAM: word #41 = xxxxxxxx, expected 00000000

    Part 1 shrank the test to 185 operations with the same signature; the run above confirms it. What matters in the stimulus is a fill run that relies on FILL's reset value, then a copy of that buffer into the peripheral.

    Rows 5 and 6: history and the other tools

    Terminal
    $ git checkout -q 03ebdc1 && tb/run.sh random 23
    dbglab: test=random seed=23
    PASS test=random seed=23
    $ git checkout -q b357bd8 && tb/run.sh random 23
    dbglab: test=random seed=23
    FAIL test=random seed=23 cycle=2385: STREAM: word #41 = xxxxxxxx, expected 00000000
    $ git checkout -q main
    $ tb/run.sh random 23 verilator
    dbglab: test=random seed=23
    PASS test=random seed=23 cycles=19235 time=192346 ns
    $ XRAND=1 tb/run.sh random 23 verilator
    dbglab: test=random seed=23
    FAIL test=random seed=23 cycle=2385: STREAM: word #41 = f0db2f1b, expected 00000000

    Commit 14, b357bd8, is the first that fails: csr: no reset on the DMA address and data registers. And the tools split on initial values: default Verilator starts unreset flip-flops at 0 (which happens to be the expected value) and passes; random start values fail at the same cycle with a real number. Both rows point at the same thing: a register that lost its reset.

    Row 7: the cone

    Terminal
    $ cat tools/noreset.ys
    ...
    tee -o noreset.txt select -list t:$dff %co1 w:* %i w:u_ram.mem* %d
    $ yosys -q -s tools/noreset.ys; cat noreset.txt
    Warning: Replacing memory \mem with list of registers. See ys/sync_fifo.sv:67
    Warning: Replacing memory \mem with list of registers. See ys/ram_slave.sv:14
    dbglab_top/u_csr.len_q
    dbglab_top/u_csr.fill_q
    dbglab_top/u_csr.dst_q
    dbglab_top/u_csr.src_q
    dbglab_top/u_dma.u_buf.mem[3]
    dbglab_top/u_dma.u_buf.mem[2]
    dbglab_top/u_dma.u_buf.mem[1]
    dbglab_top/u_dma.u_buf.mem[0]
    $ tools/cone.sh ys dbglab_top u_dma.fill_val_q
    ...
    == u_dma.fill_val_q
    u_dma.state
    u_dma.fill_val_q
    u_csr.fill_q
    u_csr.dma_start

    Yosys calls a flip-flop with no reset a $dff and one with an asynchronous reset an $adff, so the first query lists every register without a reset, leaving out the RAM's words. Eight remain: the DMA's four-word buffer, which is always written before it is read, and four CSR registers. The second query is row 7 proper: the cone one stage behind the DMA's fill value holds four registers, and the only one that was ever unknown is u_csr.fill_q, which is also on the no-reset list.

    Rows 8 and 9: a reference, and the checker

    The commit before is a reference that differs in one commit; comparing a good and a bad run diffs that pair for seed 17 and finds the same register first. Then check the checker:

    Terminal
    $ grep -n "FILL | RW" README.md
    62:| 0x024 | FILL | RW | value written in fill mode. Reset 0 |
    $ grep -n "m_fill    =" tb/dbglab_tb.sv
    69:  logic [31:0] m_fill    = 32'h0;
    145:      CSR_FILL:    m_fill    = data;

    The register map says FILL resets to 0. The bench's model keeps its own 0 (line 69) and changes it only when software writes FILL (line 145); it never reads the RTL's value. The bench's expected 00000000 is the spec's value, so the design is wrong.

    What the evidence says

    Nine rows, each a command or two, and one sentence for the bug report: `u_csr.fill_q` in `csr_block` has no reset since commit 14 (`b357bd8`); it is X from reset, a zero fill drives it onto the bus from cycle 2227 (RAM holds it from 2230), and a later copy carries it to the stream pins at cycle 2381; the register map says FILL resets to 0. Every part of that sentence came from a run on this page. What it does not yet contain is a tested cause; "the missing reset is the whole story" is a hypothesis, and Part 4 tests it before anyone edits the RTL.

    Traps

    Common pitfall

    Stopping at the first block that looks guilty. RAM read data went X at 2361, and RAM is easy to blame. Its input, the bus write data, was X at 2227: RAM stored the damage, it did not make it. Stop only at a block whose inputs were right and whose outputs were wrong.

    Common pitfall

    Clearing a suspect because it is unknown and harmless somewhere else. SRC, DST, LEN and FILL all lost their reset in the same commit, and the first three are written before use. That makes the commit look safe ("software always writes them first"). Check each register on its own: FILL is not written before the fill run that uses it.

    Why this matters

    Senior lens. In a triage meeting, the checklist doubles as a status report: "rows 1, 5 and 6 done: first wrong at 2227 on the DMA write data, since commit 14, and it splits the simulators on initial values; row 9 done, the register map backs the bench" tells the room where the bug is and what is left without anyone opening a waveform.

    Teams that localise fast make the cheap rows free: nightly runs in two simulators and with random initial values, a dump for every failure, and a netlist they can query. Each row then costs a command, not a skill.

    Interview

    Asked: "You have a reliable repro of a data corruption in a block you did not design. How do you find where it starts before reading the RTL?"

    Weak answer: I would open the waveform at the failure and trace the corrupted data backwards through each module until I find where it first goes wrong.

    Strong answer: First the cheap references: does the commit before pass, and does another simulator or a random-initial-value run agree? Those take minutes and can name the change or the bug class. Then I find the first wrong value: I check signals at each block boundary on the data path in the dump and keep the earliest, so I know which block had right inputs and wrong outputs. In that block I list the registers one stage behind the wrong value and clear them by their values. Before blaming the design, I check the checker's expected value against the spec. I stop when I can say which signal, which cycle, which module and, if I have history, which commit.

    Follow-ups: (1) Which row would you skip for a hang, and what replaces it? (2) What if the commit before also fails?

    Check yourself The first-unknown times show a RAM word going X at cycle 2361 and the bus write data at 2227. Your colleague wants to debug the RAM. What do you say?

    The RAM's input was already wrong 134 cycles earlier, so the RAM is not the first block with right inputs and wrong outputs; it stored a bad word. Move upstream to whatever drove the bus write data at 2227 (here a DMA fill run) and repeat until the inputs are right.

    Check yourself Rows 5 and 6 are free on your project: the commit before passes, and the other simulator passes too. Which rows would you still run before saying Localise is done?

    Row 1 (the first wrong signal and cycle) and row 9 (the checker). History names the change and the tool split names the bug class, but neither names the signal and cycle, and neither proves the checker is right. Rows 7 or 8 then turn the changed lines into the one register or term that made the first wrong value.

    Remember

    • Localise is finished when you can name one signal, one cycle and one module where the values first go wrong, and the checker is cleared.

    • Cheap references first: the commit before, the other simulator, the other configuration. Each takes a minute and can cut the search to one change or one class of bug.

    • Walk the cone from the first wrong value, pruned by values, and check the checker before you blame the design.

    Rule of thumb: do not start Hypothesise until you can write the first wrong signal, cycle and module in one sentence.

    How this was checked

    dbglab at HEAD 49c5f04 (rebuilt with dbglab/build-history.sh), 2026-10-17. tb/run.sh random 23 (Icarus Verilog 13.0) and with +OPS=185: same FAIL line. DUMP=1 tb/run.sh random 23 wrote the VCD; python3 first_x.py <vcd> --after 50000 --clock dbglab_tb.cycle with the nine signals shown. tb/run.sh random 23 verilator (Verilator 5.048) passes (cycles=19235); with XRAND=1 it fails at cycle 2385 with f0db2f1b.

    History: git checkout -q 03ebdc1 && tb/run.sh random 23 passes; at b357bd8 it fails with the same line (git 2.54.0). Yosys 0.69+post on the parser-workaround copy of rtl/ described in the cone-of-influence lesson: select -list t:$dff %co1 w:* %i w:u_ram.mem* %d and cone.sh ys dbglab_top u_dma.fill_val_q, output as shown.

    Limits: one failure, an X bug, on a small design; for a wrong known value, rows 1 to 3 need a reference value per signal (row 8). The checklist is a minimum. It does not replace judgement about which row to run first.

    Keep going

    • Practice: Inline checkers: X detection and handshake liveness

    • Practice: Waveform debug: a write transaction timeout

    • Code lab: X-propagation model

    • Interview: run a mock interview on this topic

    Related topics

    • Part 3 · Localise

    • The reproduction checklist: the step before

    • Part 4: hypothesise and test, the step after

    • Symptom, fault and cause: the same register, seen from the symptom

    Previous

    33 / 107

    Next: Part 4 · Hypothesise and Test

    Up next

    Up nextPart 4 · Hypothesise and Test

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The First Wrong Cycle
    • Bisection in Time
    • Bisection in Space