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
    • Reading a Waveform with a Question
    • Dump Strategy
    • Logging That Helps
    • Assertions as Always-On Probes
    • Scoreboards Report Late
    • Comparing a Good and a Bad Run
    • X in a Waveform
    • Counters and Performance Probes
    • Observability by Design
  • 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 2 · Observe
  1. Topics
  2. The Art of Hardware Debug
  3. Part 2 · Observe
  4. Reading a Waveform with a Question

Part 2 · Observe · Foundation (F)

Reading a Waveform with a Question

Why browsing a waveform wastes hours, and how one written question picks the lanes and cycles that matter, shown on a real dbglab waveform read twice.

On this page

    Why this matters

    A waveform shows everything the design did, so without a question you read signals that do not matter and chase anything that looks odd. One written question names the signal, the cycle and the value you expect, and that decides what to look at.

    After this lesson you can: turn a failure message into one written question, choose the lanes and cycles it needs, judge each lane right or wrong, and move the question when the window holds no wrong input.

    Note

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

    Before this: The Debug Loop

    Twelve cycles and a red line

    The regression of dbglab has one new failure, in the interrupt test:

    Terminal
    $ tb/run.sh irq_w1c 1
    dbglab: test=irq_w1c seed=1
    FAIL test=irq_w1c seed=1 cycle=84: IRQ: irq=1 while CTRL=00 enables no interrupt

    You dump the run, open the waveform at cycle 84, and add the signals that look relevant: the CPU's bus handshake, the DMA engine's busy and done, the control register, the interrupt. Twelve cycles, six lanes, all of them moving or stuck in ways that could mean something. Where do you look first?

    What people usually do, and why it fails

    Most people browse. They scan the lanes from top to bottom, stop at whatever looks unusual, and build a story around it. A waveform viewer makes this easy: one more signal is one click, and every signal looks equally important on the screen.

    Browsing fails for two reasons. A real design always has something that looks odd: a request waiting, a counter wrapping, a flag that pulses. If you have no question, every odd thing is a candidate, and you follow the first one. And a waveform cannot tell you what is wrong, only what happened. Wrong means different from what was expected, and you cannot judge a lane until you have said what it should be. Browsing skips that step, so it feels like work and gives you nothing to rule out.

    The method: one question, then only what it needs

    This is the Observe step of the debug loop. Its output is the first wrong value you can point at. Five rules get you there:

    1. Write the question first. One signal, one cycle, one expected value: why is `irq` 1 at cycle 84, when CTRL enables no interrupt? The failure message usually gives you all three.

    2. Pick lanes from the logic, not the screen. Keep the questioned signal and the signals it is computed from. Look at the RTL line that drives it, not at the hierarchy browser.

    3. Pick the cycles. Start at the cycle in the question and the cycle before it. Widen only when an input is wrong and you need to see when it went wrong.

    4. Judge each lane: right or wrong, at that cycle. A lane you cannot judge has no place in this question.

    5. Move the question. When you find a wrong input, it becomes the next question: why is this input wrong, and since when? When every input in the window is right, or wrong from the first slot, the cause is outside the window.

    The picture: one real window of dbglab

    This is dbglab's own RTL and bench, at HEAD, running irq_w1c with seed 1: the run that failed above. The window is the last twelve cycles before the failing check, cycles 73 to 84. ctrl is the CSR block's CTRL register.

    Each slot is one bus clock, sampled just after the rising edge; slot 0 is cycle 73 and slot 11 is cycle 84.

    • Slots 0 to 10: the CPU holds cpu_valid high with cpu_ready low; the DMA is busy.

    • Slot 10 (cycle 83): dma_done pulses for one cycle.

    • Slot 11 (cycle 84): the DMA is idle, the CPU gets ready, and irq rises.

    • Every slot: ctrl holds 1 (01 in hex).

    Do it: read the same window twice

    Read 1: browsing

    Read top to bottom, as most people do. The first thing that jumps out is the CPU: cpu_valid is high for twelve cycles and ready never comes until the last slot. That looks like a stalled bus, and a stall is a classic bug, so this is where a browsing reader starts digging. Is the arbiter stuck? Is the DMA hogging the bus? Then dma_done pulses, irq rises, and the CPU is finally served. A plausible story forms: the DMA held the bus too long and something went wrong with the interrupt at the end.

    Notice what that read did not do. It never said what any lane should have been. It spent its attention on the CPU lanes, the most visible thing on the screen, and it ended with a story that cannot be tested.

    Read 2: with the question

    Now write the question down first: why is `irq` 1 at cycle 84, when software has not enabled any interrupt? Then find what irq is made of. One line of the CSR block drives it:

    design.sv
    // snippet: rtl/csr_block.sv at HEAD, line 99
      assign irq      = (done_q & ctrl_q[CTRL_DONE_IE]) | (err_q & ctrl_q[CTRL_ERR_IE]);

    So the lanes that matter are irq, the done flag (set by dma_done the cycle before) and ctrl. The CPU's valid and ready are not in that equation. They may be interesting, but they cannot answer this question, so you stop looking at them. Now judge each lane at cycle 84 and the cycle before:

    Lane

    Value

    Expected

    Verdict

    dma_done, slot 10

    1 for one cycle

    1: the bench started a copy and it finished

    right

    irq, slot 11

    1

    follows the equation from the values above it

    right, given its inputs

    ctrl, slots 0 to 11

    01 (the done-interrupt enable)

    00: the bench has not written CTRL, and the register map says it resets to 0

    wrong

    Three lanes, one verdict each, and the answer is not where the failure is. irq did exactly what its logic says. Its input ctrl is wrong, and it is wrong in every slot. The window holds no cycle where ctrl changes, so the cause is earlier than cycle 73. Rule 5 applies: move the question.

    The next question

    The new question: when did CTRL become 01, and who wrote it? That needs a different view, not a wider window. Twelve cycles of six lanes will not show it; one line per change of CTRL will. A small probe compiled next to the design prints every change of ctrl_q and every CPU write to CTRL (address 0x2008):

    ctrl_watch.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // not-run: needs dbglab's sources and bench; compile it next to them (command in the Evidence box)
    // ctrl_watch: prints every change of the CSR CTRL register and every CPU write to it.
    `timescale 1ns/1ps
    module ctrl_watch;
      always @(dbglab_tb.dut.u_csr.ctrl_q)
        $display("cycle %0d: ctrl_q = %h  (rst_n=%b)", dbglab_tb.cycle, dbglab_tb.dut.u_csr.ctrl_q, dbglab_tb.rst_n);
      always @(posedge dbglab_tb.clk)
        if (dbglab_tb.cpu_valid && dbglab_tb.cpu_ready && dbglab_tb.cpu_write && dbglab_tb.cpu_addr == 32'h2008)
          $display("cycle %0d: CPU writes CTRL = %h", dbglab_tb.cycle, dbglab_tb.cpu_wdata);
    endmodule
    Terminal
    $ ../probes/run-probe.sh ../probes/ctrl_watch.sv irq_w1c 1
    dbglab: test=irq_w1c seed=1
    cycle 1: ctrl_q = 01  (rst_n=0)
    FAIL test=irq_w1c seed=1 cycle=84: IRQ: irq=1 while CTRL=00 enables no interrupt

    Read it line by line. CTRL changed once, at cycle 1, while reset was still asserted. The CPU never wrote it. So CTRL came out of reset as 01, and the register map says 0. The first wrong value is CTRL's reset value; everything in the twelve-cycle window was a consequence of it. Which commit changed the reset value is a job for bisection in history, which finds it in four runs.

    What the evidence says

    The question cut six lanes to three and twelve cycles to two, and then it said something the browsing read never could: the answer is outside this window. That is a real result. It tells you not to widen the waveform around cycle 84, where nothing will change, but to go back to reset.

    And the CPU stall that caught the browsing reader? It is real and it is by design: the DMA keeps the bus for a whole chunk of a copy, so a CPU poll waits. Whether that is acceptable is a fair question, and a later case study shows when it is not. It is not this question, and a browsing reader would have spent the morning on it.

    Every waveform hunt follows the same shape: question, lanes from the logic, verdicts, then either a wrong input to follow or a new question about an earlier time. For the waveform patterns that recur in RTL (a missing default, a blocking-assignment race), see the RTL course's waveform debug lessons, and for walking a wrong value back cycle by cycle, Part 3: Localise.

    Traps

    Common pitfall

    Judging a lane at the wrong side of the edge. This waveform shows each value just after the rising edge. The bench's check, like an assertion, samples just before it. So irq is 1 in slot 11, and the check that fails reads it at the next edge, which the bench reports as cycle 84. When a check and a waveform seem to disagree by one cycle, check which side of the edge each one reads.

    Common pitfall

    Absence in the window is not absence. No CPU write to CTRL appears in cycles 73 to 84, and that proves nothing about cycles 0 to 72. Only the probe, which watched the whole run, can say never. Before you say a signal never did something, check that you watched the whole time it could have.

    Why this matters

    Senior lens. Ask for the question before you look at someone's waveform. When a junior engineer asks for help with a waveform, the most useful first reply is: what value did you expect, on which signal, at which cycle? Half the time, writing that down answers it.

    Put the question in the bug report too. "irq is 1 at cycle 84; CTRL should be 00 but has been 01 since reset" lets the next person start from your verdicts instead of browsing the same window again.

    Interview

    Asked: "Here is a waveform of a failing test. Find the bug."

    Weak answer: I would go through the signals one by one and look for anything that seems wrong, like a stuck handshake or a glitch, and then dig into whatever looks suspicious.

    Strong answer: First I ask what the failure says: which signal was wrong, at which cycle, and what was expected. Then I find the RTL line that drives that signal and keep only its inputs on the screen. I judge each input at that cycle against what it should be. If one is wrong, it becomes my next question and I walk back. If they are all right, or one is wrong from the first cycle of the window, the cause is outside the window, so I ask about an earlier time rather than staring harder at this one.

    Follow-ups: (1) What do you do if the expected value is not written down anywhere? (2) How do you decide how many cycles to dump?

    Check yourself In the window above, a colleague says the bug must be in the arbiter, because the CPU waits twelve cycles for ready. What question would you ask them, and what would settle it?

    Ask: what value should `cpu_ready` have had, and at which cycle? If they cannot say, the wait is not evidence. The failing check is about irq, and irq's equation does not contain the CPU handshake, so the arbiter cannot explain it. The DMA holds the bus for its chunk by design. A long wait is a performance question with its own expected value, worth its own report.

    Check yourself Suppose ctrl had been 00 in slots 0 to 10 and 01 in slot 11. What would your next question be, and which window would you look at?

    Then ctrl changed inside the window, at cycle 84, so the next question is who wrote CTRL at cycle 83 or 84? You would add the bus lanes that can write it (the CPU's write handshake, address and data) for those two cycles. The question, not habit, decides that the CPU lanes now belong on the screen.

    Remember

    • Before you open the waveform, write the question: which signal, which cycle, what value you expected.

    • Keep the lanes the questioned signal is computed from. A lane you cannot call right or wrong does not belong on the screen.

    • If every input is right in the window, or wrong in every slot, the cause is outside the window. Ask a new question about an earlier cycle.

    Rule of thumb: if you cannot say what value a lane should have, it is not part of your question.

    How this was checked

    dbglab at HEAD 49c5f04 (rebuilt with dbglab/build-history.sh), Icarus Verilog 13.0, 2026-10-17. tb/run.sh irq_w1c 1 prints FAIL test=irq_w1c seed=1 cycle=84: IRQ: irq=1 while CTRL=00 enables no interrupt.

    Waveform: COURSE=dbg node scripts/tools/gen-waves.mjs observe:reading-a-waveform-with-a-question on wave-src/reading-a-waveform-with-a-question/irq-early.sv, a wrapper that includes dbglab's RTL and bench unchanged, selects irq_w1c (seed 1) and ends at 840 ns, before the failing check. Edges 72 to 83, the bench's cycles 73 to 84. It matched its expectations and a second run changed nothing. A per-cycle probe over cycles 0 to 90 gave the same values.

    Second question: ctrl_watch.sv compiled next to dbglab with iverilog -g2012 -Wno-timescale -I tb and run with vvp -n build/probe.vvp +TEST=irq_w1c +SEED=1 printed one change of ctrl_q (to 01, at cycle 1 with rst_n=0) and no CPU write to CTRL before the failure.

    Limits: no waveform viewer is installed here, so the waveform is drawn from the VCD by the course's tools; the method is the same in any viewer. One bug, one window, simulation only.

    Keep going

    • Practice: Waveform debug: a write transaction timeout

    • Practice: FSM illegal-state debugging: waveform analysis and assertions

    • Interview: run a mock interview on this topic

    Related topics

    • Part 2 · Observe

    • Dump strategy: what to dump, and when

    • Part 3: Localise

    • RTL: waveform bug patterns (latch and race)

    • RTL: debug hierarchy before touching code

    Previous

    15 / 107

    Next: Dump Strategy

    Up next

    Up nextDump Strategy

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Logging That Helps
    • Assertions as Always-On Probes
    • Scoreboards Report Late
    cpu_validcpu_readydma_busydma_donectrl1irq
    dbglab, irq_w1c seed 1, cycles 73 to 84 (slots 0 to 11): the CPU's handshake, the DMA's busy and done, CTRL and irq