You own dbglab's CSR block. A ticket arrives overnight: "irq test broken after the DMA changes, please fix ASAP. Probably the arbiter giving the DMA priority. Fails in regression." You do not know which commit, which seed, which simulator or what "broken" means. You do not even know whether it fails on your machine. How long before you can start debugging, and how much of what it says can you trust?
A report is usually written in a hurry, by someone who has just spent an hour on the failure and assumes the reader shares their context. Three things go wrong. The facts are missing: no commit, seed or command, so the reader cannot reproduce it and starts a round of questions. The theory replaces the symptom: "probably the arbiter" in the first line sends the reader to the arbiter, and if the guess is wrong that is where their morning goes. And the work already done is lost: the reporter tried other seeds and the other simulator, but the reader does not know that and repeats it.
The report is the first step of the debug loop. Its output, the failure as expected versus actual with the command that shows it, is the input to every step after it. When it is wrong, the whole loop starts in the wrong place.
Field | What goes in it | Why the reader needs it |
|---|---|---|
Title | The symptom: which test, what was wrong, where. No cause. | It is what people search, skim and triage on. A cause in the title is believed before it is tested. |
Expected versus actual | What the specification or the test says should happen; what happened, with the exact message | "Broken" is not a fact. The gap between the two is the bug's definition. |
Repro command | A command that can be pasted: repository and commit, test, seed, options, simulator and version | It turns the report into an experiment anyone can run. It is also the command that will prove the fix. |
First observed | The first run that failed and the last that passed, with their commits or dates | It bounds the search in history before anyone opens a waveform. |
Frequency | Every run, or n out of m, over which seeds and simulators | A deterministic failure and a 1-in-500 failure are debugged in completely different ways. |
Attachments | The log, the dump or how to make one, the bisect log if any | Evidence the reader would otherwise have to regenerate. |
Already ruled out | What you tried and what each result showed | It stops the next person repeating your hour, and tells them which explanations are already dead. |
Guesses (optional, labelled) | A suspected area, marked as a guess, kept below the facts | Useful context, as long as nobody mistakes it for a finding. |
Four rules for filling them in:
Copy, do not paraphrase. The FAIL line goes in exactly as printed. Paraphrase loses the cycle, the address and the value.
Run the repro from a clean checkout before you file. A command that only fails in your working tree, with your local edits or stale build, is not a repro.
Separate observed from inferred. Facts first; anything you concluded goes under its own heading and says how sure you are.
One bug per report. Two failure signatures are two reports until someone shows they share a cause. They can link to each other.
Here is the failure behind the 9 a.m. ticket, reported first the way it arrived and then the way it should have.
Field | Bad report | Good report |
|---|---|---|
Title | irq test broken after DMA changes | irq_w1c: irq asserts after the first DMA copy while CTRL enables no interrupt (every seed, HEAD 49c5f04) |
Expected versus actual | "broken" | Expected: the register map gives CTRL a reset value of 0, so the copy sets STATUS.done and irq stays low. Actual: |
Repro | none |
|
First observed | "after the DMA changes" | Nightly of 2026-03-10 at 026f8bf (fails at cycle 116 there). Passed on 2026-03-09 at 2e63466, the commit just before; 026f8bf is the only change between them. |
Frequency | "fails in regression" | Every run: seeds 1, 2 and 3 all fail at cycle 84; same in Verilator 5.048 |
Attachments | none |
|
Ruled out | nothing stated | Not the simulator (Verilator identical). Not the seed (1 to 3). Not a local setup (fresh build of the history). Not the bench: 026f8bf changes only |
Guess | "probably the arbiter", in the first line | None offered. 026f8bf is a CSR refactor, so the CSR block's reset values are worth a look first (a guess, not a finding). |
Every claim in the bad report is either unfalsifiable ("broken") or a theory ("the arbiter"), and its one hint, "after the DMA changes", is wrong: the commit that changed the result touched no DMA code. The good report is longer, and every line is something the reader would otherwise have had to find out.
Start from the regression's FAIL line, then turn it into a command that works from nothing. dbglab rebuilds its history with fixed hashes, so a fresh folder is a fair stand-in for a colleague's machine:
$ R=$(mktemp -d)/repro
$ scripts/data/hardware-debug-course/dbglab/build-history.sh $R > /dev/null && cd $R && git checkout -q 49c5f04 && tb/run.sh irq_w1c 1; echo "exit $?"
dbglab: test=irq_w1c seed=1
FAIL test=irq_w1c seed=1 cycle=84: IRQ: irq=1 while CTRL=00 enables no interrupt
exit 1Same message, same cycle, exit status 1, on a tree built from scratch. That one line is the most valuable thing in the report. Note what it pins: the repository (rebuilt from its patches), the commit by hash, the test, the seed and, by default, the simulator. The version of the simulator goes next to it, because a different version can change the result.
iverilog -V 2>&1 | head -1 # Icarus Verilog version 13.0 (stable) (v13_0)
verilator --version # Verilator 5.048 2026-04-26$ tb/run.sh irq_w1c 2
dbglab: test=irq_w1c seed=2
FAIL test=irq_w1c seed=2 cycle=84: IRQ: irq=1 while CTRL=00 enables no interrupt
$ tb/run.sh irq_w1c 3
dbglab: test=irq_w1c seed=3
FAIL test=irq_w1c seed=3 cycle=84: IRQ: irq=1 while CTRL=00 enables no interrupt
$ tb/run.sh irq_w1c 1 verilator
dbglab: test=irq_w1c seed=1
FAIL test=irq_w1c seed=1 cycle=84: IRQ: irq=1 while CTRL=00 enables no interruptEvery seed and both simulators, at the same cycle. That is a deterministic failure that does not depend on the stimulus, which already tells the reader something: whatever is wrong happens on the test's fixed opening sequence, before random choices matter.
dbglab's regression runs nightly, one commit a day. Run the test at the last nightly that passed and the first that failed:
$ git checkout -q 2e63466 && tb/run.sh irq_w1c 1
dbglab: test=irq_w1c seed=1
PASS test=irq_w1c seed=1
$ git checkout -q 026f8bf && tb/run.sh irq_w1c 1
dbglab: test=irq_w1c seed=1
FAIL test=irq_w1c seed=1 cycle=116: IRQ: irq=1 while CTRL=00 enables no interrupt
$ git show --stat --format="%h %ad %s" --date=short 026f8bf
026f8bf 2026-03-10 csr: collect register reset values in dbglab_pkg
rtl/csr_block.sv | 4 ++--
rtl/dbglab_pkg.sv | 7 +++++++
2 files changed, 9 insertions(+), 2 deletions(-)Two facts for the report. First observed at 026f8bf, last passed at 2e63466, with no commit between them. And the failing cycle is 116 there and 84 at HEAD: the same message at a different cycle. Say so in the report, so nobody greps for cycle=84 in old logs and concludes the failure is new. The --stat line belongs in ruled out: the change touched no bench file and no DMA file.
Here the history was short enough to check by hand. With a longer gap you would let bisection in history find the commit and attach its git bisect log.
$ git checkout -q main && 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
$ cat build/icarus-irq_w1c-1.log
dbglab: test=irq_w1c seed=1
FAIL test=irq_w1c seed=1 cycle=84: IRQ: irq=1 while CTRL=00 enables no interrupt
FATAL: tb/dbglab_tb.sv:57: test failed
Time: 845000 Scope: dbglab_tb.fail
$ DUMP=1 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
$ wc -c < dbglab.vcd
49131Back at HEAD, the log is four lines, so paste it. The dump is small here (49 KB); on a real project, attach the command that makes it rather than gigabytes of waveform.
Look at what the good report already did to the search. Every seed, both simulators rules out the stimulus and the tool. One commit separates pass from fail, touching only two CSR files puts the cause, almost certainly, inside a change of nine added and two removed lines. The reader can start at Observe with a short list of suspects, and the bad report's arbiter theory is dead before anyone looked at the arbiter. None of this needed a waveform. Writing a careful report is often the first half of localising the bug.
Whether you are on the same commit with no local edits (git status, git rev-parse HEAD); whether a stale build or a leftover environment variable or plusarg is changing your run (for dbglab, DUMP, XRAND or an old build/ folder); and whether you use the same simulator and version. Rerun in a fresh checkout and a fresh shell. If it still differs, that difference is a finding and belongs in the report.
Two, linked to each other. Different signatures may be different bugs, and one report invites someone to close both when only one is fixed. If debugging later shows a shared cause, closing one as a duplicate of the other takes a minute.
dbglab at HEAD 49c5f04, git 2.54.0, Icarus Verilog 13.0, 2026-10-17. The good report's repro, run as shown in a fresh folder: build-history.sh <dir> && cd <dir> && git checkout -q 49c5f04 && tb/run.sh irq_w1c 1 failed at cycle 84 with exit status 1.
Ruled-out checks, same repository: tb/run.sh irq_w1c 2 and 3 fail at cycle 84; tb/run.sh irq_w1c 1 verilator (Verilator 5.048) fails identically; at v0.1 (fa71142) and at 2e63466 it passes; at 026f8bf it fails at cycle 116. git show --stat 026f8bf lists only rtl/csr_block.sv and rtl/dbglab_pkg.sv. DUMP=1 tb/run.sh irq_w1c 1 wrote a 49,131-byte VCD.
Limits: the bad report and the nightly-regression framing are illustrative, written for this page (dbglab has one commit per day, so a commit stands for a nightly). Field names vary between bug trackers; the content is what matters.