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. The Cost of a Bug by Stage

Part 0 · What Debugging Is · Working (W)

The Cost of a Bug by Stage

Why the same bug costs minutes in RTL and a respin in silicon: the stages, what each run costs and who it involves (illustrative numbers, labelled), and a real comparison of one dbglab failure localised with full visibility and with pass or fail only.

On this page

    Why this matters

    A bug that takes an afternoon in RTL simulation can take weeks and a mask change in silicon, because every later stage runs slower, involves more people and shows you less. Knowing the curve tells you where to spend effort and why moving a failure back to simulation is usually the first move.

    After this lesson you can: list the stages from RTL to the field with what a debug run costs and what it shows at each, and explain with a real example why the same bug needs more experiments when visibility shrinks.

    Note

    Level: Working (W) · Debug-loop step: The whole loop (why each step gets dearer later)

    Before this: The Debug Loop, Symptom, Fault and Cause

    Why does the same bug cost a week?

    Two engineers meet the same kind of bug, a register left out of reset. The first sees it as an X in her block simulation on a Tuesday afternoon and fixes it before she goes home. The second meets it as "the buffer sometimes holds garbage after power-up" from the bring-up lab, three months after tape-out. Same line of RTL. Why does the second one cost a week of five people's time and, possibly, a new set of masks?

    What people usually do, and why it fails

    Teams under schedule pressure treat verification as a gate to pass rather than a search for bugs: "the regression is green, ship it to integration". Bugs are then found wherever they happen to show, and each one is debugged with whatever that stage offers. The cost is invisible because it lands on other people, later. The usual result is a bug found in emulation or the lab that an assertion or a targeted test would have shown in block simulation in minutes.

    The method: know the curve, act on it

    Every stage of a chip's life can find bugs. What changes from one to the next is how long a debug run takes, how many people must be involved, how much of the design you can see, and what a fix costs. The table is illustrative: real numbers depend on the project, the team and the tools, but the direction is the same everywhere.

    Stage

    One debug run (illustrative)

    Who gets involved (illustrative)

    What you can see

    What a fix costs (illustrative)

    RTL, your own simulation

    seconds to minutes

    you

    every signal, every cycle

    an edit and a rerun: minutes

    Block DV regression

    minutes to an hour

    designer and DV engineer

    every signal, but long tests and large dumps

    an edit, a review, a regression: hours

    Integration (subsystem or SoC simulation)

    hours

    several block owners

    dumps limited to windows; logs from many blocks

    a change that other blocks must re-verify: days

    Gate-level simulation

    hours to a day

    adds synthesis and physical design

    netlist names, X pessimism, timing

    re-synthesis or a netlist edit (ECO): days to weeks

    Emulation or FPGA prototype

    fast runs, hours to rebuild

    adds the emulation team

    trigger windows and chosen probes

    an RTL fix plus a rebuild: about a week

    Silicon, bring-up lab

    a lab session

    lab, firmware, design, DV, product

    pins, registers read by software, scan dumps, trace buffers

    a metal fix or a respin: months, plus mask cost

    Field

    whatever the customer can tell you

    adds support and the customer

    a description of the symptom

    a workaround, an erratum or a recall

    Three rules follow from the curve:

    1. Spend effort early. A check that finds a bug in RTL is worth more than the same check at integration, because every later run costs more. Assertions, lint and targeted tests are cheapest where the curve is lowest.

    2. Move the failure back. When a late stage finds a bug, the first job is to reproduce it in the cheapest stage that can show it, usually simulation. Debugging in a late stage is a last resort. Part 6 covers this for each stage.

    3. Buy visibility before you need it. Later stages see less. Debug registers, counters and trace hooks designed in at RTL are what let the lab see anything at all.

    The picture: the cost curve

    The relative cost of a bug by stage, on a logarithmic axis. Illustrative only: the factor-of-ten steps show the usual shape, not measured data.

    The vertical axis is logarithmic, so each grid line is ten times the one below. The rule of thumb many teams use, "ten times dearer per stage", is a habit of speech rather than a law, but no project reports the curve going down.

    Do it: one bug, two levels of visibility

    Most of that curve cannot be run here: there is no emulator, no gate-level flow for dbglab and no lab. One part of it can be measured honestly: what happens to the number of experiments when visibility drops. Take one real failure, dbglab's random test with seed 17, and localise it twice.

    With full visibility: one dump, one script

    In simulation you can record every signal. DUMP=1 writes a VCD; a short script then asks the question that matters for an unknown value: which signals were unknown when reset ended, and which turned unknown later, in what order?

    example.py
    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
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    # not-run: reads the VCD that dbglab writes with DUMP=1 (output below is from a real run)
    """Where do unknowns come from? Reads a VCD and lists, for the design under dbglab_tb.dut:
      1. the signals still unknown (x or z) when reset is released;
      2. the signals that turn unknown later, after holding a known value, earliest first.
    usage: first_x.py <file.vcd> [how-many]
    Cycle n is the time from the n-th rising edge of dbglab_tb.clk to the next, as the bench counts."""
    import sys
    
    path, limit = sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 12
    names, scope, vals = {}, [], {}
    at_reset, later = [], {}                     # later: name -> (cycle, time)
    clk_id = rst_id = None
    released, cycle, rst_cycle = False, 0, 0
    unknown = lambda v: any(c in "xXzZ" for c in v)
    
    def step(t, changes):                        # all changes of one time step; the clock edge first
        global cycle, released, rst_cycle, at_reset
        for v, code in changes:
            if code == clk_id and v == "1" and vals.get(code) == "0": cycle += 1
        for v, code in changes:
            if code == rst_id and v == "1" and not released:
                released, rst_cycle = True, cycle
                at_reset = sorted(n for c, val in vals.items() if unknown(val) for n in names.get(c, []))
            if released and unknown(v) and code in vals and not unknown(vals[code]):
                for n in names.get(code, []): later.setdefault(n, (cycle, t))
            vals[code] = v
    
    with open(path) as f:
        for line in f:                           # header: identifier code -> full names
            w = line.split()
            if not w: continue
            if w[0] == "$scope": scope.append(w[2])
            elif w[0] == "$upscope": scope.pop()
            elif w[0] == "$var":
                full = ".".join(scope + [w[4]])
                if full.startswith("dbglab_tb.dut."): names.setdefault(w[3], []).append(full)
                if full == "dbglab_tb.rst_n": rst_id = w[3]
                if full == "dbglab_tb.clk": clk_id = w[3]
            elif w[0] == "$enddefinitions": break
        t, changes = 0, []
        for line in f:                           # value changes, one time step at a time
            line = line.strip()
            if not line or line[0] == "$": continue
            if line[0] == "#":
                step(t, changes); t, changes = int(line[1:]), []; continue
            changes.append(tuple(line[1:].split()) if line[0] in "bBrR" else (line[0], line[1:]))
        step(t, changes)
    
    print(f"unknown at reset release (cycle {rst_cycle}): {len(at_reset)} signals")
    for n in at_reset[:limit]: print("  " + n)
    if len(at_reset) > limit: print(f"  ... {len(at_reset) - limit} more")
    print(f"turned unknown later: {len(later)} signals, earliest first")
    for n, (c, tt) in sorted(later.items(), key=lambda kv: (kv[1][1], kv[0]))[:limit]:
        print(f"  cycle {c:5d}  {n}")
    Terminal
    $ DUMP=1 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)
    $ python3 ../tools/first_x.py dbglab.vcd 12
    unknown at reset release (cycle 4): 20 signals
      dbglab_tb.dut.dma_dst
      dbglab_tb.dut.dma_fill
    ...
      dbglab_tb.dut.u_csr.dst_q
      dbglab_tb.dut.u_csr.fill_q
      dbglab_tb.dut.u_csr.len_q
      dbglab_tb.dut.u_csr.src_q
      ... 8 more
    turned unknown later: 19 signals, earliest first
      cycle    31  dbglab_tb.dut.u_dma.fill_val_q
      cycle   486  dbglab_tb.dut.m_wdata
      cycle   486  dbglab_tb.dut.u_dma.m_wdata
      cycle   486  dbglab_tb.dut.u_dma.wdata_q
      cycle   486  dbglab_tb.dut.u_fab.m_wdata
      cycle   488  dbglab_tb.dut.s_wdata
    ...
      cycle   488  dbglab_tb.dut.u_ram.wdata
      cycle  7948  dbglab_tb.dut.cpu_rdata

    Read the second list as a chain. The DMA's copy of the fill value goes unknown at cycle 31, when DMA run #1 latches it (harmless until a fill uses it). At cycle 486 the DMA's write data goes unknown, at 488 it reaches the RAM's write port, and at 7948 the RAM's read data, which the bench reports a cycle later. The first list says where the chain can start: four CSR registers (SRC, DST, LEN and FILL) are unknown when reset ends, with the wires that carry them and the peripheral FIFO's read data, and the chain uses one of them, u_csr.fill_q. (Reset is released during cycle 4; the probe in the previous lesson samples at the next edge and says cycle 5.) One simulation and one script, together about 0.4 seconds, name the signal and the cycle. (The register map says FILL resets to 0, which makes this the fault; the previous lesson walks the same chain with a probe.)

    With pass or fail only

    Now take away the inside view, as the lab would. Silicon has no X: an unreset flip-flop powers up at whatever value it lands on. Verilator's XRAND=1 mode models that by starting every unreset flip-flop at a random value, chosen by a seed. Run the same test, then power it up five more times with different start values:

    Terminal
    $ 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)
    $ for n in 1 2 3 4 5; do build/vl/Vdbglab_tb +TEST=random +SEED=17 +verilator+rand+reset+2 +verilator+seed+$n | grep -E '^(PASS|FAIL)'; done
    FAIL test=random seed=17 cycle=7949: SCOREBOARD: RAM word 816 (address 00001cc0) read 8f27a164, expected 00000000 (last written by DMA run #4)
    FAIL test=random seed=17 cycle=7949: SCOREBOARD: RAM word 816 (address 00001cc0) read 0b8ad843, expected 00000000 (last written by DMA run #4)
    FAIL test=random seed=17 cycle=7949: SCOREBOARD: RAM word 816 (address 00001cc0) read 1c50f669, expected 00000000 (last written by DMA run #4)
    FAIL test=random seed=17 cycle=7949: SCOREBOARD: RAM word 816 (address 00001cc0) read 3e4e8947, expected 00000000 (last written by DMA run #4)
    FAIL test=random seed=17 cycle=7949: SCOREBOARD: RAM word 816 (address 00001cc0) read b95cf52d, expected 00000000 (last written by DMA run #4)

    A different wrong value at every power-up. Without the word xxxxxxxx there is nothing to say "uninitialised"; it looks like memory corruption, or a flaky board. And this is still generous: a real chip has no scoreboard naming the DMA run that last wrote the word.

    Suppose all you have is whether a run passes, and one knob, the number of operations. The halving search from guessing vs searching still works:

    Terminal
    $ XRAND=1 SIM=verilator python3 ../search/ops_search.py halve 17 1500
    run    1: OPS= 750 FAIL   suspects left:  750 (ops 1..750)
    run    2: OPS= 375 FAIL   suspects left:  375 (ops 1..375)
    ...
    run   10: OPS=  44 pass   suspects left:    2 (ops 45..46)
    run   11: OPS=  45 FAIL   suspects left:    1 (ops 45..45)
    11 runs: operation 45 is the first that leaves the design wrong

    Eleven runs to learn that operation 45, a DMA start, leaves the RAM wrong. That is an operation, not a signal. The next experiments would need hypotheses: read the DMA's registers back through software after reset, try the run with FILL written first, and so on, each one another run.

    What the evidence says

    Full visibility (simulation, 4-state)

    Pass or fail only (random power-up)

    Runs to localise

    1 run and 1 script

    11 runs, then more for each hypothesis

    Where it points

    u_csr.fill_q, unknown from reset; the chain to cycle 7948

    operation 45, a DMA start

    What the symptom looks like

    an X: plainly uninitialised

    a different wrong value every power-up

    Measured compute time here

    about 0.4 s

    about 0.3 s for the 11 short runs, after a 9 s build

    In this design every run is cheap, so both columns finish in seconds. Multiply the run counts by the illustrative cost of a run at each stage: 11 runs at a second each is nothing; 11 lab sessions with a team at the bench is a week. That is how the curve is made: lower visibility multiplies the number of runs, and later stages multiply the cost of each run.

    Traps

    Common pitfall

    "It is cheaper to find it later, when everything is integrated." Integration does find bugs that blocks cannot, such as two blocks with different ideas of a protocol. But a bug that a block test could have shown costs more there, because each run is slower and each dump is a window, not the whole run.

    Common pitfall

    Treating simulation's X as the truth about silicon. Four-state simulation shows an unreset register as X, which is a gift: it says unknown. Silicon gives a value. A check that passes in a two-state run with every register at 0 (Verilator's default) may fail on the bench. Run with random start values before you trust it.

    Why this matters

    Principal lens. The curve is the business case for verification and for design-for-debug. When you argue for an assertion library, a formal run on a control block or debug registers that cost area, argue in curve terms: the illustrative ratio between an RTL bug and a respin is several orders of magnitude, so a check that removes one late bug pays for a great deal of early work.

    Track, for every escaped bug, the earliest stage that could have found it and the stage that did. The gap between the two, summed over a project, is the most honest measure of a verification plan; Part 9 turns it into an escape analysis.

    Interview

    Asked: "Why is finding a bug in silicon so much more expensive than finding it in RTL simulation?"

    Weak answer: Because you have to do a respin, and masks are expensive.

    Strong answer: The fix is only part of it. Each debug run is slower and needs a lab and several teams, and you see much less: pins, registers software can read, scan dumps and trace buffers instead of every signal. Less visibility means more experiments to localise the same bug, and each experiment costs more, so the costs multiply. Then the fix is a metal ECO or a respin, plus re-verification and schedule. That is why the first step with a silicon failure is to reproduce it in simulation, where the same bug may take one dump to localise.

    Follow-ups: (1) What would you build into a design to make silicon debug cheaper? (2) When is it right to debug on the emulator rather than in simulation?

    Check yourself A bug was found in emulation after a three-week block-level campaign that passed. The block engineer says block DV is therefore pointless. What is wrong with the argument?

    One escape does not show the stage is useless; it shows a gap in it. The useful question is whether a block test or assertion could have caught this bug, and why it did not (a missing scenario, a seed that never reached it, a check that did not look). Block DV likely found many bugs that never reached emulation, each at a fraction of the cost. Close the gap at block level, so the next bug like it is found where it is cheap.

    Check yourself In the lab, a buffer "sometimes holds garbage after power-up". What would you do first, and why?

    Move it back to simulation, the cheapest stage that can show it. "After power-up" and "sometimes" suggest a register or memory that depends on its start value. Run the failing scenario in simulation with four-state X (an unknown would show up plainly) and with random start values, then localise with a full dump. Lab time is better spent confirming the fix than finding the cause.

    Remember

    • Each stage multiplies the cost of one run, the people involved and the size of the fix. The numbers vary by project; the shape does not.

    • Visibility falls as cost rises: every signal in RTL, pins and scan dumps in silicon. Fewer things seen means more experiments per bug.

    • On dbglab's seed 17, one dump and one script found the first X in under a second; pass or fail only took 11 runs and still named no signal.

    Rule of thumb: find it in the cheapest stage that can show it, and move any later failure back there before you debug it.

    How this was checked

    Every cost, time and team size in the table and the figure is illustrative, chosen to show the usual shape; none is measured or taken from a real project.

    Full visibility, dbglab HEAD 49c5f04, Icarus Verilog 13.0, 2026-10-17: DUMP=1 tb/run.sh random 17 (0.18 s, a 3,192,373-byte VCD) then python3 first_x.py dbglab.vcd 12 (the script shown, 0.19 s).

    Pass or fail only, Verilator 5.048 with XRAND=1 (unreset flip-flops start random, standing in for power-up): XRAND=1 tb/run.sh random 17 verilator, then the built binary run with +verilator+seed+1 to +5 gave five different wrong values; XRAND=1 SIM=verilator python3 ops_search.py halve 17 1500 (from the guessing-vs-searching lesson) took 11 runs to name operation 45. Gate level, emulation and silicon were not run.

    Keep going

    • Practice: Inline checkers: X/Z detection and bindable monitors

    • Interview: run a mock interview on this topic

    Related topics

    • Part 0 · What Debugging Is

    • Guessing vs searching: the halving search used here

    • Part 6: debug by stage

    • Bring-up: failure triage in the lab

    • Bring-up: debug interfaces

    Previous

    3 / 107

    Next: Guessing vs Searching

    Up next

    Up nextGuessing vs Searching

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Debug Loop
    • Meet dbglab
    • A Bug Report Others Can Act On