Chapter 12 of 15
Where a Failure Lives: Four Places to Check First
The interview question behind 'walk me through how you'd debug a failing test': before touching a waveform, a failure can only structurally live in one of four places -- the DUT, the testbench, the environment, or nowhere at all -- grounded in three real, already-documented incidents on axil_regfile.
"Walk me through how you'd debug a failing test" is a common DV interview question, and the naive answer — "I'd open the waveform" — skips the question that actually narrows anything down. A waveform shows what happened; it doesn't say where the wrongness came from. Before looking at a single signal, a failure can only structurally live in one of four places, and this module — like the two before it — doesn't need to invent an example for any of them. axil_regfile's own history already has one real, documented incident for three of the four, and the fourth is sitting on this project's own to-do list right now.
Four places, not one
- The DUT — the RTL itself does something the spec doesn't allow. The fix belongs in the design.
- The testbench — the DUT is actually behaving correctly, but the code driving or checking it — a sequence, a driver, a monitor, a scoreboard's expected value — has an error: a race, a wrong assumption about timing, a mistaken check. The fix belongs in the test infrastructure, not the design.
- The environment — neither the DUT nor the testbench code is wrong. A tool doesn't support a language feature being used, or behaves differently than assumed. The fix isn't a code change in either place — it's a tool or flow change.
- Nowhere — there's no bug at all. The "failure" is the test correctly observing the DUT doing exactly what the spec says it should, and the surprise lives in whoever read the log, not in any piece of code.
Each one, grounded in a real incident
DUT: the combinational loop. An earlier version of axil_regfile's s_axi_awready/s_axi_wready also gated on !write_fire — reads reasonable, "don't accept a new AW/W the instant the current one fires." But write_fire depends on aw_fire, which depends on s_axi_awready itself: awready → write_fire → awready, a genuine combinational loop with no stable solution (uvm-advanced ch1). This was a real defect in the design, found by simulating the RTL rather than reading it, and the fix — depending only on registered state (aw_have/w_have/s_axi_bvalid) — changed the DUT, not the testbench.
Testbench: the BVALID/irq race. Calling irq_event.wait_trigger() immediately after a threshold-crossing write returns looked safe and wasn't: BVALID and irq change on the identical clock edge, confirmed directly by instrumenting both with $time-printing always blocks and watching them land at the exact same timestamp (uvm-advanced ch4). axil_regfile itself was never wrong here — the driver's unblocking and the monitor's trigger() are independent processes with no guaranteed relative order, a timing assumption baked into the virtual sequence, not the DUT. The fix — forking the wait together with the triggering write — changed the testbench, not the design.
Environment: Icarus and clocking blocks. Icarus Verilog doesn't implement SystemVerilog clocking blocks at all, confirmed by trying the simplest possible isolated case (SV ch13, uvm-advanced ch1). Neither the DUT nor the testbench had anything wrong with it — every clocking-block-based interface in this site's history is correct SystemVerilog that a compliant simulator runs without complaint. The fix was naming the limitation and redirecting to a commercial simulator (Aldec Riviera-PRO on EDA Playground) — no code changed anywhere, because no code was broken.
Nowhere: the tests this project hasn't written yet. Coverage ch6 and Test-Planning ch5 both flagged the same still-open scenario: writing STATUS, writing COUNT, writing an unmapped address. When a test finally exercises these, the correct, spec-mandated result is SLVERR or DECERR — not OKAY. A response code that isn't OKAY showing up in a log is exactly the shape of something that looks like a failure to anyone who hasn't checked the spec first. It isn't one. Treating it as a DUT bug, or worse, "fixing" the DUT to always return OKAY, would take a correct design and break it.
Which to check first
The four categories aren't equally expensive to rule out, and cost is a reason to check some before others. Confirming "nowhere" — is this response actually what the spec requires — is usually the cheapest check available: reread the relevant section, confirm the expected value, done in minutes. Confirming "environment" is nearly as cheap: has this exact construct run cleanly somewhere else, does the tool's documentation actually claim support for it. Both are worth ruling out before spending real diagnostic effort assuming a genuine bug exists at all — the DUT-bug and testbench-bug categories are where the real, involved diagnostic work lives (chapter 3's subject), and it's wasted effort to start there before confirming there's actually a bug, or that it's not sitting in the tool rather than the code.
Summary
- A failure can only structurally live in one of four places: the DUT, the testbench, the environment, or nowhere at all — narrowing which one, before touching a waveform, is what actually accelerates a debug session.
- The combinational loop (
uvm-advancedch1) is a real DUT bug: the RTL itself violated a spec rule, found by simulating it, fixed in the design. - The
BVALID/irqrace (uvm-advancedch4) is a real testbench bug: the DUT was correct throughout, the virtual sequence's timing assumption was wrong, fixed in the testbench. - Icarus not implementing
clockingblocks (SV ch13) is a real environment mismatch: neither the DUT nor the testbench had anything wrong with it, fixed by changing simulators, not code. STATUS/COUNT/unmapped-write tests, once written, will produceSLVERR/DECERR— the correct result, not a failure. Confirming "nowhere" and "environment" are usually the cheapest checks available, worth ruling out before assuming a real bug and diving into the DUT or testbench.
A test fails. Before looking at any waveform, what does this chapter argue is the first useful question to ask?
The combinational loop in an earlier version of s_axi_awready/s_axi_wready (uvm-advanced ch1) is classified as a DUT bug rather than a testbench bug. Why?
A future test writes to STATUS and gets SLVERR back. Why does this chapter argue that confirming 'this might not be a bug at all' is usually worth checking before assuming a real defect exists?