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
    • Hang, Livelock or Starvation
    • Who Is Waiting for Whom
    • Finding the Cycle
    • Credit and Token Loss
    • Valid/Ready Deadlock Patterns
    • Watchdogs and Hang Detectors
    • A Full Hang on dbglab
  • 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 7 · Hangs and Deadlocks
  1. Topics
  2. The Art of Hardware Debug
  3. Part 7 · Hangs and Deadlocks
  4. Who Is Waiting for Whom

Part 7 · Hangs and Deadlocks · Working (W)

Who Is Waiting for Whom

Building the wait-for table at a hang systematically: one rule per agent written from the spec, a small script that applies the rules to a VCD dumped at the hang, and how to read the result as a cycle or a chain that ends in nobody. Run on two real dbglab hangs.

On this page

    Why this matters

    At a hang every agent is waiting, and the one the watchdog names is rarely the cause. A table built by hand late at night misses agents and bends definitions; rules written once from the spec and applied by a script give the same table every time, in seconds.

    After this lesson you can: write one wait-for rule per agent from a design's spec, apply the rules to a dump taken at a hang with a small script, and read the result as a cycle of waits or a chain that ends at an agent owing nothing.

    Note

    Level: Working (W) · Debug-loop step: Observe

    Before this: Hang, Livelock or Starvation, Case study: the deadlock under back-pressure

    The second hang this month

    Last month dbglab hung under back-pressure, and someone built the wait-for table by hand from a probe, late at night. It took an hour, and the first version left out the sink, which turned out to be the arrow that closed the loop. This morning a different test hangs, in Verilator only, with the same kind of line: WATCHDOG: no bus or stream progress for 2000 cycles.

    The method is known: snapshot every agent and ask what each one waits for. Can you make the table the same every time, so it takes seconds and misses nobody?

    What people usually do, and why it fails

    The usual start is the agent the watchdog line names. dbglab's line prints the CPU's valid, ready and address, so people start at the CPU and follow by hand: the CPU waits for the bus, the bus is held by the DMA, and so on. Three things go wrong. Agents that nobody thinks of are left out: the sink in its own clock domain, the bench's own checker. Definitions drift: is a FIFO that is full waiting, or holding? And the walk stops at the first plausible culprit, which in a cycle is any of them.

    The method: rules first, snapshot second

    1. List every agent that can hold something or wait for something: masters, the fabric, slaves, FIFOs (each side), consumers in other clock domains, and software.

    2. Write one rule per agent from the spec: a condition on signals, the thing it waits for, and the agent that can give it. A rule that cannot be written from the spec is a gap in the spec.

    3. Give every agent a row, including the ones that wait for nothing. An idle agent is a fact, not an omission.

    4. Take the snapshot after progress has stopped: the end of the dump that the watchdog closed, or any cycle well after the last progress.

    5. Follow the waits from each waiting agent: back to an agent already on the path is a cycle (a deadlock); reaching agents that wait for nothing is a chain that ends (a lost credit, word or wake-up).

    6. Check each arrow against its payload: the address and data of a waiting request, the destination of a waiting packet.

    For dbglab, the rules are short enough to fit in a table. Each line is one if in the script below.

    Agent

    Waiting when

    Waits for

    Given by

    CPU port

    valid=1 ready=0

    ready (the bus)

    FABRIC

    Fabric

    an owner holds the grant with no request and no transfer

    the owner to use or release the bus

    the owner

    DMA

    WAIT with paced=1 dreq=0; or a request without ready

    a free FIFO entry; or the bus

    FIFO; or FABRIC

    FIFO

    full; or empty

    the sink to take a word; or a word

    SINK; or CPU or DMA

    Sink

    sink_en=0; or enabled with the FIFO empty

    a CTRL write; or a word

    CPU; or FIFO

    This is Observe on the debug loop, done so that it is the same every time.

    The picture: a loop or a chain

    The two shapes the table takes on dbglab's two real hangs. Left: every arrow leads on and the walk comes back to the CPU. Right: the walk stops at two agents that wait for nothing.

    A loop is a deadlock: nothing outside it can break it. A chain that ends is not a deadlock at all; something the waiting agents are owed has gone missing. The fixes are different in kind, which is why the shape comes first.

    Do it: the rules as a script

    dbglab writes a VCD when it runs with DUMP=1. The watchdog ends the run, so the end of the dump is a snapshot of the hang.

    terminal
    1
    2
    3
    4
    5
    6
    7
    8
    $ DUMP=1 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
    $ mv dbglab.vcd b2.vcd
    $ DUMP=1 tb/run.sh periph_power 1 verilator
    dbglab: test=periph_power seed=1
    FAIL test=periph_power seed=1 cycle=2421: WATCHDOG: no bus or stream progress for 2000 cycles; cpu_valid=0 cpu_ready=0 cpu_addr=00003008
    $ mv dbglab.vcd b5v.vcd

    The script reads only the signals the rules need, keeps their last values, and applies one rule per agent. It uses the standard library only and works on both simulators' dumps.

    example.py
    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
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    # not-run: reads a VCD that dbglab writes with DUMP=1 (output below is from a real run)
    """waitfor.py: the wait-for table of dbglab at a hang, read from a VCD.
    usage: python3 waitfor.py <dump.vcd> [time_ns] [--edges]   (default: the last time in the dump)
    Each agent has one rule, written once from the design's spec: given the signal values at the
    hang, what is this agent waiting for, and which agent can give it? The script prints one row per
    agent, then follows the waits from every waiting agent until they loop or end."""
    import sys
    
    WANT = {  # short name -> hierarchical name under dbglab_tb
        "valid": "dut.m_valid", "ready": "dut.m_ready", "write": "dut.m_write", "addr": "dut.m_addr",
        "grant": "dut.u_fab.grant", "busy": "dut.u_fab.busy", "lock": "dut.m_lock",
        "dma": "dut.u_dma.state", "paced": "dut.u_dma.paced_q", "dreq": "dut.dreq",
        "full": "dut.u_per.wfull", "empty": "dut.u_per.rempty", "sink_en": "dut.u_per.sink_en_p",
        "words": "stream_words", "cycle": "cycle",
    }
    DMA_STATES = ["IDLE", "CHUNK", "RD_REQ", "RD_RSP", "POP", "LOAD", "WAIT", "WR_REQ", "WR_RSP", "DRAIN", "FINISH"]
    
    
    def read_vcd(path, at=None):
        """Values (as bit strings) of the WANT signals at time "at" (or at the end of the dump)."""
        ids, scope, vals, t = {}, [], {}, 0
        with open(path) as f:
            for line in f:
                tok = line.split()
                if not tok:
                    continue
                if tok[0] == "$scope":
                    scope.append(tok[2])
                elif tok[0] == "$upscope":
                    scope.pop()
                elif tok[0] == "$var":
                    name = ".".join(scope + [tok[4]])
                    for k, h in WANT.items():
                        if name.endswith("dbglab_tb." + h):
                            ids.setdefault(tok[3], []).append(k)
                elif tok[0][0] == "#":
                    t = int(tok[0][1:])
                    if at is not None and t > at * 1000:     # timescale 1ps
                        break
                elif tok[0][0] in "01xz" and tok[0][1:] in ids:
                    for k in ids[tok[0][1:]]:
                        vals[k] = tok[0][0]
                elif tok[0][0] == "b" and len(tok) == 2 and tok[1] in ids:
                    for k in ids[tok[1]]:
                        vals[k] = tok[0][1:]
        return vals, t // 1000
    
    
    def bit(v, i, width):
        """Bit i of a VCD vector (VCD drops leading zeros)."""
        v = v.rjust(width, "0")
        return v[width - 1 - i]
    
    
    def rules(s):
        """One row per agent: (agent, state, waits_for_agents, what_it_waits_for)."""
        v, r = s["valid"], s["ready"]
        cpu_v, cpu_r, dma_r = bit(v, 0, 2), bit(r, 0, 2), bit(r, 1, 2)
        cpu_addr = int(s["addr"].rjust(64, "0")[32:], 2)
        grant, lock = s["grant"].rjust(2, "0"), s["lock"].rjust(2, "0")
        dma = DMA_STATES[int(s["dma"], 2)]
        rows = []
        # CPU port (master 0): a request held without ready waits for the bus.
        if cpu_v == "1" and cpu_r == "0":
            kind = "write" if bit(s["write"], 0, 2) == "1" else "read"
            rows.append(("CPU", f"valid=1 ready=0 {kind} {cpu_addr:08x}", ["FABRIC"], "ready (the bus)"))
        else:
            rows.append(("CPU", f"valid={cpu_v}", [], "nothing (idle)"))
        # Fabric: an owner that holds the grant without a request keeps everyone else out.
        owner = {"01": "CPU", "10": "DMA"}.get(grant)
        if s["busy"] == "1":
            rows.append(("FABRIC", f"grant={grant} busy=1", ["SLAVE"], "a slave's response"))
        elif owner and bit(v, 0 if owner == "CPU" else 1, 2) == "0":
            held = "lock=" + lock
            rows.append(("FABRIC", f"grant={grant} busy=0 {held}", [owner], f"its owner, the {owner}, to use or release the bus"))
        else:
            rows.append(("FABRIC", f"grant={grant} busy=0", [], "nothing (free or issuing)"))
        # DMA engine (master 1).
        if dma == "WAIT" and s["paced"] == "1" and s["dreq"] == "0":
            rows.append(("DMA", f"state={dma} paced=1 dreq=0", ["FIFO"], "dreq (a free FIFO entry)"))
        elif dma in ("RD_REQ", "WR_REQ") and dma_r == "0":
            rows.append(("DMA", f"state={dma} ready=0", ["FABRIC"], "ready (the bus)"))
        elif dma in ("RD_RSP", "WR_RSP"):
            rows.append(("DMA", f"state={dma}", ["FABRIC"], "a response"))
        else:
            rows.append(("DMA", f"state={dma}", [], "nothing (idle)" if dma == "IDLE" else "nothing (working)"))
        # Peripheral FIFO: full waits for the sink to drain; empty waits for a writer.
        if s["full"] == "1":
            rows.append(("FIFO", "full=1", ["SINK"], "the sink to take a word"))
        elif s["empty"] == "1":
            rows.append(("FIFO", "full=0 empty=1", ["CPU", "DMA"], "a word from the CPU or the DMA"))
        else:
            rows.append(("FIFO", "full=0 empty=0", [], "nothing (has words, has space)"))
        # Sink (pclk domain): disabled waits for the CPU's CTRL write; enabled and empty waits for a word.
        if s["sink_en"] == "0":
            rows.append(("SINK", "sink_en=0", ["CPU"], "the CPU to set sink_en (CTRL write)"))
        elif s["empty"] == "1":
            rows.append(("SINK", "sink_en=1 empty=1", ["FIFO"], "a word in the FIFO"))
        else:
            rows.append(("SINK", "sink_en=1", [], "nothing (draining)"))
        return rows
    
    
    def follow(rows, start):
        """Follow the waits from "start" until they come back to an agent already on the path (a
        cycle) or reach agents that wait for nothing (the chain ends: what is owed was lost)."""
        waits = {a: w for a, _, w, _ in rows}
        path = [start]
        while True:
            nxt = waits.get(path[-1], [])
            back = [n for n in nxt if n in path]
            if back:
                return path + [back[0]], "CYCLE"
            onward = [n for n in nxt if waits.get(n)]
            if not onward:
                return path, "ENDS at " + ", ".join(nxt) + ", which wait for nothing"
            path.append(onward[0])
    
    
    def main():
        args = [a for a in sys.argv[1:] if a != "--edges"]
        vals, t = read_vcd(args[0], int(args[1]) if len(args) > 1 else None)
        missing = [k for k in WANT if k not in vals]
        if missing:
            sys.exit(f"signals not found in the dump: {missing}")
        print(f"at {t} ns (bench cycle {int(vals['cycle'], 2)}, stream words {int(vals['words'], 2)})")
        rows = rules(vals)
        if "--edges" in sys.argv:                     # one line per wait, for cycles.py
            for a, ev, w, what in rows:
                for b in w:
                    print(f"EDGE {a} {b} {what}; {ev}")
            return
        print(f"{'agent':7} {'evidence':34} {'waits for':12} what")
        for a, ev, w, what in rows:
            print(f"{a:7} {ev:34} {','.join(w) or '-':12} {what}")
        for a, _, w, _ in rows:
            if w:
                path, verdict = follow(rows, a)
                print(f"from {a}: {' -> '.join(path)}: {verdict}")
                break
    
    
    if __name__ == "__main__":
        main()

    Hang one: B2, in Icarus

    Terminal
    $ python3 waitfor.py b2.vcd
    at 21655 ns (bench cycle 2166, stream words 0)
    agent   evidence                           waits for    what
    CPU     valid=1 ready=0 write 00003008     FABRIC       ready (the bus)
    FABRIC  grant=10 busy=0 lock=10            DMA          its owner, the DMA, to use or release the bus
    DMA     state=WAIT paced=1 dreq=0          FIFO         dreq (a free FIFO entry)
    FIFO    full=1                             SINK         the sink to take a word
    SINK    sink_en=0                          CPU          the CPU to set sink_en (CTRL write)
    from CPU: CPU -> FABRIC -> DMA -> FIFO -> SINK -> CPU: CYCLE
    • The first line is the snapshot's time: the dump closes one bus cycle after the FAIL line's count, which is why it says 2166.

    • Five rows, five waits: every agent is waiting, including the sink in the other clock domain, the row that was missing from last month's hand-built table.

    • The walk goes CPU, fabric, DMA, FIFO, sink and back to the CPU: a cycle, so a deadlock. It is the same table the case study built by hand, produced in a second from a file.

    • Check the edge back to the CPU: the sink waits for a CTRL write, and the CPU's waiting request is a write to 0x3008, the CTRL register. The case study's probe also showed its data, 0x05, which sets sink_en. So the arrow is real, not just plausible.

    Hang two: B5, in Verilator

    Terminal
    $ python3 waitfor.py b5v.vcd
    at 24210 ns (bench cycle 2421, stream words 4)
    agent   evidence                           waits for    what
    CPU     valid=0                            -            nothing (idle)
    FABRIC  grant=00 busy=0                    -            nothing (free or issuing)
    DMA     state=IDLE                         -            nothing (idle)
    FIFO    full=0 empty=1                     CPU,DMA      a word from the CPU or the DMA
    SINK    sink_en=1 empty=1                  FIFO         a word in the FIFO
    from FIFO: FIFO: ENDS at CPU, DMA, which wait for nothing
    • Three agents wait for nothing. The CPU, the fabric and the DMA are idle: no request, no grant, no run.

    • The FIFO is empty and the sink is enabled, so the sink waits for a word and the FIFO waits for a writer. Both writers are idle. The walk ends.

    • Stream words 4. The test pushed four words before powering the sink down and six while it was down, and the bench was still waiting for those six when the watchdog fired. So six words were written into the FIFO and never came out of the stream, and the FIFO is empty. They were taken out of the FIFO by something that did not deliver them.

    This is a lost-token hang, not a deadlock, and no amount of looking for a held resource would find it. The next question is where did six words go, which is a localisation job: cone-of-influence tracing follows it to the sink's read enable while its domain was off, and power-intent bugs gives the class.

    What the evidence says

    The same five rules gave two different shapes on two real hangs, in two simulators, from dumps alone. On B2 the shape is a loop and every agent is part of it; the job is to find the arrow that is a design choice, which the case study does. On B5 the shape is a chain that ends at idle agents while the checker is still owed words; the job is to find where the tokens went. The table did not find either cause. It decided which kind of search comes next, in seconds, and it did not depend on who was on call.

    Traps

    Common pitfall

    An arrow that is true in general but not here. "The sink waits for the CPU" is only a real arrow if the CPU's next action is the write that enables the sink. If the CPU were waiting to read RAM, the sink's wait would lead nowhere and the loop would be an illusion. Print the payload of every waiting request and check it.

    Common pitfall

    Agents that are not in the dump. The bench's stream checker owed six words in B5, and no signal says so; it is in the test and the log. Software, bench threads and queues in models are agents too. Read them from the log or add a dumped counter for them.

    Common pitfall

    A snapshot taken too early. At a cycle where traffic still moves, many agents wait briefly and chains end at busy agents. The rules only mean something after progress has stopped; finding the cycle shows a snapshot taken mid-traffic.

    Why this matters

    Senior lens. Keep the rules in the repository next to the design and review them with it. They are a small, executable statement of every dependency in the system, and writing them is often where a reviewer first asks "and what does the fabric wait for while it holds the grant?"

    Run the script automatically on every WATCHDOG failure in regression and attach its table to the ticket. Triage then starts from a shape (loop or chain) and a list of agents instead of a waveform nobody has opened yet.

    Interview

    Asked: "You have a dump of a hung system with a dozen blocks. How do you build the wait-for table?"

    Weak answer: I start at the block the watchdog reports and follow its stuck signals to the next block, until I find the one that is broken.

    Strong answer: Before the hang, I write one rule per agent from the spec: under which signal values it waits, for what, and who can give it, including FIFOs, consumers in other domains and software. At the hang I take a snapshot after progress has stopped and apply every rule, so every agent gets a row, idle ones too. Then I follow the waits: back to the start is a deadlock, so I look for the arrow that is a design choice; ending at idle agents means something owed was lost. Finally I check each arrow against the payload of the waiting request, because a generic dependency is not always the real one.

    Follow-ups: (1) How would you handle an agent that waits for any one of several others? (2) What would you add to the design so the same table can be built on silicon?

    Check yourself In the B2 table, suppose the CPU's waiting request had been a read of RAM address 0x1000 instead of the write to 0x3008. What changes?

    The sink's arrow to the CPU no longer closes the loop: the sink waits for a CTRL write, but the CPU is not trying to make one. The walk would go CPU, fabric, DMA, FIFO, sink, and then the sink waits for an action the CPU is not taking, which ends the chain at the CPU's program, not at its port. You would then ask why the software did not turn the sink on, a different bug.

    Check yourself Why does the FIFO row in B5 list two agents, and what would it take for that row to be part of a deadlock?

    An empty FIFO can be given a word by either writer, so it waits for the CPU or the DMA. For it to be stuck, both must be unable to write. Here both are simply idle, so it is a chain that ends. If both writers were themselves waiting, for example for the bus, the FIFO would be stuck only if every one of those waits was stuck too.

    Remember

    • One rule per agent, from the spec: given the signals, what is it waiting for, and which agent can give it? Write the rules before the hang.

    • Every agent gets a row, idle ones too. An idle agent is where a chain ends, and a chain that ends means something owed was lost.

    • Check each edge against the payload: an arrow is only real if the request that is waiting is the one that would unblock the next agent.

    Rule of thumb: write the wait-for rules before the hang, and apply them to every agent, not just the one the watchdog named.

    How this was checked

    dbglab at HEAD 49c5f04 (built with dbglab/build-history.sh), 2026-10-18. DUMP=1 tb/run.sh dma_burst_backpressure 1 (Icarus Verilog 13.0) fails with the WATCHDOG line at cycle 2165 and writes dbglab.vcd (181 kB); DUMP=1 tb/run.sh periph_power 1 verilator (Verilator 5.048) fails with WATCHDOG at cycle 2421 (240 kB).

    python3 waitfor.py <dump> (Python 3.14.3, standard library only), the script shown, prints the two tables shown: the cycle CPU, FABRIC, DMA, FIFO, SINK at 21655 ns, and the chain ending at the idle CPU and DMA at 24210 ns. The B2 table matches the hand-built one in the case study.

    Limits: the rules cover dbglab's five agents and read only dumped signals, so the bench's own queue of expected stream words is read from the log, not the dump. The script reads one time point; it does not check that the state was steady before it.

    Keep going

    • Practice: FSM deadlock and livelock debugging

    • Practice: Interface assertions for deadlock detection

    • Practice: Virtual sequence deadlock: sequences wait on each other

    • Practice: Inline checkers: X detection and handshake liveness

    • Code lab: Bus hang detect

    • Interview: run a mock interview on this topic

    Related topics

    • Part 7 · Hangs and Deadlocks

    • Case study: the deadlock under back-pressure

    • Part 5: hangs and deadlocks

    • Protocol First Principles: hang debug

    • Dump strategy

    Previous

    66 / 107

    Next: Finding the Cycle

    Up next

    Up nextFinding the Cycle

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Credit and Token Loss
    • Valid/Ready Deadlock Patterns
    • Watchdogs and Hang Detectors