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
  • 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
    • The Off-by-One That Fails at Wrap
    • The Reset Value That Changed
    • The X That Hid for Thousands of Cycles
    • The Race Two Simulators Disagree On
    • The Bus Value That Was Never Sent
    • The Response to the Wrong Master
    • The Deadlock Under Back-Pressure
    • The Credit Leak That Hangs After an Hour
    • The 2x Throughput Regression
    • The Missing Isolation
    • The Test-Mode Leak
    • The Testbench That Was Wrong
  • Part 11 · Debug as a Team5
  • Part 12 · Interview Prep5
Loading lesson

‹Part 10 · Case Studies
  1. Topics
  2. The Art of Hardware Debug
  3. Part 10 · Case Studies
  4. The Deadlock Under Back-Pressure

Part 10 · Case Studies · Senior (S)

The Deadlock Under Back-Pressure

A real dbglab hang, told as a mystery: a watchdog with no wrong value, the wait-for table at the hang, the cycle it reveals, the commit that made it, a fix that looks right and is not, and the check that stops it coming back.

On this page

    Why this matters

    A hang has no wrong value to walk back from, so the first-wrong-cycle habit finds nothing and guessed fixes, like a longer timeout, only move the hang. Asking who is waiting for whom finds the cycle in minutes.

    After this lesson you can: turn a watchdog failure into a wait-for table, find the cycle in it, confirm the cause with a prediction a wrong fix would fail, and add a check that fires long before the watchdog.

    Note

    Level: Senior (S) · Debug-loop step: The whole loop (won at Observe: the wait-for table)

    Before this: The Debug Loop, Bisection in History

    The report

    Tuesday's regression of dbglab has three new red lines. Nobody changed the test; nobody changed the peripheral. This is the whole report:

    Terminal
    $ tb/regress.sh icarus 1 2 3
    ...
    dma_burst_backpressure   seed 1      FAIL
    dma_burst_backpressure   seed 2      FAIL
    dma_burst_backpressure   seed 3      FAIL
    ...
    $ grep FAIL build/icarus-dma_burst_backpressure-1.log
    FAIL test=dma_burst_backpressure seed=1 cycle=2165: WATCHDOG: no bus or stream progress for 2000 cycles; cpu_valid=1 cpu_ready=0 cpu_addr=00003008

    What the test does: it stops the peripheral's sink, then starts a DMA copy of 48 words into the peripheral's FIFO with paced writes, so the DMA writes a word only when the FIFO has space (dreq). Each time the CPU finds the FIFO full, it turns the sink on for a while by writing CTRL at 0x3008, then off again. The FIFO fills and drains, again and again. That is back-pressure, and the test passed last week.

    Stop here if you want to try it yourself. You have the design (dbglab, the debug loop lesson shows how to probe it) and the line above. Where would you look first?

    Check yourself Want a hint before you read the hunt?

    The watchdog line already tells you one thing that is waiting: the CPU, on a write to 0x3008 that never gets ready. Ask who it is waiting for, and then who that agent is waiting for. Keep going until you come back to the CPU.

    The hunt

    This hunt is won at Observe (the wait-for table), narrowed at Localise and decided at Test.

    Reproduce: it fails the same way every time

    Terminal
    $ tb/run.sh dma_burst_backpressure 1
    dbglab: test=dma_burst_backpressure seed=1
    FAIL test=dma_burst_backpressure seed=1 cycle=2165: WATCHDOG: no bus or stream progress for 2000 cycles; cpu_valid=1 cpu_ready=0 cpu_addr=00003008
    $ tb/run.sh dma_burst_backpressure 1 verilator
    dbglab: test=dma_burst_backpressure seed=1
    FAIL test=dma_burst_backpressure seed=1 cycle=2165: WATCHDOG: no bus or stream progress for 2000 cycles; cpu_valid=1 cpu_ready=0 cpu_addr=00003008

    Same cycle in both simulators, and every seed from 1 to 50 fails. This is not a race or a rare ordering; something always gets stuck. Good news: a deterministic hang is the easy kind.

    Observe: there is no first wrong value

    Your usual move, walking back from the first wrong value, has nothing to start from. No read returned bad data; the scoreboard never complained. The watchdog fired because nothing happened for 2,000 cycles. The last progress was at cycle 164, and the 2,000 cycles after it are identical. A waveform of the hang shows flat lines.

    So change the question. In a hang, every agent is waiting for something. Take a snapshot of each one at the hang and write down what it is waiting for. A probe compiled next to the design prints that snapshot:

    hang_probe.sv
    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
    // not-run: needs dbglab's sources and bench; compile it next to them (command in the Evidence box)
    // hang_probe: a second top-level module compiled next to dbglab. It reads the design through
    // hierarchical names and changes nothing. +AT=<c> prints a snapshot at bus cycle c;
    // +FROM=<a> +TO=<b> prints one line per cycle from a to b.
    `timescale 1ns/1ps
    module hang_probe;
      int unsigned at_c, from_c, to_c;
      initial begin
        if (!$value$plusargs("AT=%d", at_c))     at_c   = 0;
        if (!$value$plusargs("FROM=%d", from_c)) from_c = 1;
        if (!$value$plusargs("TO=%d", to_c))     to_c   = 0;
        if (to_c >= from_c) $display("cycle  cpu v r addr     grant lock  dma     dreq full sink_en");
      end
      always @(posedge dbglab_tb.clk) begin
        #0.5;
        if (dbglab_tb.cycle >= from_c && dbglab_tb.cycle <= to_c)
          $display("%5d      %b %b %h  %b    %b    %-7s %b    %b    %b",
            dbglab_tb.cycle, dbglab_tb.dut.m_valid[0], dbglab_tb.dut.m_ready[0], dbglab_tb.dut.m_addr[0],
            dbglab_tb.dut.u_fab.grant, dbglab_tb.dut.m_lock, dbglab_tb.dut.u_dma.state.name(),
            dbglab_tb.dut.dreq, dbglab_tb.dut.u_per.wfull, dbglab_tb.dut.u_per.pctrl_q[0]);
        if (dbglab_tb.cycle == at_c) begin
          $display("--- cycle %0d ---", at_c);
          $display("cpu   valid=%b ready=%b write=%b addr=%h wdata=%h", dbglab_tb.dut.m_valid[0], dbglab_tb.dut.m_ready[0],
                   dbglab_tb.dut.m_write[0], dbglab_tb.dut.m_addr[0], dbglab_tb.dut.m_wdata[0]);
          $display("fab   grant=%b busy=%b lock=%b", dbglab_tb.dut.u_fab.grant, dbglab_tb.dut.u_fab.busy, dbglab_tb.dut.m_lock);
          $display("dma   state=%s paced=%b dreq=%b", dbglab_tb.dut.u_dma.state.name(), dbglab_tb.dut.u_dma.paced_q, dbglab_tb.dut.dreq);
          $display("fifo  full=%b empty=%b", dbglab_tb.dut.u_per.wfull, dbglab_tb.dut.u_per.rempty);
          $display("sink  sink_en(reg)=%b sink_en(pclk)=%b", dbglab_tb.dut.u_per.pctrl_q[0], dbglab_tb.dut.u_per.sink_en_p);
        end
      end
    endmodule
    Terminal
    $ ../probes/run-probe.sh ../probes/hang_probe.sv dma_burst_backpressure 1 +AT=2165
    dbglab: test=dma_burst_backpressure seed=1
    --- cycle 2165 ---
    cpu   valid=1 ready=0 write=1 addr=00003008 wdata=00000005
    fab   grant=10 busy=0 lock=10
    dma   state=WAIT paced=1 dreq=0
    fifo  full=1 empty=0
    sink  sink_en(reg)=0 sink_en(pclk)=0
    FAIL test=dma_burst_backpressure seed=1 cycle=2165: WATCHDOG: no bus or stream progress for 2000 cycles; cpu_valid=1 cpu_ready=0 cpu_addr=00003008

    Now turn each line into a row: who is waiting, for what, and the evidence. Bit 1 of grant and lock is the DMA, bit 0 the CPU.

    Agent

    Waiting for

    Evidence at cycle 2165

    CPU

    ready for its write of 0x05 to CTRL (0x3008), which turns the sink on

    valid=1 ready=0 addr=00003008 wdata=00000005

    Bus fabric

    a response from its owner, the DMA, before it re-arbitrates; the DMA holds lock

    grant=10 busy=0 lock=10

    DMA

    dreq: a free FIFO entry for its next paced write

    state=WAIT paced=1 dreq=0

    FIFO

    the sink to drain a word

    full=1

    Sink

    sink_en, which only the CPU's write sets

    sink_en(reg)=0

    Read down the second column and you come back to where you started: the CPU waits for the fabric, the fabric for the DMA, the DMA for the FIFO, the FIFO for the sink, the sink for the CPU. That is a cycle of waits, which is what a deadlock is. Nothing outside the cycle can break it, so the watchdog is the only thing that ever happens again.

    The wait-for cycle at the hang. Each arrow means "waits for". The accent arrow is the rule added last week: the fabric keeps the grant while the DMA holds lock.

    The picture folds the sink into the FIFO box, so the five rows become four arrows. Three of the four are normal, designed behaviour: a paced DMA waits for space, a stopped sink does not drain, a CPU waits for the bus. Only one arrow is a choice: the fabric keeping its grant for an owner that is not using it. That is where to look.

    Localise: when did the cycle close?

    A hang has no first wrong value, but it has a first cycle in which the cycle of waits is complete. Trace the window before the last progress:

    Terminal
    $ ../probes/run-probe.sh ../probes/hang_probe.sv dma_burst_backpressure 1 +FROM=150 +TO=169
    dbglab: test=dma_burst_backpressure seed=1
    cycle  cpu v r addr     grant lock  dma     dreq full sink_en
      150      1 0 00003004  10    10    WAIT    1    0    0
      151      1 0 00003004  10    10    WR_REQ  1    0    0
      152      1 0 00003004  10    00    WR_RSP  0    1    0
      153      1 0 00003004  00    00    CHUNK   0    1    0
      154      1 1 00003004  01    10    RD_REQ  0    1    0
      155      1 0 00003004  01    10    RD_REQ  0    1    0
      156      0 0 00003004  00    10    RD_REQ  0    1    0
      157      1 0 00003008  10    10    RD_REQ  0    1    0
      158      1 0 00003008  10    10    RD_RSP  0    1    0
    ...
      164      1 0 00003008  10    10    RD_RSP  0    1    0
      165      1 0 00003008  10    10    POP     0    1    0
      166      1 0 00003008  10    10    LOAD    0    1    0
      167      1 0 00003008  10    10    WAIT    0    1    0
      168      1 0 00003008  10    10    WAIT    0    1    0
      169      1 0 00003008  10    10    WAIT    0    1    0
    FAIL test=dma_burst_backpressure seed=1 cycle=2165: WATCHDOG: no bus or stream progress for 2000 cycles; cpu_valid=1 cpu_ready=0 cpu_addr=00003008
    • Cycle 152: the DMA's last write of a chunk fills the FIFO (full goes to 1), and the chunk's last response drops lock.

    • Cycles 153 to 155: the grant is released; the CPU wins it and reads the peripheral STATUS (0x3004). It sees the FIFO full, so its next move is to turn the sink on.

    • Cycle 157: the CPU asks to write CTRL (0x3008), one cycle too late: the DMA, already waiting, won the grant at this edge and raised lock for its next chunk.

    • Cycles 158 to 166: the DMA reads four words while the CPU waits. Before last week, the CPU would have won the bus after the first of these responses.

    • Cycle 167, the cycle closes: the DMA enters WAIT with dreq 0, still holding lock and the grant. From here every line is the same.

    Who changed the rules? The test passed last week, so bisect it, exactly as in bisection in history:

    Terminal
    $ git bisect start HEAD v0.1
    ...
    $ git bisect run tb/run.sh dma_burst_backpressure 1
    ...
    PASS test=dma_burst_backpressure seed=1
    ...
    FAIL test=dma_burst_backpressure seed=1 cycle=2165: WATCHDOG: no bus or stream progress for 2000 cycles; cpu_valid=1 cpu_ready=0 cpu_addr=00003008
    ...
    c4a71b585e287d527b9c2a827a3969bca0334445 is the first bad commit
    commit c4a71b585e287d527b9c2a827a3969bca0334445
    Author: dbglab <dbglab@example.com>
    Date:   Thu Mar 12 10:00:00 2026 +0000
    
        dma: keep the bus for a whole chunk
    
        The arbiter re-arbitrates after every response, which costs a cycle
        per beat. The DMA now raises m_lock from the first read of a chunk to
        its last write, and the fabric keeps the grant while the owner holds
        lock. dma_copy runs about 20% faster.
    ...

    Four runs, and the guilty commit is a performance change, not a bug fix, which is exactly why nobody suspected it. Its two key lines:

    design.sv
    1
    2
    3
    4
    5
    // snippet: commit 11, rtl/bus_fabric.sv line 35 and rtl/dma_engine.sv lines 74-75
      assign accept  = rsp_fire && !m_lock[owner];      // a locked owner keeps the bus for its next beat
    
      assign m_lock = ((state == RD_REQ) || (state == RD_RSP) || (state == POP) || (state == LOAD)
                    || (state == WAIT) || (state == WR_REQ) || (state == WR_RSP)) && !chunk_end;

    The waveform below shows the same mechanism in a reduced copy: dbglab's arbiter unchanged, these two rules copied, a two-word chunk and a two-entry FIFO that starts full with the sink stopped. Grant values: 0 nobody, 1 CPU, 2 DMA.

    Read it as two masters and one bus: find the slot where the CPU should have won and did not.

    • Slots 0 to 1: the DMA requests and is granted (grant = 2); dma_lock is high for the whole chunk.

    • Slot 2: the CPU raises cpu_valid for the write that would turn the sink on.

    • Slot 3: the DMA's first read has had its response, so the old fabric would hand the bus to the waiting CPU here. With lock high it keeps it, and the DMA makes its second read.

    • Slots 5 to 11: the DMA has stopped requesting (dma_valid = 0) and waits for dreq; grant stays 2, cpu_ready stays 0. This never ends.

    Hypothesise and test: the fix that looks right

    Hypothesis: the DMA holds the bus through WAIT, and WAIT can only end after a bus write by someone else. The obvious fix follows: stop asserting lock in WAIT. Before you run it, predict. The fabric only looks at lock at a response: accept is rsp_fire && !m_lock[owner]. The last response before WAIT is the fourth read's, at cycle 164, when lock was still high, so the grant was kept then. In WAIT no response ever comes, so accept never fires, whatever lock says. Prediction: this fix still hangs, at the same cycle.

    Terminal
    $ sed -n 74,75p rtl/dma_engine.sv        # a copy of HEAD with WAIT removed from m_lock
      assign m_lock = ((state == RD_REQ) || (state == RD_RSP) || (state == POP) || (state == LOAD)
                    || (state == WR_REQ) || (state == WR_RSP)) && !chunk_end;
    $ tb/run.sh dma_burst_backpressure 1
    dbglab: test=dma_burst_backpressure seed=1
    FAIL test=dma_burst_backpressure seed=1 cycle=2165: WATCHDOG: no bus or stream progress for 2000 cycles; cpu_valid=1 cpu_ready=0 cpu_addr=00003008

    The prediction held: the plausible fix fails. This experiment is worth more than a pass, because it shows exactly which arrow in the cycle matters. It is the fabric's decision to keep the grant at a response, made before the DMA knew it would have to wait. A fix has to change that decision.

    The fix and the lesson

    Fix: never keep a grant for an owner that may wait

    The safe fix puts back the rule from before commit 11: the fabric re-arbitrates after every response.

    design.sv
    // snippet: the fix, rtl/bus_fabric.sv line 35
      assign accept  = rsp_fire;

    Prove: the repro, a sweep, and nothing new

    Terminal
    $ sed -n 35p rtl/bus_fabric.sv
      assign accept  = rsp_fire;
    $ tb/run.sh dma_burst_backpressure 1
    dbglab: test=dma_burst_backpressure seed=1
    PASS test=dma_burst_backpressure seed=1 cycles=772 time=7716 ns
    $ for s in $(seq 1 50); do tb/run.sh dma_burst_backpressure $s > /dev/null || echo "fail $s"; done
    $ tb/regress.sh icarus 1 2 3 | grep -E "FAIL|failing"
    irq_w1c                  seed 1      FAIL
    irq_w1c                  seed 2      FAIL
    irq_w1c                  seed 3      FAIL
    periph_power             seed 1      FAIL
    periph_power             seed 2      FAIL
    periph_power             seed 3      FAIL
    6 failing run(s)

    The repro passes, the 50-seed sweep prints nothing, and the regression's only failures are two other known bugs with their own signatures. The random test, which mixes CPU and DMA traffic for 60 seeds, shows no WATCHDOG either. One cost is real: dma_copy seed 1 goes back from 11816 ns to 15366 ns. The fix gives up last week's speed-up, and the lens below shows how to keep it.

    Prevent: a check that fires 1,984 cycles earlier

    The watchdog caught the hang 2,000 cycles late and could only say nothing moves. A check on the rule itself fires as soon as it is broken: an owner that holds the grant without using it while the other master waits.

    grant_idle_check.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // not-run: needs dbglab's sources and bench; compile it next to them (command in the Evidence box)
    // grant_idle_check: fails when a bus owner holds the grant without using it while another master waits.
    `timescale 1ns/1ps
    module grant_idle_check #(parameter int LIMIT = 16);
      logic [1:0] grant, valid;
      logic       busy;
      int unsigned idle = 0;
      assign grant = dbglab_tb.dut.u_fab.grant;
      assign valid = dbglab_tb.dut.m_valid;
      assign busy  = dbglab_tb.dut.u_fab.busy;
      always @(posedge dbglab_tb.clk) begin
        if (|(grant & ~valid) && !busy && |(valid & ~grant)) idle++;
        else idle = 0;
        if (idle > LIMIT) begin
          $display("FAIL cycle=%0d: GRANT_IDLE: owner grant=%b has not used the bus for %0d cycles while valid=%b waits",
                   dbglab_tb.cycle, grant, idle, valid);
          $fatal(1, "grant held while idle");
        end
      end
    endmodule
    Terminal
    $ ../probes/run-probe.sh ../probes/grant_idle_check.sv dma_burst_backpressure 1      # at HEAD
    dbglab: test=dma_burst_backpressure seed=1
    FAIL cycle=181: GRANT_IDLE: owner grant=10 has not used the bus for 17 cycles while valid=01 waits

    It fires at cycle 181 instead of 2165, and its message names the rule, the owner and the waiter. With the fix it stays silent on every test it was run on. The limit of 16 cycles is a design choice: long enough for a chunk's internal steps, short enough to catch an owner that is waiting on the outside world. In a real bench this belongs in the bus checker as an assertion, next to the valid/ready rules.

    Which step won this hunt? Observe. The wait-for table turned "nothing happens" into a cycle with one suspicious arrow. Test saved it from a wrong fix that looked like the obvious one.

    Common pitfall

    Raising the watchdog limit, or adding a timeout to WAIT. Both make the test end differently, not correctly. A DMA that gives up in WAIT drops a word of the stream; a longer watchdog just waits longer for an event that cannot happen. If a fix does not remove an arrow from the wait-for cycle, it is not a fix.

    Why this matters

    Senior lens. The fix above is safe but slow. A fix that keeps the speed-up asks which chunks can wait on the outside world? Only paced ones: an unpaced chunk's reads and writes need nothing but the bus it already holds. Locking only unpaced chunks (&& !paced_q on m_lock) passed the same seeds, the regression and the idle-grant check, and kept dma_copy at 11816 ns. Choose it only with the check in place, because the next mode that waits on something will reopen the hole.

    In review, ask of every lock, credit or grant: what does the holder wait for while holding it, and can that event need the thing it holds? Commit 11 would have failed that question in one minute. Its message even said what it did: "the fabric keeps the grant while the owner holds lock".

    Interview

    Asked: "A regression test hangs and the watchdog fires. There is no error before it. How do you debug it?"

    Weak answer: I would open the waveform where the watchdog fired and look for the signal that is stuck, then add a timeout so the design recovers.

    Strong answer: A hang has no first wrong value, so I snapshot every agent at the hang and build a wait-for table: who is waiting, for what, with the signal values as evidence. If following the waits comes back to the start, it is a deadlock, and I look for the arrow that is a choice rather than a protocol rule, usually something holding a shared resource while waiting. Then I trace back to the cycle where the loop closed, bisect if it used to pass, and predict what a candidate fix does before running it. I prove it with a seed sweep and add an assertion on the broken rule, so the next one fires in tens of cycles, not thousands.

    Follow-ups: (1) How would you tell a deadlock from a livelock in the same waveform? (2) The hang happens once a week on an FPGA and never in simulation. What changes?

    Check yourself A colleague proposes a different fix: make the CPU's write to 0x3008 high priority in the arbiter. Does it break the cycle?

    No. Priority only matters when the arbiter is choosing, and here it never chooses: with lock held, accept never fires, so the grant is never released. A higher-priority CPU waits just as long. The fix must change when the fabric gives the grant up, not who wins once it does.

    Check yourself With the fix in, someone adds a second paced peripheral whose space signal depends on an interrupt the CPU must service. Which rule from this case study protects you, and which check would catch a violation?

    The rule: never hold a grant while waiting for an event that may need the bus, here the CPU's interrupt service. With accept = rsp_fire the fabric releases the grant after every response, so the DMA cannot hold the bus through a wait. If someone reintroduces a lock, grant_idle_check (or its assertion form) fires after 17 idle cycles, long before a watchdog would.

    Remember

    • A hang has no first wrong value. Snapshot every agent at the hang and write down what each one is waiting for.

    • A deadlock is a cycle of waits. Break it where an agent holds a shared resource while waiting for something that needs it.

    • A fix must break the cycle in every case. Predict what a candidate fix changes, then run it; this one bug has a fix that looks right and still hangs.

    Rule of thumb: never hold a grant while waiting for an event that may need the bus.

    How this was checked

    dbglab at HEAD 49c5f04 (rebuilt with dbglab/build-history.sh), 2026-10-17. tb/regress.sh icarus 1 2 3 fails dma_burst_backpressure seeds 1 to 3. tb/run.sh dma_burst_backpressure 1 and the same with verilator (Verilator 5.048) both print the WATCHDOG line at cycle 2165; seeds 1 to 50 all fail.

    Observe: hang_probe.sv compiled next to dbglab with iverilog -g2012 -Wno-timescale -I tb -o build/probe.vvp <rtl/files.f> tb/bus_rules.sv tb/dbglab_tb.sv hang_probe.sv (Icarus Verilog 13.0) and run with vvp -n build/probe.vvp +TEST=dma_burst_backpressure +SEED=1 +AT=2165, then +FROM=150 +TO=169. Bisect: git bisect start HEAD v0.1, git bisect run tb/run.sh dma_burst_backpressure 1 (git 2.54.0) found c4a71b5 (commit 11) in 4 runs.

    Fixes, on copies of HEAD: dropping WAIT from m_lock still fails at cycle 2165. assign accept = rsp_fire; passes seeds 1 to 50 of dma_burst_backpressure, leaves only irq_w1c and periph_power failing in tb/regress.sh icarus 1 2 3, and gives no WATCHDOG in random seeds 1 to 60 (its 16 failures are the other bugs' signatures); dma_copy seed 1 takes 15366 ns instead of 11816 ns. Locking only unpaced chunks passes the same checks at 11816 ns. grant_idle_check.sv fires at cycle 181 at HEAD and stays silent with either fix on smoke, dma_copy, periph_stream, dma_burst_backpressure and random, seeds 1 to 3.

    Waveform: COURSE=dbg node scripts/tools/gen-waves.mjs case-studies:deadlock on wave-src/deadlock/hang-reduced.sv, a reduced copy (dbglab's arbiter unchanged, the lock and accept lines copied, a two-word chunk and a two-entry FIFO). It matched its expectations and a second run changed nothing. Limits: one design, simulated only; the reduced copy shows the mechanism, not dbglab's cycle numbers.

    Keep going

    • Practice: FSM deadlock and livelock debugging

    • Practice: Deadlock and livelock: detection, debug and SVA

    • Practice: A memory arbiter for two masters

    • Practice: Inline checkers: X detection and handshake liveness

    • Code lab: Testbench timeout

    • Code lab: AXI stream handshake

    • Interview: run a mock interview on this topic

    Related topics

    • Part 10 · Case Studies

    • Part 7: hangs and deadlocks

    • Part 5: bug classes

    • Bisection in history

    • RTL: the valid/ready handshake

    • RTL: bus arbitration

    • RTL: stalling without deadlock

    Previous

    92 / 107

    Next: The Credit Leak That Hangs After an Hour

    Up next

    Up nextThe Credit Leak That Hangs After an Hour

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Off-by-One That Fails at Wrap
    • The Reset Value That Changed
    • The X That Hid for Thousands of Cycles
    cpu_validcpu_readygrant02dma_validdma_lockdreq
    Reduced copy of the hang: the DMA locks the bus for its chunk, then waits for FIFO space while the CPU's sink-enable write waits for the bus