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

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Protocol Metrics Reference
  • Protocols Glossary
  • Reading Path
  • Protocol Fundamentals25
    • Layering & Transaction Model
    • Handshake & Backpressure
      • Handshake & Backpressure: Concepts
      • Handshake & Backpressure: Integration
      • Handshake & Backpressure: Debug
      • Handshake & Backpressure: Interview
      • Handshake & Backpressure: Deep Dive
      • Handshake & Backpressure: Worked Examples
    • Ordering & Outstanding Rules
    • Bandwidth & Latency Budgeting
    • Protocol Fundamentals: Tricky Q&A
  • AMBA Family (APB / AHB / AXI)17
  • Coherence Fabrics (ACE / CHI)17
  • Memory Interfaces (DDR / LPDDR / HBM)17
  • PCIe & CXL17
  • Embedded Peripherals (I2C / SPI / UART)17
  • High-Speed I/O (USB / Ethernet / MIPI)17
  • Protocol Verification & Compliance21
  • Protocols Interview Prep5
  • Interface Protocols Cheatsheet
Loading lesson

←Handshake & Backpressure
  1. Topics
  2. Bus Protocols
  3. Protocol Fundamentals
  4. Handshake & Backpressure
  5. Handshake & Backpressure: Concepts
On this page

    Interface ProtocolsAll levels

    Handshake & Backpressure: Concepts

    Concepts for Handshake & Backpressure.

    Core concepts

    VALID/READY semantics

    Read first: Layering & Transaction Model: what is crossing this boundary?

    Assume one synchronous source and destination, sampled on rising edges of the same clock, outside reset. The source presents VALID plus a payload; the destination presents READY. Exactly one item is accepted at an edge where VALID && READY is true. A high VALID or READY alone is not a transfer, and signal changes between sampled edges do not create an extra transfer.

    For the AXI-style interface used here, Rule 1: outside reset, an asserted VALID and its payload must remain stable until an accepting rising edge with VALID && READY. Rule 2: a source must not wait for READY before presenting an available item. Rule 3: a destination may wait until it has observed VALID before asserting READY. Waiting permission does not permit an AXI combinational input-to-output path: implement the dependency through suitable state. Arm IHI 0022H A3.1–A3.2 specifies both the timing restriction and handshake rules.

    Retained offer: one clock, outside reset

    Before E1 the source offers item A with payload D0, and the destination cannot accept.

    Edge

    VALID / READY

    Payload

    Accepted

    Source after edge

    E1

    1 / 0

    D0

    None

    Retain A/D0

    E2

    1 / 0

    D0

    None

    Retain A/D0

    E3

    1 / 1

    D0

    A

    A may retire

    E4

    1 / 1

    D1

    B

    B may retire

    What to notice

    • A/D0 is unchanged at E1, E2 and the accepting edge E3.

    • B/D1 appears only after A was accepted; E3 and E4 are two transfers.

    Check yourself May D1 replace D0 before E3 because READY will rise soon?

    No. READY is 0 at E1 and E2, so A was not accepted and must remain offered through E3.

    The waiting deadlock trap

    The classic bug is gating a pending offer with READY: valid = ready & internal_valid. If the destination starts with READY=0 and waits to observe VALID, the producer never presents its item and the destination never sees a reason to become ready. This is a waiting deadlock even when the destination's decision is registered. A literal combinational loop is a separate circuit problem and is not necessary for this deadlock.

    Do not rely on synthesis to repair an invalid dependency. Drive VALID from retained pending-item state, preserve the payload during a stall, and choose buffering that obeys the selected interface timing rules. Registering one signal without preserving the item and controlling acceptance is not a complete repair. A directed test with READY initially low exposes the waiting bug in simulation.

    Registered waiting deadlock

    Before E0, A is pending and both outputs are 0. The buggy source registers VALID_next = pending && READY_seen; the destination registers READY_next = VALID_seen.

    Edge

    Pending

    VALID / READY

    Accepted

    After edge

    E0

    A

    0 / 0

    None

    Both keep waiting

    E1

    A

    0 / 0

    None

    Both keep waiting

    E2

    A

    0 / 0

    None

    No progress

    What to notice: Follow good_valid before good_ready rises, then the good_accepted count. Compare the faulty pair: both outputs remain low despite pending work. These are pre-edge samples; registered counters become visible on a later sample.

    Check yourself Which pair makes progress, and what evidence distinguishes waiting from a completed transfer?

    The repaired pair's VALID and READY overlap and good_accepted reaches one. The faulty pair keeps bad_valid, bad_ready and bad_accepted at zero. A pending item alone is not a transfer.

    What to notice

    • Both decisions are registered; this deadlock needs no combinational feedback path.

    • A remains pending but is never presented, so the destination never responds.

    Check yourself Does adding another register repair the waiting dependency?

    No. The source must present retained pending state independently of READY. Another register leaves the circular waiting policy intact.

    Check yourself How can you compare the waiting source with a repaired source in a bench?

    Both destinations register READY after observing VALID. Both sources start with one pending item carrying A0. The faulty source waits for READY; its pair stays in the zero-output state. The repaired source presents the item independently, retains it until acceptance and completes once. The bench checks six active edges; the closed zero-state dependency explains why the faulty pair cannot escape by waiting longer.

    pending_source.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
    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
    // Same-clock teaching model: registered waiting can deadlock without a combinational loop.
    module pending_source #(parameter WAIT_READY = 0) (
      input wire clk,
      input wire rst,
      input wire ready,
      output reg valid,
      output wire [7:0] data
    );
      reg pending;
      assign data = 8'hA0;
    
      always @(posedge clk) begin
        if (rst) begin
          pending <= 1'b1;
          valid <= 1'b0;
        end else if (valid && ready) begin
          pending <= 1'b0;
          valid <= 1'b0;
        end else if (pending) begin
          // WAIT_READY=1 is the deliberately faulty policy.
          if (WAIT_READY == 0 || ready) valid <= 1'b1;
        end
      end
    endmodule
    
    module waiting_destination (
      input wire clk,
      input wire rst,
      input wire valid,
      output reg ready
    );
      always @(posedge clk) begin
        if (rst) ready <= 1'b0;
        else ready <= valid;
      end
    endmodule
    
    module tb;
      reg clk = 1'b0;
      always #5 clk = ~clk;
      reg rst = 1'b1;
      wire good_valid, good_ready, bad_valid, bad_ready;
      wire [7:0] good_data, bad_data;
      integer good_accepted = 0;
      integer bad_accepted = 0;
    
      pending_source #(.WAIT_READY(0)) good_source (
        .clk(clk), .rst(rst), .ready(good_ready), .valid(good_valid), .data(good_data)
      );
      pending_source #(.WAIT_READY(1)) bad_source (
        .clk(clk), .rst(rst), .ready(bad_ready), .valid(bad_valid), .data(bad_data)
      );
      waiting_destination good_destination (
        .clk(clk), .rst(rst), .valid(good_valid), .ready(good_ready)
      );
      waiting_destination bad_destination (
        .clk(clk), .rst(rst), .valid(bad_valid), .ready(bad_ready)
      );
    
      always @(posedge clk) begin
        if (rst) begin
          good_accepted <= 0;
          bad_accepted <= 0;
        end else begin
          if (good_valid && good_ready) begin
            if (good_data !== 8'hA0) begin
              $display("FAIL: repaired source changed the pending payload");
              $finish;
            end
            good_accepted <= good_accepted + 1;
          end
          if (bad_valid && bad_ready) bad_accepted <= bad_accepted + 1;
          if (bad_valid || bad_ready || !bad_source.pending) begin
            $display("FAIL: faulty pair did not preserve its closed zero-output waiting state");
            $finish;
          end
        end
      end
    
      initial begin
        $dumpfile("wave.vcd");
        $dumpvars(0, tb);
        repeat (2) @(posedge clk);
        @(negedge clk); rst = 0;
        repeat (6) @(posedge clk);
        #1;
        if (good_accepted != 1 || good_source.pending || good_valid) begin
          $display("FAIL: repaired source did not complete its one pending item");
          $finish;
        end
        if (bad_accepted != 0 || !bad_source.pending || bad_valid || bad_ready) begin
          $display("FAIL: waiting source did not remain blocked with work pending");
          $finish;
        end
        $display("PASS: repaired source completed once; registered mutual waiting retained pending work without progress");
        $finish;
      end
    endmodule
    • Never: valid = ready (direct or through combinational logic).

    • Retain pending VALID and payload until an accepting edge.

    • Review both dependency rules and actual buffering capacity.

    Skid buffers and pipelined handshakes

    A skid buffer retains an extra accepted item when registered backpressure takes time to reach the producer. Consider a design with an output register and one extra skid slot: two items total. If item A is stalled at the output while upstream still sees READY, accepted item B must be stored in the skid slot. Output VALID and A remain unchanged; the buffer does not withdraw the stalled offer.

    Once both slots are occupied, upstream READY must stop further acceptance before item C can overflow storage. After A transfers, B can advance without being lost, duplicated or reordered. Exact latency and bypass behaviour depend on the design; buffering cannot make the downstream accept while its READY is low. Size storage for the number of additional items that can actually be accepted before backpressure takes effect.

    Two-slot skid buffer: signals before each edge

    Storage is an output register plus one skid slot. Before E1, output A is occupied, the skid slot is empty, upstream offers B and downstream stalls. Item 'None' means VALID is 0.

    Edge

    IN item

    IN_READY

    OUT item

    OUT_READY

    E1

    B

    1

    A

    0

    E2

    C

    0

    A

    0

    E3

    C

    0

    A

    1

    E4

    C

    1

    B

    1

    E5

    None

    1

    C

    1

    The same edges: accepted items and resulting storage

    Q_after counts both storage slots immediately after the edge.

    Edge

    IN accepted

    OUT accepted

    Output / skid after edge

    Q_after

    E1

    B

    None

    A / B

    2

    E2

    None

    None

    A / B

    2

    E3

    None

    A

    B / empty

    1

    E4

    C

    B

    C / empty

    1

    E5

    None

    C

    Empty / empty

    0

    What to notice: Read input and output acceptance separately. During a stalled output, its offered data must stay unchanged even if another input is accepted into the spare slot. The bad_diverged flag belongs to the faulty comparison, not the correct buffer.

    Check yourself Does out_valid staying high while out_ready is low mean the output item was delivered repeatedly?

    No. Only an edge with both high delivers an item. The accepted and delivered counters track different boundaries; their difference accounts for stored work.

    What to notice

    • C is offered at E2/E3 but not accepted; the source retains it through E4.

    • E4 accepts C and completes B, so occupancy stays one.

    • Registered backpressure costs the E3 input opportunity in this policy.

    Check yourself Can the source replace C after E3?

    No. IN_READY is 0 at E3; C is first accepted at E4. Keep C and its payload stable through E4.

    Check yourself How can a bench distinguish a retained item from an accepted item that was lost?

    The good buffer must preserve output order, accepted-minus-delivered occupancy and a stalled output offer. Each instance receives A/B/C once through its own handshake. The faulty copy drops the extra accepted item; the bench must detect its conservation failure. Payloads A0, B1 and C2 identify items A, B and C. The bench first loads A, then exercises the five edges in the tables above.

    skid2.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
    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
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    // Two-slot valid/ready evidence for the Bus Protocols teaching pilot.
    // The correct instance retains one extra accepted beat; BUG=1 accepts that beat but drops it.
    module skid2 #(parameter BUG = 0) (
      input  wire       clk,
      input  wire       rst,
      input  wire       in_valid,
      output wire       in_ready,
      input  wire [7:0] in_data,
      output wire       out_valid,
      input  wire       out_ready,
      output wire [7:0] out_data
    );
      reg       out_full, skid_full;
      reg [7:0] out_word, skid_word;
    
      assign in_ready  = !skid_full;
      assign out_valid = out_full;
      assign out_data  = out_word;
    
      wire in_accept  = in_valid && in_ready;
      wire out_accept = out_valid && out_ready;
      wire stalled    = out_valid && !out_ready;
    
      always @(posedge clk) begin
        if (rst) begin
          out_full  <= 1'b0;
          skid_full <= 1'b0;
        end else if (stalled) begin
          if (in_accept && BUG == 0) begin
            skid_full <= 1'b1;
            skid_word <= in_data;
          end
        end else if (skid_full) begin
          out_full  <= 1'b1;
          out_word  <= skid_word;
          skid_full <= 1'b0;
        end else begin
          out_full <= in_valid;
          out_word <= in_data;
        end
      end
    endmodule
    
    module tb;
      reg clk = 1'b0;
      always #5 clk = ~clk;
    
      reg       rst = 1'b1;
      reg       in_valid = 1'b0;
      reg [7:0] in_data = 8'h00;
      reg       out_ready = 1'b0;
    
      wire       in_ready, out_valid;
      wire [7:0] out_data;
      wire       bug_in_ready, bug_out_valid;
      wire [7:0] bug_out_data;
      reg traffic_started = 1'b0;
      reg [1:0] bad_source_index = 0;
      wire bug_in_valid = traffic_started && bad_source_index < 3;
      wire [7:0] bug_in_data = bad_source_index == 0 ? 8'hA0 :
                             bad_source_index == 1 ? 8'hB1 : 8'hC2;
    
      skid2 #(.BUG(0)) good (
        .clk(clk), .rst(rst), .in_valid(in_valid), .in_ready(in_ready),
        .in_data(in_data), .out_valid(out_valid), .out_ready(out_ready),
        .out_data(out_data)
      );
      skid2 #(.BUG(1)) bad (
        .clk(clk), .rst(rst), .in_valid(bug_in_valid), .in_ready(bug_in_ready),
        .in_data(bug_in_data), .out_valid(bug_out_valid), .out_ready(out_ready),
        .out_data(bug_out_data)
      );
    
      integer good_accepted = 0;
      integer good_delivered = 0;
      integer bad_accepted = 0;
      integer bad_delivered = 0;
      reg [7:0] expected [0:2];
      integer expected_head = 0;
      reg bad_diverged = 1'b0;
      reg good_was_stalled = 1'b0;
      reg [7:0] held_good_word;
    
      always @(posedge clk) begin
        if (rst) begin
          good_accepted <= 0;
          good_delivered <= 0;
          bad_accepted <= 0;
          bad_delivered <= 0;
          expected_head <= 0;
          bad_diverged <= 1'b0;
          good_was_stalled <= 1'b0;
          bad_source_index <= 0;
        end else begin
          if ((good_accepted - good_delivered) != ({1'b0, good.out_full} + {1'b0, good.skid_full})) begin
            $display("FAIL: good buffer violated accepted-minus-delivered conservation");
            $finish;
          end
          if (good_was_stalled && (!out_valid || out_data !== held_good_word)) begin
            $display("FAIL: good output changed its stalled offer");
            $finish;
          end
          good_was_stalled <= out_valid && !out_ready;
          held_good_word <= out_data;
          if (in_valid && in_ready) good_accepted <= good_accepted + 1;
          if (out_valid && out_ready) begin
            if (expected_head >= 3) begin
              $display("FAIL: good buffer delivered an unexpected extra item");
              $finish;
            end
            if (out_data !== expected[expected_head]) begin
              $display("FAIL: good stream expected %02x, got %02x", expected[expected_head], out_data);
              $finish;
            end
            expected_head <= expected_head + 1;
            good_delivered <= good_delivered + 1;
          end
          // Each instance gets A/B/C once, advancing on its own input handshake.
          // The mutant's incorrect READY must not make the shared source repeat C.
          if (bug_in_valid && bug_in_ready) begin
            bad_accepted <= bad_accepted + 1;
            bad_source_index <= bad_source_index + 1'b1;
          end
          if (bug_out_valid && out_ready) bad_delivered <= bad_delivered + 1;
          if ((bad_accepted - bad_delivered) != ({1'b0, bad.out_full} + {1'b0, bad.skid_full}))
            bad_diverged <= 1'b1;
        end
      end
    
      task drive(input v, input [7:0] d, input rdy);
        begin
          @(negedge clk);
          in_valid = v;
          in_data = d;
          out_ready = rdy;
          if (v) traffic_started = 1'b1;
        end
      endtask
    
      initial begin
        expected[0] = 8'hA0;
        expected[1] = 8'hB1;
        expected[2] = 8'hC2;
        $dumpfile("wave.vcd");
        $dumpvars(0, tb);
    
        repeat (2) @(posedge clk);
        @(negedge clk);
        rst = 1'b0;
        drive(1'b1, 8'hA0, 1'b1); // A enters the output register.
        drive(1'b1, 8'hB1, 1'b0); // A stalls; B enters the skid slot.
        drive(1'b1, 8'hC2, 1'b0); // C is retained upstream; no input acceptance.
        drive(1'b1, 8'hC2, 1'b1); // A leaves; C is still not accepted this edge.
        drive(1'b1, 8'hC2, 1'b1); // B leaves while C enters.
        drive(1'b0, 8'h00, 1'b1); // C leaves.
        drive(1'b0, 8'h00, 1'b1);
    
        if (good_accepted != 3 || good_delivered != 3 || expected_head != 3) begin
          $display("FAIL: good counts accepted=%0d delivered=%0d checked=%0d", good_accepted, good_delivered, expected_head);
          $finish;
        end
        if (!bad_diverged) begin
          $display("FAIL: dropped-beat mutant was not detected");
          $finish;
        end
        if (bad_accepted != 3 || bad_delivered >= bad_accepted) begin
          $display("FAIL: mutant did not accept all three inputs and lose a retained item");
          $finish;
        end
        $display("PASS: correct skid retained A/B/C; conservation exposed the dropped-beat mutant");
        $finish;
      end
    endmodule

    Backpressure propagation

    When a downstream destination stops accepting, finite queues can absorb work temporarily; once their admission thresholds are reached, backpressure can propagate upstream through FIFOs, arbiters and bridge controls. The source you see stalled may be separated from the limiting service point, but the stall itself does not identify that point.

    Plot actual FIFO occupancy alongside its local full/empty flags and accepted input/output events. High occupancy can explain frequent input stalls. An apparently empty FIFO with a stalled upstream offer needs further investigation: reset, arbitration, registered flags or a downstream dependency can prevent acceptance. Occupancy alone does not identify the cause.

    • Measure stall cycles per channel: % cycles where valid=1 && ready=0.

    • Credit exhaustion: sender has data but credit count = 0.

    • Arbiter service: inspect eligibility, grants and completion assumptions; a QoS field alone does not establish fairness.

    Common misconceptions

    If valid and ready are both high most of the time, the interface is healthy.

    Average handshake rate hides tail latency. A master stalled 40% of cycles on AR may still show 'active' waveforms. Measure stall percentage and p99 latency, not visual activity.

    FIFO depth=2 is enough because average throughput is low.

    Size from the largest possible backlog of accepted writes minus completed reads, including any additional acceptances before backpressure takes effect. A burst's beat count already measures items; multiplying it by a clock ratio is not a general FIFO-depth formula. Average rate does not bound burst backlog or a paused consumer.

    Credit-based protocols are fundamentally different from ready/valid.

    Both regulate admission from finite capacity, but their contracts differ. A ready/valid destination makes a per-edge acceptance decision; a credit protocol grants units that the sender accounts for until return/update. Debug compares advertised capacity, consumption, return and the traffic class—do not substitute one protocol's timing rules for the other.

    Key takeaways

    • Acceptance happens at a sampled VALID && READY edge; retain an unaccepted offer through its accepting edge.

    • Count storage as accepted inputs minus completed outputs, with initial occupancy included.

    • Present pending work without waiting for READY, and assess fairness separately from local backpressure.

    Apply the mechanism

    Continue to the integration exercise

    Then trace a failure

    Previous

    11 / 158

    Next: Handshake & Backpressure: Integration

    Up next

    Up nextHandshake & Backpressure: Integration

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Handshake & Backpressure: Debug
    • Handshake & Backpressure: Interview
    • Handshake & Backpressure: Deep Dive
    clkgood_validgood_readygood_dataa0good_accepted01bad_validbad_readybad_accepted0
    Registered waiting: the repaired pair completes; the mutually waiting pair does not.
    clkrstin_validin_dataa0b1c20in_readyout_validout_data0a0b1c2out_readygood_accepted0123good_delivered012bad_diverged
    Two-slot skid buffer: accepted items survive a stalled output; a dropped-item variant diverges.