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. Valid/Ready Deadlock Patterns

Part 7 · Hangs and Deadlocks · Working (W)

Valid/Ready Deadlock Patterns

The three classic valid/ready deadlocks: valid waiting for ready, a combinational loop between them, and two channels each waiting for the other. Real reproductions of all three in one bench, the signal levels each leaves at the hang, and the lint pass, structural check or rule that catches each before simulation.

On this page

    Why this matters

    Most handshake hangs are one of three patterns, and each leaves a different picture at the hang: both signals low, both low with a loop behind them, or two channels both valid and both not ready. Knowing the three turns a flat waveform into a named rule, often one lint would have caught.

    After this lesson you can: recognise the three valid/ready deadlock patterns from the levels at a hang, reproduce each, and catch them earlier with the dependency rule, a combinational-loop check or the response rule.

    Note

    Level: Working (W) · Debug-loop step: Localise, Prevent

    Before this: RTL: the valid/ready handshake, Who Is Waiting for Whom

    Three interfaces, three flat lines

    A new accelerator is wired into a subsystem through three valid/ready interfaces: a data stream in, a second stream from a converter, and a request/response port to a lookup table. The first simulation after integration hangs at once. Each interface was unit-tested on its own against a well-behaved model, and every unit test passed.

    All three waveforms end in flat lines. Are these three bugs, or one? And which side of each interface is wrong?

    What people usually do, and why it fails

    A hung handshake usually gets a skid buffer, a register slice, or a sink tied to ready = 1 "to see if data comes out". Each changes the timing, and sometimes the hang moves or disappears. None of them names the broken rule, and tying ready high removes back-pressure, which the design needs. The unit tests passed because each block was tested against a model that never waits: a model sink that raises ready before valid hides pattern 1, and a model that always takes responses hides pattern 3.

    The rules are in the RTL course (rules that keep flow control safe) and in Protocol First Principles (the valid/ready rules, channel dependencies). This lesson is about recognising which one was broken, from a hang.

    The method: read the levels, then the dependency

    1. At the hang, print valid and ready of every channel, with how many transfers each has done. A channel that finished is not part of the hang.

    2. Both low: each side waits for the other to start. Find the side whose output depends on the other's: a source that waits for ready before raising valid is wrong (pattern 1). If the dependency is combinational in both directions, it is a loop (pattern 2): run lint.

    3. Valid high, ready low on one channel: ordinary back-pressure. Follow the sink's wait, as in who is waiting for whom.

    4. Valid high, ready low on two channels between the same agents: each channel's ready waits for the other channel (pattern 3). Find the agent that will not take what it is owed until it can send.

    5. Name the rule broken, and fix the side that broke it. In pattern 1 the sink's "ready after valid" is legal; the source is wrong.

    Pattern

    Levels at the hang

    First found by

    Rule broken

    Fix

    1: valid waits for ready

    valid=0 ready=0

    review, or an assertion that the source offers data it has

    valid must not depend on ready

    raise valid when data exists

    2: combinational loop

    valid=0 ready=0 (or X, or an oscillation)

    lint (UNOPTFLAT), synthesis loop check

    no combinational path ready to valid

    remove the gate, or register it

    3: two channels

    both channels valid=1 ready=0

    review, or a response-acceptance assertion

    take owed responses unconditionally

    accept responses independently of requests

    On the debug loop this is Localise (which side, which rule) and Prevent (which check catches the next one before simulation).

    The picture: the arrow that should not exist

    The dependency each pattern adds, in the accent. The sink's ready = valid in panels 1 and 2 is legal; the arrow back from it is not.

    Do it: all three in one bench

    Three pairs run side by side. BAD=0 is correct everywhere; BAD=1, 2 or 3 breaks that one pattern. The sink in pairs 1 and 2 uses ready = valid, which the rules allow. The responder takes no new request while its one response waits, a legal one-deep design. When no channel has handshaken for 100 cycles, the bench prints every channel's levels.

    vr_src.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
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    // Three valid/ready deadlock patterns. Each pair is correct unless BAD names it.
    // vr_src: sends N words. WAIT_RDY=1 (pattern 1) offers valid only after it has seen ready.
    // COMB_RDY=1 (pattern 2) gates valid with ready combinationally.
    module vr_src #(parameter bit WAIT_RDY = 0, parameter bit COMB_RDY = 0, parameter int N = 50) (
      input  logic       clk, rst_n,
      output logic       valid,
      input  logic       ready,
      output logic [7:0] data
    );
      logic       v_q;
      logic [7:0] n_q;
      assign valid = COMB_RDY ? (v_q && ready) : v_q;
      assign data  = n_q;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin v_q <= 1'b0; n_q <= '0; end
        else begin
          if (valid && ready) n_q <= n_q + 8'd1;
          if (!v_q || (valid && ready))
            v_q <= (32'(n_q) + 32'(valid && ready) < N) && (WAIT_RDY ? ready : 1'b1);
        end
    endmodule
    
    // vr_sink: takes words; ready follows valid (allowed: ready may wait for valid).
    module vr_sink (input logic valid, output logic ready);
      assign ready = valid;
    endmodule
    
    // Pattern 3: a requester and a responder with one request and one response channel.
    // The responder holds one response and takes no new request until it is taken.
    // HOLD_RSP=1: the requester takes a response only when its own request is not stuck.
    module requester #(parameter bit HOLD_RSP = 0, parameter int N = 50) (
      input  logic       clk, rst_n,
      output logic       req_valid,
      input  logic       req_ready,
      output logic [7:0] req_data,
      input  logic       rsp_valid,
      output logic       rsp_ready,
      input  logic [7:0] rsp_data,                     // checked by the bench
      output int unsigned got
    );
      logic [7:0] n_q;
      assign req_valid = (32'(n_q) < N);
      assign req_data  = n_q;
      assign rsp_ready = HOLD_RSP ? !(req_valid && !req_ready) : 1'b1;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin n_q <= '0; got <= 0; end
        else begin
          if (req_valid && req_ready) n_q <= n_q + 8'd1;
          if (rsp_valid && rsp_ready) got <= got + 1;
        end
    endmodule
    
    module responder (
      input  logic       clk, rst_n,
      input  logic       req_valid,
      output logic       req_ready,
      input  logic [7:0] req_data,
      output logic       rsp_valid,
      input  logic       rsp_ready,
      output logic [7:0] rsp_data
    );
      assign req_ready = !rsp_valid;                    // one response at a time
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin rsp_valid <= 1'b0; rsp_data <= '0; end
        else if (req_valid && req_ready) begin rsp_valid <= 1'b1; rsp_data <= req_data + 8'd100; end
        else if (rsp_ready) rsp_valid <= 1'b0;
    endmodule
    
    Terminal
    $ iverilog -g2012 -Pvr_tb.BAD=0 -o v0.vvp vr_tb.sv && vvp -n v0.vvp
    PASS: 50 words on each stream and 50 responses in 100 cycles (BAD=0)
    $ vvp -n v1.vvp
    HANG at cycle 201
      pattern 1 stream   valid=0 ready=0  0 of 50 words
      pattern 2 stream   valid=0 ready=0  50 of 50 words
      pattern 3 request  valid=0 ready=1  50 of 50 requests
      pattern 3 response valid=0 ready=1  50 of 50 responses
    FAIL cycle=201: no handshake on any channel for 100 cycles
    ...
    $ vvp -n v2.vvp
    HANG at cycle 201
      pattern 1 stream   valid=0 ready=0  50 of 50 words
      pattern 2 stream   valid=0 ready=0  0 of 50 words
    ...
    $ vvp -n v3.vvp
    HANG at cycle 152
      pattern 1 stream   valid=0 ready=0  50 of 50 words
      pattern 2 stream   valid=0 ready=0  50 of 50 words
      pattern 3 request  valid=1 ready=0  1 of 50 requests
      pattern 3 response valid=1 ready=0  0 of 50 responses
    FAIL cycle=152: no handshake on any channel for 100 cycles
    ...
    • Read the counts first. Lines with 50 of 50 are finished channels; their levels are just idle. In each run exactly one channel has stopped short.

    • BAD=1: pattern 1 has moved 0 words, with both signals low. The source holds its data and waits to see ready; the sink waits to see valid. The hang starts at reset: not one word ever moved.

    • BAD=2: pattern 2 looks identical in this simulator: 0 words, both low. The difference is not in the waveform; it is in the structure, below.

    • BAD=3: one request went through, then both channels show valid=1 ready=0. The responder holds the first response and takes no new request; the requester has its second request up and refuses the response until that request is accepted. Each channel waits for the other. Note that the first transfer worked, which is why a single-transfer unit test passes.

    The loop that lint sees

    Pattern 2's valid = v_q && ready and the sink's ready = valid form a combinational loop. Simulation settled it at 0 and called it a hang; the tools that look at structure call it what it is. Verilator reports it while building the BAD=2 bench, and a Yosys structural check reports it on the pair alone:

    Terminal
    $ verilator --binary --timing --assert -Wno-fatal -GBAD=2 --top-module vr_tb -Mdir vl2 vr_tb.sv
    %Warning-UNOPTFLAT: vr_tb.sv:75:17: Signal unoptimizable: Circular combinational logic: 'vr_tb.v2'
    ...
    pair.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // snippet: pair.sv is vr_src and vr_sink from above (vr_src's "assign valid" is line 9), then this wrapper
    // pair: one source and one sink, for a structural check
    module pair #(parameter bit COMB = 1) (input logic clk, rst_n, output logic [7:0] data, output logic fire);
      logic valid, ready;
      vr_src #(.COMB_RDY(COMB)) s (.clk, .rst_n, .valid, .ready, .data);
      vr_sink k (.valid, .ready);
      assign fire = valid && ready;
    endmodule
    Terminal
    $ yosys -q -p "read_verilog -sv pair.sv; hierarchy -top pair; proc; flatten; opt_clean; check -assert"
    Warning: found logic loop in module pair:
        cell $flatten\s.$logic_and$pair.sv:9$13 ($logic_and) source: pair.sv:9.30-9.42
          B[0] --> Y[0]
        wire \ready source: pair.sv:24.16-24.21
    ERROR: Found 1 problems in 'check -assert'.

    Both name the loop through valid and ready. With COMB set to 0 the same check reports 0 problems. Pattern 1 has no loop, because the source's wait goes through a flop, so lint is silent on it: the same mistake, one flop apart, and only one of the two is visible to a tool.

    What the evidence says

    The levels at the hang separated pattern 3 from the other two at a glance: two channels both offering and both refused. Patterns 1 and 2 looked identical in simulation, and only a structural check split them. In all three the fix belongs to one side, and in patterns 1 and 3 it is not the side that looks guilty: the sinks and the responder followed the rules. Each failed on its first or second transfer, so these are not rare bugs; they were hidden by unit-test models that never waited.

    Traps

    Common pitfall

    Blaming the side that waits legally. A sink whose ready waits for valid, or a responder that is one deep, is allowed. Fix the source that waits for ready, or the requester that will not take its response. Changing the legal side hides the bug until the next legal partner.

    Common pitfall

    Trusting the simulator on a loop. A combinational loop settled to 0 here; with an inverter in it, it would oscillate, and a different simulator or a gate netlist may settle it to 1. Treat UNOPTFLAT and a synthesis loop warning on a handshake as a bug, not as noise.

    Common pitfall

    Unit tests with polite models. A model that raises ready early, or always takes responses, cannot find patterns 1 and 3. Every interface test should include a partner that waits for valid before ready and one that is slow to take responses.

    Why this matters

    Senior lens. Write the dependencies into the interface specification: for each output, which inputs it may depend on combinationally, and which waits are allowed between channels. Then patterns 1 and 3 become review items and assertion targets, and pattern 2 becomes a lint rule enforced at every integration.

    For a new interface, a formal liveness check with a fair partner (every request eventually accepted, every response eventually taken) finds all three patterns in minutes, including ones that need a specific ordering. The RTL course's first formal run shows the flow.

    Interview

    Asked: "Two blocks connected by valid/ready hang right after reset, with both valid and ready low. What do you look for?"

    Weak answer: One of them is not driving its signal. I would tie ready high to see if data starts to flow.

    Strong answer: Both low means each side waits for the other to go first, so I look for a source whose valid depends on ready, which the rule forbids; the sink is allowed to wait for valid. If the dependency is combinational in both directions it is a loop, and lint will report it, so I run Verilator lint or a synthesis loop check. If two channels were involved and both showed valid high and ready low, I would look instead for an agent that will not take a response until its next request is accepted. Tying ready high only hides it and breaks back-pressure.

    Follow-ups: (1) Why may ready depend on valid but not the other way round? (2) How would you write an assertion for pattern 1?

    Check yourself In pattern 3, the requester's designer says the fix belongs in the responder: give it a two-deep response buffer. Does that remove the deadlock?

    Here it would pass: with a free slot, the requester's next request is accepted, so it keeps taking responses and the buffer never fills (a two-entry responder in this bench gives PASS: 50 words on each stream and 50 responses in 51 cycles (BAD=3)). That is luck, not a fix. The dependency is still there: if the responder ever stops taking requests for another reason (busy, back-pressure downstream), the requester refuses responses, the buffer fills, and the same circular wait forms at any depth. The rule is on the requester: take every response it is owed without waiting for its own next request.

    Check yourself Pattern 1's source waits for ready through a flop, so lint is silent. What check would catch it before a hang?

    An assertion inside the source, or on a bound checker that can see its data-available signal: if the source has a word, valid must be high within one cycle, whatever ready is. In review, the question is simply "does valid depend on ready anywhere?" Here it does, through v_q <= ... && ready.

    Remember

    • valid=0 ready=0 at a hang: one side waits for the other to go first. valid must never wait for ready; ready may wait for valid.

    • A combinational path from ready to valid and back is a loop. Both simulators here settle it to 0, like pattern 1; others may not. Lint and synthesis report it.

    • valid=1 ready=0 on two channels at once: each waits for the other. An agent must take the responses it is owed without needing its next request accepted.

    Rule of thumb: valid never waits for ready, and nothing waits for a new request to be accepted before it takes a response it is owed.

    How this was checked

    vr_tb.sv as shown, 2026-10-18, Icarus Verilog 13.0: iverilog -g2012 -Pvr_tb.BAD=<b> -o v<b>.vvp vr_tb.sv && vvp -n v<b>.vvp: BAD=0 PASS in 100 cycles; BAD=1 and BAD=2 HANG at cycle 201; BAD=3 HANG at cycle 152, with the levels shown. Verilator 5.048 (-GBAD=<b>, --binary --timing --assert -Wno-fatal) prints the same lines; for BAD=2 its build log has %Warning-UNOPTFLAT ... Circular combinational logic: 'vr_tb.v2'. A scratch copy with a two-entry responder (req_ready = n < 2, rsp_valid = n != 0), BAD=3: PASS in 51 cycles.

    Structural check (Yosys 0.69): pair.sv is vr_src and vr_sink from this page plus the pair wrapper shown. yosys -q -p "read_verilog -sv pair.sv; hierarchy -top pair; proc; flatten; opt_clean; check -assert" reports a logic loop and exits with an error; with chparam -set COMB 0 pair it reports 0 problems.

    Limits: three minimal pairs with fixed traffic. Real interfaces hide the same dependencies behind more logic, so lint finds pattern 2 and review or formal finds patterns 1 and 3.

    Keep going

    • Practice: Ready/valid deadlock: combinational feedback and the registered fix

    • Practice: AXI handshake assertions: valid and ready

    • Practice: AXI channel coupling and deadlock avoidance

    • Practice: Two-stage valid/ready pipeline with skid buffers

    • Code lab: AXI stream handshake

    • Code lab: Skid buffer

    • Interview: run a mock interview on this topic

    Related topics

    • Part 7 · Hangs and Deadlocks

    • RTL: rules that keep flow control safe

    • Protocol First Principles: the valid/ready rules

    • Protocol First Principles: channel dependencies

    • Protocol First Principles: the consumption assumption

    • RTL interview: the valid/ready contract

    Previous

    69 / 107

    Next: Watchdogs and Hang Detectors

    Up next

    Up nextWatchdogs and Hang Detectors

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Hang, Livelock or Starvation
    • Who Is Waiting for Whom
    • Finding the Cycle