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. Meet dbglab

Part 0 · What Debugging Is · Foundation (F)

Meet dbglab

The course's running design: a two-master bus with a DMA engine, a CSR block, RAM and a stream peripheral in a second clock and power domain, its register map, its self-checking tests, and how to build its history and run them.

On this page

    Why this matters

    You cannot see a wrong value in a design you do not know: debugging starts with a map of the blocks, the addresses and what each test checks. dbglab is the one system this course debugs again and again, so learning it once pays on every page.

    After this lesson you can: draw dbglab from memory, find any register in its map, build its history, run any test in either simulator, and read a PASS or FAIL line and the log it points to.

    Note

    Level: Foundation (F) · Debug-loop step: Before the loop: know the system

    Before this: The Debug Loop

    Your first week on a new block

    A new team hands you a design and a failing regression on the same day. "It is small, just a bus and a DMA." Before you open a waveform you need to know what the blocks are, where each register lives and what each test checks; without that, every signal name is a guess. How do you build that map quickly, and what does it look like for the design this course uses?

    What people usually do, and why it fails

    The usual start is to open the failing waveform and scroll, where every signal looks equally interesting, or to read the RTL top to bottom, which takes a day and teaches every line and none of the structure. Both skip the cheap step: a one-page map built from the top-level file and the register map, checked by running a test that passes. With a map you ask "which block could have written this word?" Without one you ask "what is this signal?"

    The method: a map in four parts

    1. Blocks and connections. From the top-level module only: masters, slaves, clocks, and anything that crosses between them.

    2. The address map. Which address reaches which block, and each register's reset value: the specification you will compare against.

    3. The tests. What each one drives and checks. A test that never reads a register cannot catch that register's bug.

    4. One run that passes. Build, run, read the output: you learn the log format and run time before you need them.

    This comes before the debug loop: you do it once per system, not once per bug.

    The picture: dbglab's blocks

    dbglab at HEAD, drawn from dbglab_top.sv: two masters share bus_fabric (accent), which reaches four slaves; the peripheral's words cross into the pclk domain through an async FIFO to a sink inside a power-gated domain.

    Read it in the order a word travels:

    • Two masters. The CPU port (master 0) is driven by the bench, as software would drive it. dma_engine (master 1) copies words in chunks of up to four through a sync_fifo; software programs it through csr_block, and it raises done and an interrupt at the end.

    • One bus, one transfer at a time. A master holds valid, address and data until ready; each request gets one response pulse. Inside bus_fabric, arb_4 picks the owner round-robin and bus_decode3 selects the slave.

    • Four slaves. A 1024-word RAM, the CSR block, a default slave that answers unmapped addresses with an error (DECERR), and the stream peripheral.

    • A second clock domain. Words written to the peripheral go into an async_fifo, drained on the unrelated pclk by periph_sink, which can be paused, so the bus sees back-pressure; dreq tells the DMA when there is space. The sink's enable crosses through the synchroniser sync2_rtl.

    • A power gate. The sink sits in periph_pd, which software can switch off; isolation clamps its outputs meanwhile. An unpowered domain's outputs are X in Icarus and noise in Verilator, as silicon would drive garbage.

    The FIFOs, synchroniser, arbiter, decoder, RAM and default slave are the RTL course's modules, unchanged: bus integration, the synchronous FIFO, the asynchronous FIFO and the register map and CSR RTL.

    The register map

    Every register is one 32-bit word. This is the project README's map, the specification: when the hardware disagrees with it, one of them is wrong.

    Address

    Name

    Access

    Fields and reset value

    0x2000

    ID

    RO

    0xDB1AB001

    0x2004

    SCRATCH

    RW

    software scratch

    0x2008

    CTRL

    RW

    [0] done_ie, [1] err_ie, [2] paced, [3] dst_fixed, [4] fill. Reset 0

    0x200C

    STATUS

    RO / W1C

    [0] busy, [1] done (write 1 to clear), [2] err (write 1 to clear)

    0x2010

    SRC

    RW

    DMA source byte address

    0x2014

    DST

    RW

    DMA destination byte address

    0x2018

    LEN

    RW

    DMA length in words, 0 to 1023

    0x201C

    START

    WO

    write 1 to start; ignored while busy

    0x2020

    ERR_ADDR

    RO

    address of the access that failed

    0x2024

    FILL

    RW

    value written in fill mode. Reset 0

    0x3000

    DATA

    WO

    push one word; SLVERR and dropped when the FIFO is full

    0x3004

    PSTATUS

    RO / W1C

    [0] full, [1] overflow (write 1 to clear)

    0x3008

    PCTRL

    RW

    [0] sink_en, [1] iso_en, [2] pwr_on. Reset 0x05

    The DMA modes in CTRL: paced waits for dreq before each write, dst_fixed keeps writing one address (the peripheral's DATA port), and fill writes the FILL value instead of copying. irq = (done and done_ie) or (err and err_ie). (The README calls the peripheral's registers STATUS and CTRL; the P here avoids confusion.)

    The tests and what they check

    Test

    What it drives and checks

    smoke

    ID, SCRATCH, a few RAM words, error responses for unmapped addresses

    dma_copy

    memory-to-memory copies of 0 to 64 words

    irq_w1c

    STATUS.done is sticky and write-1-to-clear; irq follows the enables in CTRL

    periph_stream

    CPU writes into the peripheral with flow control, pauses and an overflow

    dma_burst_backpressure

    a paced DMA copy into the peripheral while the sink stops and starts

    periph_power

    power the sink down and up with words waiting in the FIFO

    random

    seeded mixed CPU and DMA traffic, 1500 operations unless +OPS=<n>

    Four kinds of check run in every test, and each prints its own tag in the FAIL line:

    • A reference model of the RAM and registers: every CPU read is compared with it (SCOREBOARD), and the whole RAM at the end (SCOREBOARD (final sweep)). It keeps its own copy of the documented reset values rather than importing the RTL's.

    • Stream checks on the peripheral's pins: order, values, unknowns (STREAM). The interrupt has its own (IRQ).

    • Protocol rules on both master ports (tb/bus_rules.sv): no unknown valid, address or write flag; valid and payload held until ready; a response only for an accepted request. A break prints FAIL bus_rules.

    • A watchdog (WATCHDOG) when nothing moves on the bus or the stream for 2000 cycles, so a hang becomes a failure.

    The seed drives the bench's own xorshift generator, so a seed gives the same stimulus, cycle for cycle, in Icarus and Verilator. Part 1 builds on that.

    Do it: build the history and run a test

    Build the repository

    dbglab is a history you build, not a folder you copy. The script creates a fresh git repository from a patch series with fixed authors and dates, so the hashes match on every machine: when a page names 026f8bf, you have the same commit.

    terminal
    D=scripts/data/hardware-debug-course/dbglab     # in this site's repository
    $D/build-history.sh ~/dbglab                    # the folder must not exist yet
    cd ~/dbglab
    Terminal
    $ git log --oneline --decorate
    49c5f04 (HEAD -> main) tb: report cycles and sim time in the PASS line
    122e88a periph_pd: isolate only the outputs that leave the peripheral
    b357bd8 csr: no reset on the DMA address and data registers
    03ebdc1 dma: one width for the beat counters
    bc2bb34 dma: fill mode
    c4a71b5 dma: keep the bus for a whole chunk
    27acd30 periph: deepen the async FIFO to 16 entries
    026f8bf csr: collect register reset values in dbglab_pkg
    2e63466 Fix comments in the fabric and the DMA; README register map
    a60b6f7 periph: power gate model with output isolation
    246ff00 dma: stop on an error response and report it
    fa71142 (tag: v0.1) tb: seeded random test and regression script
    6e5cfb4 dma: peripheral-paced writes to a fixed destination
    4326801 tb: valid/ready protocol checker and no-progress watchdog
    bf4a3ec Add stream peripheral behind an async FIFO in the pclk domain
    2f08392 Add bus fabric, RAM, CSR block and DMA engine with first tests

    Sixteen commits over about two weeks, read from the bottom. v0.1 is the first release, with a clean regression. The eleven commits after it are ordinary project work: features, refactors, speed-ups. Some of them broke something. The course finds those bugs one at a time, each in the lesson whose method suits it, and does not name them here.

    Run one test

    Terminal
    $ tb/run.sh smoke 1
    dbglab: test=smoke seed=1
    PASS test=smoke seed=1 cycles=76 time=756 ns
    $ tb/run.sh dma_copy 3
    dbglab: test=dma_copy seed=3
    PASS test=dma_copy seed=3 cycles=1182 time=11816 ns
    $ tb/run.sh random 7 verilator
    dbglab: test=random seed=7
    PASS test=random seed=7 cycles=19037 time=190366 ns
    $ ls build
    dbglab.vvp
    icarus-build.log
    icarus-dma_copy-3.log
    icarus-smoke-1.log
    verilator-build.log
    verilator-random-7.log
    vl

    Read it line by line:

    • tb/run.sh <test> [seed] [icarus|verilator], Icarus by default. The first line names the test and seed, so a log always says what produced it.

    • The last line is PASS with the length in cycles and nanoseconds, or FAIL test=... seed=... cycle=...: with the check's tag and message: one greppable line. The exit status is 0, 1, or 125 if it does not build, as git bisect run expects (bisection in history).

    • The full log is in build/<sim>-<test>-<seed>.log. An Icarus run takes well under a second; the first Verilator run compiles (8.2 s in all here), later ones reuse the build.

    • DUMP=1 also writes dbglab.vcd (3.2 MB for a random run that stops at cycle 7,949). +OPS=<n> sets the random test's length; XRAND=1 with Verilator starts unreset flip-flops at random values instead of zero.

    Run the regression at the release

    tb/regress.sh runs every test over a few seeds and exits with the number of failures. At the release:

    Terminal
    $ git checkout -q v0.1
    $ time tb/regress.sh icarus 1 2 3
    smoke                    seed 1      PASS
    smoke                    seed 2      PASS
    smoke                    seed 3      PASS
    dma_copy                 seed 1      PASS
    ...
    dma_burst_backpressure   seed 3      PASS
    random                   seed 1      PASS
    random                   seed 2      PASS
    random                   seed 3      PASS
    0 failing run(s)
    tb/regress.sh icarus 1 2 3  0.93s user 0.25s system 71% cpu 1.661 total

    Eighteen runs, all passing, in under two seconds. Now at the latest commit:

    Terminal
    $ git checkout -q main
    $ tb/regress.sh icarus 1 2 3 | tail -1
    9 failing run(s)

    Nine failing runs out of 21 (a power test arrived after the release). That is where the rest of the course begins; the method comes before the waveform.

    What the evidence says

    Four facts every later lesson leans on. The same command gives the same run, cycle for cycle, in either simulator. A run costs under a second, so experiments are cheap. v0.1 is known good, which turns many questions into "what changed since then?". And every check names what it saw and where, so a FAIL line is already a small report.

    Traps

    Common pitfall

    The map is the specification, not the truth. The register table says what the design should do. When a read disagrees, do not assume either side is right; find out which one moved, and when.

    Common pitfall

    A clean regression is not a clean design. Three seeds per test miss bugs that need another seed or a longer run. Green tells you what was checked, not what is true.

    Why this matters

    Senior lens. On a real project the map is the first thing to ask for, and often nobody has drawn it. Draw it from the top-level file and the register map, have the block owner correct it, and keep it: it is the cheapest debug tool there is.

    Notice what dbglab gives you that many projects do not: stable commit hashes, one command per test, an exit code a script can use, and a seed that means the same thing in two simulators. Insist on all four when you set up a project.

    Interview

    Asked: "You join a team and get a design you have never seen and a failing test. What do you do in the first hour?"

    Weak answer: I would open the waveform at the failure and start tracing signals back until I find something wrong.

    Strong answer: First a map: the top-level blocks and clocks, the address map with reset values, and what the failing test drives and checks. Then I run a passing test to learn the log format, and rerun the failing one with its seed to confirm it fails the same way. Only then do I look at the failure, asking which block could have produced the wrong value.

    Follow-ups: (1) What would you ask the block owner first? (2) How do you check your map is right?

    Check yourself A FAIL line says SCOREBOARD: RAM word 300 read 1234abcd, expected 00000000 (last written by DMA run #5). Using the map, which blocks could have put the wrong value there, and which could not?

    Every RAM write comes through the fabric from a master, so the suspects are the DMA (the model says it wrote last), the CPU port, the fabric and the RAM. The CSR block matters only through what it tells the DMA (length, addresses, fill value). The peripheral, the async FIFO and the pclk domain cannot write the RAM at all. That halves the design before you open a file.

    Check yourself Why does the bench keep its own copy of the register reset values instead of importing them from the RTL package?

    A check that takes its expected values from the thing it checks can never disagree with it. If an RTL reset value changed by mistake and the model imported it, both would change together and every test would pass. An independent copy, taken from the specification, can catch that bug.

    Remember

    • dbglab: two masters (CPU port, DMA) share one valid/ready bus to RAM, CSRs, a default slave and a stream peripheral that crosses into pclk.

    • tb/run.sh <test> [seed] [icarus|verilator] prints one PASS or FAIL line; exit 0, 1, or 125 if it does not build. A seed fixes the stimulus.

    • The history has bugs in it on purpose. v0.1 is clean; later commits are not. The course finds the bugs one at a time.

    Rule of thumb: learn the map before you debug: which block, which address, which test checks it.

    How this was checked

    dbglab built with scripts/data/hardware-debug-course/dbglab/build-history.sh <dir> (git 2.54.0, 2026-10-17): HEAD 49c5f04, tag v0.1 at fa71142, 16 commits. The block diagram is drawn from rtl/dbglab_top.sv, rtl/bus_fabric.sv and rtl/periph_slave.sv at HEAD; the register map is the project README's at HEAD.

    Runs at HEAD with Icarus Verilog 13.0: tb/run.sh smoke 1 (PASS, 76 cycles), tb/run.sh dma_copy 3 (PASS, 1182 cycles); with Verilator 5.048: tb/run.sh random 7 verilator (PASS, 19037 cycles, 8.2 s including the first compile). DUMP=1 tb/run.sh random 17 wrote a 3.2 MB VCD.

    git checkout -q v0.1 && tb/regress.sh icarus 1 2 3: 18 runs, 0 failing, 1.7 s. At HEAD the same command reports 9 failing runs out of 21.

    Limits: dbglab is a teaching system of about 1,500 lines, simulated only. Its bugs are real but planted; a production SoC has more blocks, more clocks and far longer tests.

    Keep going

    • Practice: A memory arbiter for two masters

    • Practice: A simple single-channel DMA controller

    • Practice: A memory-mapped register interface

    • Practice: Asynchronous FIFO with CDC synchronisation

    • Code lab: Round-robin arbiter

    • Code lab: DMA control FSM

    • Code lab: Async FIFO with Gray pointers

    • Interview: run a mock interview on this topic

    Related topics

    • Part 0 · What Debugging Is

    • The debug loop (one dbglab bug walked end to end)

    • A bug report others can act on

    • Bisection in history: using dbglab's commits

    • RTL: bus integration (the fabric, decoder and arbiter)

    Previous

    5 / 107

    Next: A Bug Report Others Can Act On

    Up next

    Up nextA Bug Report Others Can Act On

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Debug Loop
    • Symptom, Fault and Cause
    • The Cost of a Bug by Stage