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. Hang, Livelock or Starvation

Part 7 · Hangs and Deadlocks · Working (W)

Hang, Livelock or Starvation

Telling a deadlock, a livelock and starvation apart from evidence: attempts and progress counted per agent and per window, on one small system that can fail all three ways, and a liveness monitor that names the class instead of timing out.

On this page

    Why this matters

    A test that stops finishing can be a deadlock, a livelock or starvation, and each has a different cause and a different owner. Two counters per agent, attempts and progress, name the class in one window, before anyone opens a waveform.

    After this lesson you can: classify a stalled test as deadlock, livelock or starvation from per-agent attempt and progress counts, and build a liveness monitor that names the class instead of timing out.

    Note

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

    Before this: Starvation and Livelock, Timeouts and Watchdogs in Benches

    Three tickets, one word

    The overnight regression has three new failures in three different blocks, and all three tickets say the same thing: hang. One test ended with a watchdog line. The other two ran until the global timeout, 20,000 cycles of a test that should take 500. Three engineers open three waveforms at the last cycle.

    Are these the same kind of failure? What would you measure before anyone scrolls a waveform?

    What people usually do, and why it fails

    The word hang gets used for anything that stops finishing, so all three get the hang treatment: look at the last cycles for a stuck signal. That works for exactly one of them. In the other two the signals are not stuck at all: requests rise and fall, grants come and go, and the waveform looks busy. One engineer decides the design is "just slow" and raises the timeout; another adds a bigger buffer.

    The three classes have different causes. A deadlock is a cycle of waits: fix the arrow that should not exist (who is waiting for whom is the next lesson). A livelock is activity that never completes: look for symmetry in retries. Starvation is one agent never served while others are: look at the arbiter's policy. Starting with the wrong one costs a day. Part 5 showed each class on its own; here the job is to tell them apart on one system, from numbers.

    The method: two counters per agent

    1. Name the agents that share something: masters, engines, ports, threads of a bench.

    2. Define an attempt for each: a step towards a goal that it can take on its own. Here: winning its first resource. On a bus: a request issued, a retry, a NACK received.

    3. Define progress for each: a goal reached. Here: a job finished. On a bus: a response received, a word delivered. Never a toggle, a grant on its own or a poll.

    4. Count both per agent in windows a few times longer than the longest legal job (64 cycles here, with jobs of about 6).

    5. Classify each agent whose progress was zero for two windows in a row: attempts above zero means it is livelocked; zero attempts means it is blocked.

    6. Then classify the system: nobody attempting and nobody progressing is a deadlock; a stuck agent that keeps trying is a livelock; blocked agents while others progress is starvation, or a partial deadlock.

    Class

    Attempts

    Progress

    Activity watchdog

    Next step

    Deadlock

    zero everywhere

    zero everywhere

    fires

    the wait-for table

    Livelock

    keep coming for the stuck agents

    zero for them

    silent

    look for symmetry in retries and back-off

    Starvation

    zero for the starved agent

    normal for the others

    silent

    the wait-for table, then the arbiter's policy

    Slow but healthy

    normal

    low but not zero

    silent

    a performance question, not a liveness one

    One case shares starvation's numbers: a deadlock of some agents while others, which do not need the same resources, keep working. Zero attempts with others progressing means blocked; the wait-for table says whether it is behind an arbiter (starvation) or behind a cycle (a partial deadlock).

    On the debug loop this is Observe: it decides which kind of observation comes next.

    The picture: three fingerprints

    The same counter pair in the same window (cycles 65 to 128) for the three broken modes of the design below. Outline: attempts; filled: jobs done.

    Read across a panel. Empty everywhere is a deadlock. Outline without fill is an agent that tries and fails. Nothing at all for one agent while the others are full is starvation.

    Do it: one system, four modes

    Three agents share two resources (think of two buffers, two locks, or a bus and a FIFO). A job needs both for three cycles. Each resource has an arbiter that keeps its owner until the owner lets go. The modes are four ways the same design gets built:

    • MODE 0, as intended: everyone takes R0 then R1; round-robin arbiters.

    • MODE 1: agent 1 takes R1 first and holds it while it waits for R0.

    • MODE 2: agent 1 still takes R1 first, but agents 0 and 1 are polite: if the second resource does not come within two cycles, they let go and retry after the same back-off.

    • MODE 3: both orders are right, but the arbiters are fixed priority and agents 0 and 1 come back after a one-cycle pause.

    The bench carries the liveness monitor from the method, an activity watchdog (no request or grant changed for 200 cycles) and a global timeout.

    res_arb.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
    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
    // res_arb: one shared resource (a buffer, a lock, a bus). It has at most one owner; the owner
    // keeps it while its request stays high. RR=1 picks round-robin from the last owner, RR=0 is
    // fixed priority (requester 0 first).
    module res_arb #(parameter bit RR = 1) (
      input  logic       clk, rst_n,
      input  logic [2:0] req,
      output logic [2:0] gnt
    );
      logic [1:0] owner, last;                         // owner 3 = free
      logic [1:0] pick;
      always_comb begin
        pick = 2'd3;
        for (int k = 1; k <= 3; k++) begin
          int i;
          i = RR ? (int'(last) + k) % 3 : k - 1;
          if (req[i] && pick == 2'd3) pick = 2'(i);
        end
      end
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin owner <= 2'd3; last <= 2'd2; end
        else if (owner == 2'd3) begin if (pick != 2'd3) begin owner <= pick; last <= pick; end end
        else if (!req[owner]) owner <= 2'd3;             // released: free for one cycle
      assign gnt = (owner == 2'd3) ? 3'b000 : 3'(1) << owner;
    endmodule
    
    // agent: does jobs that need both resources for HOLD cycles. It takes FIRST, then the other.
    // POLITE=1: if the second is not granted within 2 cycles it lets go of the first and retries
    // after BACKOFF cycles. PAUSE is the gap between jobs.
    module agent #(parameter bit FIRST = 0, parameter bit POLITE = 0, parameter int BACKOFF = 4,
                   parameter int PAUSE = 2, parameter int HOLD = 3) (
      input  logic       clk, rst_n,
      input  logic [1:0] gnt,                          // gnt[r]: this agent owns resource r
      output logic [1:0] req,
      output logic       got_first,                    // pulse: obtained its first resource (an attempt)
      output logic       done                          // pulse: a job finished (progress)
    );
      typedef enum logic [2:0] {IDLE, ASK1, ASK2, WORK, BACK} st_t;
      st_t st;
      logic [7:0] n;
      localparam bit SECOND = !FIRST;
      assign req[FIRST]  = (st == ASK1) || (st == ASK2) || (st == WORK);
      assign req[SECOND] = (st == ASK2) || (st == WORK);
      assign got_first   = (st == ASK1) && gnt[FIRST];
      assign done        = (st == WORK) && (n == 8'(HOLD - 1));
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin st <= IDLE; n <= '0; end
        else case (st)
          IDLE: if (n >= 8'(PAUSE)) begin st <= ASK1; n <= '0; end else n <= n + 8'd1;
          ASK1: if (gnt[FIRST]) begin st <= ASK2; n <= '0; end
          ASK2: if (gnt[SECOND]) begin st <= WORK; n <= '0; end
                else if (POLITE && n == 8'd1) begin st <= BACK; n <= '0; end
                else n <= n + 8'd1;
          WORK: if (done) begin st <= IDLE; n <= '0; end else n <= n + 8'd1;
          BACK: if (n >= 8'(BACKOFF)) begin st <= ASK1; n <= '0; end else n <= n + 8'd1;
          default: st <= IDLE;
        endcase
    endmodule
    

    The design as intended

    Terminal
    $ iverilog -g2012 -Plls_tb.MODE=0 -o m0.vvp lls_tb.sv && vvp -n m0.vvp
    window 1-64: attempts 3/3/2  jobs done 3/2/2
    window 65-128: attempts 3/2/3  jobs done 2/3/3
    ...
    window 385-448: attempts 3/3/2  jobs done 3/2/3
    PASS: MODE=0, 20 jobs each in 482 cycles

    This is the healthy fingerprint: in every window each agent attempts about as often as it finishes, and every agent finishes something. Keep this line; it is what the broken modes are compared with.

    Three broken modes

    Terminal
    $ vvp -n m1.vvp
    window 1-64: attempts 1/1/0  jobs done 0/0/0
    window 65-128: attempts 0/0/0  jobs done 0/0/0
      agent 0: BLOCKED (waits, never served)
      agent 1: BLOCKED (waits, never served)
      agent 2: BLOCKED (waits, never served)
    FAIL cycle=128: LIVENESS: DEADLOCK: no attempts and no progress anywhere
    ...
    $ vvp -n m2.vvp
    window 1-64: attempts 4/6/3  jobs done 0/0/3
    window 65-128: attempts 3/6/3  jobs done 0/0/3
      agent 0: LIVELOCK (tries, never finishes)
      agent 1: LIVELOCK (tries, never finishes)
      agent 2: progress
    FAIL cycle=128: LIVENESS: LIVELOCK: a stuck agent keeps trying
    ...
    $ vvp -n m3.vvp
    window 1-64: attempts 4/4/0  jobs done 4/3/0
    window 65-128: attempts 4/4/0  jobs done 4/4/0
      agent 0: progress
      agent 1: progress
      agent 2: BLOCKED (waits, never served)
    FAIL cycle=128: LIVENESS: STARVATION: others progress, a waiting agent is never served
    ...
    • MODE 1: one attempt each for agents 0 and 1 in the first window, then nothing at all. Each won its first resource and is holding it while it waits for the other's. Agent 2 waits behind them for R0. That is a deadlock, and the next step is the wait-for table.

    • MODE 2: agents 0 and 1 win their first resource again and again (agent 1 six times a window) and never finish. That is a livelock: two agents giving way to each other in step. Agent 2 slips through the gaps and finishes three jobs a window. The system is not stopped, which is why the class must be decided per agent.

    • MODE 3: the two busy agents finish four jobs a window each; agent 2 has not won R0 once. Its request is up the whole time, so it is waiting, not idle. That is starvation, and its owner is whoever chose fixed priority.

    All three verdicts arrive at cycle 128, two windows after the trouble starts. The two windows are the rule that keeps a single slow window from being called a hang.

    What the usual guards say about the same three runs

    Terminal
    $ vvp -n m1.vvp +NOLIVE
    FAIL cycle=212: ACTIVITY WATCHDOG: no request or grant changed for 200 cycles
    ...
    $ vvp -n m2.vvp +NOLIVE
    FAIL cycle=20000: TIMEOUT: jobs done 0/0/1000
    ...
    $ vvp -n m3.vvp +NOLIVE
    FAIL cycle=20000: TIMEOUT: jobs done 1250/1249/0
    ...

    The activity watchdog sees the deadlock, because there the wires really stop. It is blind to the other two: in MODE 2 the requests toggle every few cycles, in MODE 3 the bus is busier than ever. The timeout catches both at cycle 20,000, about 156 times later than the liveness monitor, and its message only hints at the class through the job counts. These are the scene's three tickets: one watchdog line and two timeouts. Verilator 5.048 prints the same lines for all of these runs.

    What the evidence says

    The class came from two numbers per agent, not from a waveform, and the same pair of counters split all three. The deadlock is the only class with no attempts anywhere; the livelock is the only one where attempts and progress come apart for the same agent; starvation is the only one where one agent's numbers differ from the others'. The monitor's message names the class, the agents and the next step, which is what Observe should hand to Localise.

    Traps

    Common pitfall

    Counting a grant as progress. If MODE 2's monitor had counted winning a resource as progress, the livelocked agents would look healthy: they win one every few cycles. Progress must be the end of the job, the thing the agent exists to do.

    Common pitfall

    A window shorter than a legal wait. With a window of 8 cycles a healthy agent that waits behind two jobs would be called blocked. Size the window from the longest legal job and wait, measured, and require two windows in a row.

    Common pitfall

    Calling it for the whole system. In MODE 2 the system made progress all along; only two agents were stuck. A system-level throughput number would have said "slow". Keep per-agent counts, and expect mixed verdicts.

    Why this matters

    Senior lens. Put the two counters into the design's own debug registers, per master or per queue, so that the same classification works in emulation and on silicon where there is no bench. Two small counters per agent and a window timer are cheap; watchdogs and hang detectors builds the in-design version.

    When a triage meeting says "hang", ask for the class before the owner. A deadlock goes to whoever owns the cycle of waits, a livelock to whoever owns the retry policy, starvation to whoever owns the arbiter. They are often three different people.

    Interview

    Asked: "Your test times out. How do you tell whether it is a deadlock, a livelock or starvation?"

    Weak answer: I look at the waveform: if nothing moves it is a deadlock, if things move it is a livelock. Starvation is when the test is slow.

    Strong answer: With two counters per agent over windows: attempts, meaning a step it took towards its goal, like a request or a retry, and progress, meaning a goal reached, like a response. No attempts and no progress anywhere is a deadlock, and I build the wait-for table. An agent with attempts but no progress is livelocked, so I look for symmetric retries. An agent that waits with no attempts while others progress is starved, so I look at the arbiter. I classify per agent, because a livelock of two agents can coexist with a third that runs, and I never count a toggle or a poll as progress.

    Follow-ups: (1) How do you choose the window length? (2) What counts as progress for a CPU that polls a status register?

    Check yourself A window shows attempts 0/9/0 and jobs done 0/0/5 for three agents, two windows in a row. Name each agent's state.

    Agent 0 is blocked (no attempts, no progress): if it is waiting, it is starved or behind a deadlock. Agent 1 is livelocked: nine attempts and nothing finished. Agent 2 is healthy. The system is neither stopped nor fine. Look first at what agent 1 retries against, and at whether agent 0 is waiting for the same resource.

    Check yourself In MODE 3, someone proposes raising agent 2's PAUSE so it asks less often. Does that help?

    No. Agent 2 is not losing because it asks too often; it loses every arbitration because fixed priority always prefers agents 0 and 1, which ask again one cycle after each job. Asking less often only makes its waits longer. The fix is in the policy: round-robin (MODE 0) or an age limit that promotes a request that has waited too long.

    Remember

    • Count two things per agent: attempts (a step towards a goal) and progress (a goal reached). Activity on the wires is neither.

    • No attempts and no progress anywhere: deadlock. Attempts but no progress: livelock. Blocked while others progress: starvation, or a partial deadlock.

    • Classify per agent, not per system. A livelock of two agents can sit next to a third that runs fine.

    Rule of thumb: count attempts and progress per agent before you open a waveform.

    How this was checked

    lls_tb.sv as shown, 2026-10-18, Icarus Verilog 13.0: iverilog -g2012 -Plls_tb.MODE=<m> -o m<m>.vvp lls_tb.sv && vvp -n m<m>.vvp for MODE 0 to 3: PASS in 482 cycles (MODE 0), DEADLOCK, LIVELOCK and STARVATION at cycle 128 (MODE 1 to 3). With +NOLIVE: MODE 1 ACTIVITY WATCHDOG at cycle 212; MODE 2 and 3 TIMEOUT at cycle 20000 (jobs 0/0/1000 and 1250/1249/0).

    Verilator 5.048 (verilator --binary --timing --assert -Wno-fatal -GMODE=<m> --top-module lls_tb) prints the same lines for all four modes, with and without +NOLIVE.

    Limits: one small system with fixed timing and no seed; three agents and two resources. Real systems mix the classes and need the window sized from measured job lengths.

    Keep going

    • Practice: FSM deadlock and livelock debugging

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

    • Practice: Round-robin arbiter: starvation, fairness and reset flush

    • Practice: Livelock-free arbiter: formal liveness

    • Code lab: Round-robin arbiter

    • Code lab: Testbench timeout

    • Interview: run a mock interview on this topic

    Related topics

    • Part 7 · Hangs and Deadlocks

    • Part 5: starvation and livelock

    • Part 5: hangs and deadlocks

    • Protocol First Principles: deadlock, livelock and starvation

    • Counters and performance probes

    Previous

    65 / 107

    Next: Who Is Waiting for Whom

    Up next

    Up nextWho Is Waiting for Whom

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Finding the Cycle
    • Credit and Token Loss
    • Valid/Ready Deadlock Patterns