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

  • How to Use This Course
  • Reading Paths
  • Concept Map
  • How to Read the Diagrams
  • Part 0: What Is a Protocol?10
  • Part 1: Timing and Clocking11
  • Part 2: Encoding and Framing12
  • Part 3: Handshakes13
    • Valid-Only Streams
    • Wait States and Stretching
    • The Valid/Ready Handshake
    • The Valid/Ready Rules
    • Request/Acknowledge: Two-Phase and Four-Phase
    • Push and Pull
    • Global Stall or Elastic Pipeline
    • Register Slices
    • Skid Buffers
    • Bubbles and Half Throughput
    • Latency-Insensitive Design
    • Protocols Compared: Handshakes
    • Drills: Handshakes
  • Part 4: Flow Control17
  • Part 5: Transactions19
  • Part 6: Concurrency and Performance17
  • Part 7: Ordering13
  • Part 8: Sharing and Arbitration12
  • Part 9: Deadlock and Liveness13
  • Part 10: Reliability and Recovery20
  • Part 11: Bring-Up and Link Management10
  • Part 12: Power8
  • Part 13: Shared State and Coherence9
  • Part 14: Security and Isolation7
  • Part 15: Specifying and Verifying11
  • Part 16: Protocols Dissected17
  • Part 17: Capstones5
  • Part 18: Interview Preparation6
Loading lesson

‹Part 3: Handshakes
  1. Topics
  2. First Principles of Hardware Protocols
  3. Part 3: Handshakes
  4. Bubbles and Half Throughput

Part 3 · Handshakes · Senior (S)

Bubbles and Half Throughput

The classic valid/ready stage that runs at exactly half speed with no stalls at all: why in_ready = !full creates a bubble every other cycle, and the one-term fix.

On this page

    Note

    Level: Senior (S) · Answers: Is the receiver ready?

    Before this: Register Slices

    A new pipeline passes every functional test. Performance testing shows it delivers exactly half the expected rate. There are no stalls: the source always has data and the sink is always ready. Every waveform looks healthy, with valid and ready toggling neatly. That neat toggling is the bug.

    How can a pipeline with no stalls lose half its throughput, and how do you see it on a waveform?

    The obvious answer, and why it breaks

    The obvious one-entry pipeline stage drives ready from "am I empty?": in_ready = !full. It never overflows, it is easy to reason about, and its ready comes from a register, which helps timing. But think through a cycle where the stage is full and its word is leaving. The stage is full, so in_ready is 0, so no new word enters in this cycle. Next cycle the stage is empty and accepts a word; the cycle after, it is full again and refuses. It alternates: one word every two cycles, forever.

    The idea: a stage that is emptying is not full

    A stage can accept a new word in the same cycle that its current word leaves, because the register is overwritten at the same clock edge the old value is taken. The correct condition is:

    `in_ready = !full || out_ready`: accept if empty, or if the current word is leaving this cycle.

    With that one extra term the stage passes a word every cycle when the sink is ready, and it still never overflows: when the sink stalls, out_ready is 0 and the stage behaves as before. The extra term is exactly what makes the forward register slice of lesson 3.8 a full-throughput stage. Its price is that in_ready now depends combinationally on out_ready, so a chain of such stages makes a long ready path, which is what skid buffers break.

    Build it: the two stages side by side

    The testbench drives two one-entry stages from sources that always have data into sinks that are always ready. The only difference is the ready condition. The checker counts words delivered over 20 cycles.

    stage.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
    // Two one-entry pipeline stages fed by a source that always has data and drained by a
    // sink that is always ready. Stage BAD accepts a word only when it is empty
    // (in_rdy = !full), so it cannot take a new word in the cycle it hands one on: it
    // alternates full and empty and passes one word every two cycles. Stage GOOD accepts
    // when it is empty or being emptied (in_rdy = !full || out_rdy): one word per cycle.
    module stage #(parameter bit GOOD = 1) (
      input  logic clk, rst_n,
      input  logic in_vld, output logic in_rdy, input logic [7:0] in_data,
      output logic out_vld, input logic out_rdy, output logic [7:0] out_data
    );
      assign in_rdy = GOOD ? (!out_vld || out_rdy) : !out_vld;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) out_vld <= 0;
        else begin
          if (out_vld && out_rdy) out_vld <= 0;              // word leaves
          if (in_vld && in_rdy) begin out_vld <= 1; out_data <= in_data; end  // word arrives
        end
    endmodule
    
    module tb;
      logic clk = 0;
      always #5 clk = ~clk;
      logic rst_n = 0;
      initial begin
        $dumpfile("wave.vcd"); $dumpvars(0, tb);
        #12 rst_n = 1;
      end
      logic [7:0] w_bad = 0, w_good = 0;
      wire rdy_bad, vld_bad, rdy_good, vld_good;
      wire [7:0] d_bad, d_good;
      stage #(.GOOD(0)) bad  (.clk, .rst_n, .in_vld(1'b1), .in_rdy(rdy_bad),  .in_data(w_bad),  .out_vld(vld_bad),  .out_rdy(1'b1), .out_data(d_bad));
      stage #(.GOOD(1)) good (.clk, .rst_n, .in_vld(1'b1), .in_rdy(rdy_good), .in_data(w_good), .out_vld(vld_good), .out_rdy(1'b1), .out_data(d_good));
      always @(posedge clk) if (rst_n) begin
        if (rdy_bad)  w_bad  <= w_bad + 1;
        if (rdy_good) w_good <= w_good + 1;
      end
      int n_bad = 0, n_good = 0, cyc = 0;
      always @(posedge clk) if (rst_n) begin
        cyc++;
        if (cyc > 2 && cyc <= 22) begin n_bad += vld_bad; n_good += vld_good; end
      end
      initial begin
        #260;
        if (n_good == 20 && n_bad == 10)
          $display("PASS: over 20 cycles the stage with in_rdy = !full passed %0d words, the stage with in_rdy = !full || out_rdy passed %0d", n_bad, n_good);
        else $display("FAIL: bad %0d, good %0d", n_bad, n_good);
        $finish;
      end
    endmodule

    Watch it run

    How to read it, slot by slot:

    • The bad stage: rdy_bad and vld_bad toggle in opposite phase, every cycle. When the stage holds a word (vld_bad = 1) it refuses a new one (rdy_bad = 0); next cycle it is empty, accepts, and is full again. d_bad changes only every other slot: 0, 1, 2, 3, 4, 5 in eleven slots.

    • The good stage: rdy_good stays high, vld_good stays high from slot 1, and d_good changes every slot: 0 to 10 in eleven slots.

    • The signature to remember: valid and ready of one stage toggling in opposite phase, with no stall anywhere, is a half-throughput stage.

    Worked numbers: what it costs a chain

    With illustrative numbers, a chain of stages is only as fast as its slowest stage:

    Diagram
    A 10-stage pipeline at 500 MHz, one word per cycle, 64-bit words:
      all stages correct:  500 M words/s x 8 B = 4.0 GB/s
      one bad stage:       250 M words/s x 8 B = 2.0 GB/s   (the whole chain runs at its rate)

    One missing term in one stage halves the bandwidth of everything behind it, without a single stall in any functional test.

    The design space

    Stage

    Throughput

    Ready path

    Use

    in_ready = !full

    Half

    Short (from a register)

    Only when half rate is enough and timing is very tight

    in_ready = !full || out_ready

    Full

    Combinational through the stage

    The standard pipeline stage (forward slice)

    Skid buffer (two entries)

    Full

    Short (from a register)

    When both full throughput and a cut ready path are needed

    Where it appears

    Where

    Why it matters

    Any valid/ready pipeline, including AXI and AXI-Stream stages

    The handshake allows a transfer every cycle; a stage that refuses while emptying wastes that capacity silently.

    Interview questions on FIFOs and pipeline stages

    A favourite: "your pipeline runs at half rate with no stalls, why?"

    What breaks

    A team measures 50% of the expected bandwidth on an otherwise idle system (illustrative). Hunting for stalls finds none, because there are none. The breakthrough comes from looking at one stage's valid and ready together: they toggle in opposite phase every cycle. The stage had in_ready = !full, copied from a FIFO template where it was correct for a deeper FIFO. Adding || out_ready doubled the bandwidth. The regression gained a throughput check: with an always-ready sink and an always-valid source, words out must equal cycles.

    Why this matters

    Senior lens. Functional tests cannot catch this bug: everything is correct, just slow. Every handshake block should have a throughput test with an always-valid source and an always-ready sink, asserting one transfer per cycle in steady state. In design reviews, look at each stage's ready expression: if it does not include the output's ready (or come from a skid buffer), ask whether half rate is intended.

    Interview

    Asked: "Your pipeline runs at half rate with no stalls. Why?"

    Weak answer: There must be a stall somewhere.

    Strong answer: A one-entry stage with in_ready = !full can't accept a word in the same cycle its current word leaves, so it alternates full and empty: one word every two cycles. On a waveform its valid and ready toggle in opposite phase every cycle with no stall anywhere. The fix is in_ready = !full || out_ready, accepting while emptying, or a two-entry skid buffer if the ready path must stay registered.

    Follow-ups: (1) Why doesn't in_ready = !full || out_ready overflow? (2) What test would catch this automatically?

    Check yourself With in_ready = !full || out_ready, what happens when the sink stalls?

    out_ready is 0, so in_ready = !full: the full stage refuses new words and holds its own, exactly as it should. Nothing overflows.

    Check yourself How do you spot a half-throughput stage on a waveform?

    Its valid and ready toggle in opposite phase every cycle, and its data changes every other cycle, while no stage downstream is stalling.

    Check yourself Why might a designer choose in_ready = !full anyway?

    Its ready comes straight from a register, giving the shortest ready path. That is acceptable only if half rate is enough; otherwise a skid buffer gives a registered ready at full rate.

    Key takeaways

    • A one-entry stage with in_ready = !full refuses a new word while its old one leaves, so it runs at half rate with no stalls.

    • The fix is in_ready = !full || out_ready, or a skid buffer when ready must stay registered.

    • Catch it with a throughput test: always-valid source, always-ready sink, one word out per cycle.

    Keep going

    Next: Lesson 3.11, Latency-Insensitive Design. The theory behind everything in this part: circuits whose behaviour does not depend on how many cycles each hop takes.

    Related topics

    • Part 3 overview: Handshakes

    • RTL course: Skid Buffer and Pipeline Stages

    Previous

    47 / 234

    Next: Latency-Insensitive Design

    Up next

    Up nextLatency-Insensitive Design

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Valid-Only Streams
    • Wait States and Stretching
    • The Valid/Ready Handshake
    clkrdy_badvld_badd_badx012345rdy_goodvld_goodd_goodx0123456789a
    Simulation output: the stage with in_rdy = !full (top three lanes) passes a word every other cycle; the stage with in_rdy = !full || out_rdy (bottom three lanes) passes one every cycle. One slot is one cycle.