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

  • RTL Foundations57
  • Combinational Design58
  • Sequential Design61
  • FSM & Control66
  • Timing & Clocking64
  • Reset & CDC58
  • Memory & Buses94
    • SRAM and ROM
    • Register Files
    • Valid/Ready Handshake
      • Rules That Keep Flow Control Safe
      • One-deep skid buffer
      • Skid Buffer and Pipeline Stages
      • AXI-Style Channels and Backpressure
      • AXI-style interpretation
      • Key takeaways
      • Three Pipeline Stage Styles, Measured
      • Common pitfalls
      • Worked Examples and Waveforms
      • Corner Cases and Stress Tests
      • Integration and Sign-off Notes
    • AXI-Lite
    • AXI-Stream
    • Bus Integration
    • Synchronous FIFO
    • Register-Map and CSR RTL
    • Memory Reliability
    • Peripheral RTL Integration
    • Full AXI4: RTL Roadmap
  • Advanced RTL60
  • Senior & Interview175
  • Glossary and Notation
Loading lesson

‹Valid/Ready Handshake
  1. Topics
  2. RTL Design
  3. Memory & Buses
  4. Valid/Ready Handshake
  5. Three Pipeline Stage Styles, Measured

Part 7 · Memory & Buses · Intermediate

Three Pipeline Stage Styles, Measured

Pass-through, plain registered and skid-buffer stages under the same traffic: measured throughput, latency, ready path and flop count.

On this page

    Why this matters

    A plain registered slice looks like the safe choice and quietly halves the link: 0.50 words per cycle with a sink that never stalls. Measuring the three stage styles under the same traffic shows what each one costs in throughput, latency, flops and timing.

    After this lesson you can: predict throughput and latency for a pass-through stage, a plain registered slice and a skid buffer under any ready pattern, and pick the right one for a link.

    Same traffic, half the throughput

    Put a plain registered slice between a source that always has data and a sink that is always ready, and it moves 0.50 words per cycle. A skid buffer in the same place moves 1.00. Both drive their upstream ready from a flop, so both are safe for timing, but only one keeps the link full.

    This lesson measures three single-stage styles side by side in one bench: the pass-through register from Valid/Ready Flow Control, a plain registered slice, and the skid buffer from Skid Buffer and Pipeline Stages.

    The three styles between a source and a sink. Only the pass-through stage has a ready path that runs straight through it; the plain slice and the skid buffer drive upstream ready from a flop. Flop counts are 9, 9 and 19

    Read each row from the sink back to the source along the dashed line. That line is ready, and where it starts decides timing.

    • Pass-through: in_ready = !out_valid || out_ready. The sink's ready goes through one OR gate to the source in the same cycle.

    • Plain slice: in_ready = !out_valid. The sink's ready only empties the slice; the source sees a flop.

    • Skid buffer: s_ready_o = ready_q, a flop that is low only when both data registers are full.

    The plain registered slice

    The pass-through stage and the skid buffer are the modules from their own lessons, unchanged. The plain slice is the obvious way to register ready: take a word when empty, hand it on, and only then take the next.

    reg_slice_naive.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // Style 2: plain registered slice. in_ready comes straight from a flop, so the slice
    // takes a new word only when it is empty: never in the cycle its word leaves.
    module reg_slice_naive #(parameter int W = 8) (
      input  logic         clk, rst_n,
      input  logic         in_valid,  output logic in_ready,  input  logic [W-1:0] in_data,
      output logic         out_valid, input  logic out_ready, output logic [W-1:0] out_data
    );
      assign in_ready = !out_valid;                        // registered: only the slice's own flop
    
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n)                     out_valid <= 1'b0;
        else if (in_valid && in_ready)  out_valid <= 1'b1;   // fill when empty
        else if (out_ready)             out_valid <= 1'b0;   // drain; no refill on this edge
    
      always_ff @(posedge clk)
        if (in_valid && in_ready) out_data <= in_data;
    endmodule

    The else if is the whole story. On the edge where the word leaves, the slice cannot also take a new one, because in_ready was low all cycle: the slice was full. Every word costs one fill cycle and at least one drain cycle.

    The measurement

    One bench drives all three stages in parallel. Each has its own source that offers a new word as soon as the last one is taken, and all three share the same sink ready. Three sink patterns run for 1,200 cycles each: always ready, pseudo-random ready (591 of 1,200 cycles), and a burst stall of 8 ready cycles followed by 4 stalled ones. A scoreboard per stage checks every word for order, loss and duplication.

    • Words per cycle: words the stage accepted in the 1,200-cycle window, divided by 1,200.

    • Latency: cycles from the edge the stage accepts a word to the edge the sink takes it. 1 is the minimum for any registered stage.

    • Combinational ready: between clock edges the bench flips the sink's ready and back. If the stage's upstream ready moves with it, there is a combinational path. A structural check in Yosys agrees: the combinational output cone of the sink ready reaches in_ready of the pass-through stage and nothing in the other two.

    • Flops: Yosys stat after synth on each module, with 8-bit data.

    Diagram
    style        pattern       words/cycle  avg latency  max latency  comb ready
    pass-through always ready  1.00         1.00         1            yes
    naive slice  always ready  0.50         1.00         1             no
    skid buffer  always ready  1.00         1.00         1             no
      (sink ready in 1200 of 1200 cycles)
    pass-through random 50%    0.49         2.03         12            yes
    naive slice  random 50%    0.33         2.00         12             no
    skid buffer  random 50%    0.49         3.05         13             no
      (sink ready in 591 of 1200 cycles)
    pass-through burst stall   0.67         1.50         5            yes
    naive slice  burst stall   0.33         1.99         5             no
    skid buffer  burst stall   0.67         2.00         5             no
      (sink ready in 800 of 1200 cycles)
    PASS: 2595 + 1404 + 2597 words scoreboarded in order, none lost or duplicated

    The same numbers as a table. Each cell is words per cycle, then the average latency in cycles in brackets.

    Style

    Always ready

    Random 50%

    Burst stall

    Combinational ready

    Flops

    Pass-through

    1.00 (1.00)

    0.49 (2.03)

    0.67 (1.50)

    Yes

    9

    Plain registered slice

    0.50 (1.00)

    0.33 (2.00)

    0.33 (1.99)

    No

    9

    Skid buffer

    1.00 (1.00)

    0.49 (3.05)

    0.67 (2.00)

    No

    19

    Read the table one column at a time.

    • Always ready: the pass-through stage and the skid buffer keep the link full. The plain slice is stuck at 0.50 with a sink that never stalls: that is its ceiling, not a back-pressure effect.

    • Random 50%: the sink is the limit, and both full-rate styles reach it at 0.49. The plain slice falls to 0.33: after each word leaves it spends a cycle refilling, then waits about two cycles for the next ready.

    • Burst stall: 8 ready cycles in 12 allow 0.67, and both full-rate styles reach it. The plain slice moves 4 words in each 8-cycle burst, so 4 in 12 is 0.33.

    • Cost: the skid buffer pays 10 more flops (a second data register and the ready flop) for full rate with a registered ready.

    Each slot is one clock cycle, and a word leaves at the edge that ends a slot where its xfer lane is high. The n_ lanes are the plain slice and the k_ lanes the skid buffer; both see the same m_ready.

    • Slots 0 to 5: n_ready alternates 1 and 0, so the plain slice delivers only in slots 1, 3 and 5. k_ready stays 1 and the skid buffer delivers in every slot from 1.

    • Slot 6: the sink stalls. k_ready is still 1, so the skid buffer takes one more word into its skid register.

    • Slots 7 and 8: both skid registers are full and k_ready is 0. Delivery resumes in slot 8 and k_ready returns in slot 9.

    • Count: in 11 slots the plain slice delivers 5 words and the skid buffer 8.

    Why the skid buffer has the longest latency

    Under random back-pressure the skid buffer's average latency is 3.05 cycles against 2.03 for the pass-through stage, at the same throughput. That is not a defect. Average occupancy equals throughput times latency (Little's law): 0.49 x 3.05 is about 1.5 words held, against about 1.0 for the pass-through stage. The skid buffer accepts a word while the sink is stalled and parks it in the second register, where it waits behind the first. The storage that hides the late ready is the same storage that adds the wait.

    With an always-ready sink every style has latency 1, because nothing ever waits.

    Which one when

    • Pass-through register: inside a block, a stage or two deep, where the ready chain is short and the clock is easy. The cheapest full-rate stage.

    • Plain registered slice: only where half rate is acceptable and flops are scarce, such as a slow configuration or control stream that never needs a word every cycle.

    • Skid buffer: at block and timing-partition boundaries, every few pass-through stages in a long pipeline, and wherever ready crosses a long wire. Full rate and a registered ready, for twice the data flops.

    Common mistakes

    Common pitfall

    Chaining pass-through stages and calling the pipeline registered. The data is registered; the ready is not. Every stage adds one gate to a single combinational path from the last sink back to the first source. Yosys ltp -noff on a flattened chain of N pipe_stage instances reports a longest path of length 2, 5 and 9 for N = 1, 4 and 8. A chain of 8 skid buffers stays at 5, the same as one.

    pipe_chain.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // snippet: N pass-through stages in a row, synthesised for the longest-path run (pipe_stage is the module from the bench)
    module pipe_chain #(parameter int N = 4, parameter int W = 8) (
      input  logic         clk, rst_n,
      input  logic         in_valid,  output logic in_ready,  input  logic [W-1:0] in_data,
      output logic         out_valid, input  logic out_ready, output logic [W-1:0] out_data
    );
      logic [N:0] v, r;
      logic [W-1:0] d [N+1];
      assign v[0] = in_valid;  assign in_ready = r[0];  assign d[0] = in_data;
      assign out_valid = v[N]; assign r[N] = out_ready; assign out_data = d[N];
      for (genvar i = 0; i < N; i++) begin : st
        pipe_stage #(W) s (.clk, .rst_n, .in_valid(v[i]), .in_ready(r[i]), .in_data(d[i]),
                           .out_valid(v[i+1]), .out_ready(r[i+1]), .out_data(d[i+1]));
      end
    endmodule
    Diagram
    Longest topological path in $paramod\pipe_chain\N=s32'00000000000000000000000000000100 (length=5):
        0: \st[3].s.out_valid
        1: \st[2].s.out_ready (via $abc$200$auto$blifparse.cc:397:parse_blif$204)
        2: \st[1].s.out_ready (via $abc$200$auto$blifparse.cc:397:parse_blif$205)
        3: \st[0].s.out_ready (via $abc$200$auto$blifparse.cc:397:parse_blif$206)
        4: \st[0].s.in_ready (via $abc$200$auto$blifparse.cc:397:parse_blif$210)
    ...

    The path for N = 4 starts at the last stage's out_valid flop and walks back through every stage's out_ready to the first in_ready. Insert a skid buffer every few stages to cut it.

    Check yourself A plain registered slice feeds a sink that is ready every cycle. What throughput do you get, and what happens if you let it accept on the edge its word leaves?

    0.50 words per cycle. Letting it accept on that edge means in_ready = !out_valid || out_ready, which is the pass-through stage: full rate, but ready is combinational again. Full rate with a registered ready needs a second data register, which is the skid buffer.

    Check yourself Under random back-pressure the skid buffer and the pass-through stage both move 0.49 words per cycle, but the skid buffer's average latency is a cycle higher. Where does the extra cycle come from?

    From the second register. The skid buffer holds about 1.5 words on average against 1.0, and a word in the skid register waits behind the one in the output register. Same throughput, more occupancy, so more latency.

    Testbench

    This bench produced the log above. A harness wraps each style with a saturating source and a scoreboard; the top module runs the three sink patterns, probes the ready path between edges, prints the numbers and fails on any lost, duplicated or reordered word.

    pipe_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
    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
    // Style 1: pass-through register (from Valid/Ready Flow Control). Data is registered,
    // but in_ready is a wire through the stage: it depends on out_ready in the same cycle.
    module pipe_stage #(parameter int W = 8) (
      input  logic         clk, rst_n,
      input  logic         in_valid,  output logic in_ready,  input  logic [W-1:0] in_data,
      output logic         out_valid, input  logic out_ready, output logic [W-1:0] out_data
    );
      assign in_ready = !out_valid || out_ready;           // combinational ready path
    
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n)          out_valid <= 1'b0;
        else if (in_ready)   out_valid <= in_valid;
    
      always_ff @(posedge clk)
        if (in_ready && in_valid) out_data <= in_data;
    endmodule
    
    // Style 2: plain registered slice. in_ready comes straight from a flop (no path from
    // out_ready), so the slice takes a new word only when it is empty: never in the cycle
    // its word leaves.
    module reg_slice_naive #(parameter int W = 8) (
      input  logic         clk, rst_n,
      input  logic         in_valid,  output logic in_ready,  input  logic [W-1:0] in_data,
      output logic         out_valid, input  logic out_ready, output logic [W-1:0] out_data
    );
      assign in_ready = !out_valid;                        // registered: only the slice's own flop
    
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n)                     out_valid <= 1'b0;
        else if (in_valid && in_ready)  out_valid <= 1'b1;   // fill when empty
        else if (out_ready)             out_valid <= 1'b0;   // drain; no refill on this edge
    
      always_ff @(posedge clk)
        if (in_valid && in_ready) out_data <= in_data;
    endmodule
    
    // Style 3: two-entry skid buffer (from Skid Buffer and Pipeline Stages, unchanged).
    module skid_buffer #(
      parameter int DW = 8
    ) (
      input  logic          clk,
      input  logic          rst_n,
      input  logic          s_valid_i,
      output logic          s_ready_o,
      input  logic [DW-1:0] s_data_i,
      output logic          m_valid_o,
      input  logic          m_ready_i,
      output logic [DW-1:0] m_data_o
    );
      logic [DW-1:0] out_data, skid_data;
      logic out_valid, skid_valid, ready_q;
      logic next_out_valid, next_skid_valid;
      logic [DW-1:0] next_out_data, next_skid_data;
      wire push, pop;
    
      assign m_valid_o = out_valid;
      assign m_data_o  = out_data;
      assign s_ready_o = ready_q;               // registered: no combinational m_ready_i to s_ready_o path
      assign push = s_valid_i && ready_q;
      assign pop  = out_valid && m_ready_i;
    
      always_comb begin
        next_out_valid  = out_valid;
        next_skid_valid = skid_valid;
        next_out_data   = out_data;
        next_skid_data  = skid_data;
        if (pop) begin
          if (skid_valid) begin
            next_out_data = skid_data;
            if (push) next_skid_data = s_data_i;  // unreachable with registered ready, kept for safety
            else      next_skid_valid = 1'b0;
          end else if (push) begin
            next_out_data  = s_data_i;
            next_out_valid = 1'b1;
          end else begin
            next_out_valid = 1'b0;
          end
        end else if (push) begin
          if (!out_valid) begin
            next_out_data  = s_data_i;
            next_out_valid = 1'b1;
          end else begin
            next_skid_data  = s_data_i;
            next_skid_valid = 1'b1;
          end
        end
      end
    
      always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
          out_valid  <= 1'b0;
          skid_valid <= 1'b0;
          ready_q    <= 1'b1;
          out_data   <= '0;
          skid_data  <= '0;
        end else begin
          out_valid  <= next_out_valid;
          skid_valid <= next_skid_valid;
          out_data   <= next_out_data;
          skid_data  <= next_skid_data;
          ready_q    <= !(next_out_valid && next_skid_valid);
        end
      end
    endmodule
    
    // One stage of the chosen style between a saturating source and the shared sink ready.
    // Keeps a scoreboard (order, loss, duplication) and the throughput and latency counts.
    module style_harness #(parameter int STYLE = 0) (
      input  logic clk, rst_n, run, m_ready
    );
      logic s_valid = 0, s_ready, m_valid;
      logic [7:0] s_data = 0, m_data;
      int cyc = 0, accepted = 0, delivered = 0, lat_sum = 0, lat_max = 0, errors = 0;
      int q_data [$];
      int q_time [$];
    
      if (STYLE == 0) begin : g
        pipe_stage #(8) dut (.clk, .rst_n, .in_valid(s_valid), .in_ready(s_ready), .in_data(s_data),
                             .out_valid(m_valid), .out_ready(m_ready), .out_data(m_data));
      end else if (STYLE == 1) begin : g
        reg_slice_naive #(8) dut (.clk, .rst_n, .in_valid(s_valid), .in_ready(s_ready), .in_data(s_data),
                                  .out_valid(m_valid), .out_ready(m_ready), .out_data(m_data));
      end else begin : g
        skid_buffer #(8) dut (.clk, .rst_n, .s_valid_i(s_valid), .s_ready_o(s_ready), .s_data_i(s_data),
                              .m_valid_o(m_valid), .m_ready_i(m_ready), .m_data_o(m_data));
      end
    
      always @(posedge clk) if (rst_n) begin           // pre-edge values: what this edge transfers
        cyc++;
        if (s_valid && s_ready) begin                  // input transfer
          q_data.push_back(s_data);
          q_time.push_back(cyc);
          accepted++;
        end
        if (m_valid && m_ready) begin                  // output transfer: check order, record latency
          if (q_data.size() == 0) errors++;
          else begin
            if (m_data !== 8'(q_data[0])) errors++;
            if (cyc - q_time[0] > lat_max) lat_max = cyc - q_time[0];
            lat_sum += cyc - q_time[0];
            void'(q_data.pop_front());
            void'(q_time.pop_front());
          end
          delivered++;
        end
        // Saturating source: offer a new word as soon as the last one is taken, hold it otherwise.
        if (s_valid && s_ready) begin s_data <= s_data + 1; s_valid <= run; end
        else if (!s_valid) s_valid <= run;
      end
    endmodule
    

    Remember

    • Pass-through: full rate, 9 flops, but ready is combinational and grows by one gate per chained stage.

    • Plain registered slice: registered ready, 9 flops, but at most one word every two cycles.

    • Skid buffer: full rate and registered ready for 19 flops; under back-pressure it holds more words, so latency rises.

    Rule of thumb: if ready must be registered and the link must run at full rate, you need two data registers.

    How this was checked

    Tool: Icarus Verilog 13.0, run on 2026-10-17. Commands: iverilog -g2012 -o s.vvp styles.sv; vvp -n s.vvp (styles.sv is the bench on the page). Result: the printed table on the page, then PASS: 2595 + 1404 + 2597 words scoreboarded in order, none lost or duplicated. The bench also runs under node scripts/tools/run-rtl-testbenches.mjs rtl/memory-buses/valid-ready (PASS).

    Tool: Yosys 0.69+post (git sha1 143eb14f9), run on 2026-10-17. Commands, for each of pipe_stage, reg_slice_naive and skid_buffer: yosys -p "read_verilog -sv stages_rtl.sv; synth -top <module>; stat" giving 9, 9 and 19 flip-flops (1 $_DFFE_PN0P_ + 8 $_DFFE_PP_; the same; 16 $_DFFE_PN0P_ + 2 $_DFF_PN0_ + 1 $_DFF_PN1_). Ready path: yosys -p "...; synth -top <module>; select -list i:<sink ready> %coe* w:<upstream ready> %i" lists pipe_stage/in_ready and nothing for the other two.

    Chain: yosys -p "read_verilog -sv stages_rtl.sv chain.sv; chparam -set N <n> pipe_chain; synth -flatten -top pipe_chain; ltp -noff" for N = 1, 4, 8 gave length 2, 5, 9; the same with skid_buffer stages gave length 5 for N = 1 and 8. The waveform comes from wave-src/valid-ready/stage-styles.sv via node scripts/tools/gen-waves.mjs memory-buses:valid-ready (no mismatch).

    Scope and limits: one deterministic pseudo-random seed and fixed patterns, 1,200 cycles each, with a saturating source; throughput figures are for that traffic. Flop counts are Yosys generic cells for 8-bit data; ltp counts generic cells on a path, not delay in a library.

    Keep going

    • Practice: Two-stage valid/ready pipeline: throughput, latency, backpressure and skid buffers

    • Code lab: Skid buffer

    • Code lab: Pipeline register stage

    • Interview: run a mock interview on this topic

    Related topics

    • Skid Buffer and Pipeline Stages

    • Valid/Ready Flow Control

    Previous

    390 / 694

    Next: Common pitfalls

    Up next

    Up nextCommon pitfalls

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Rules That Keep Flow Control Safe
    • One-deep skid buffer
    • Skid Buffer and Pipeline Stages
    clkm_readyn_readyn_xferk_readyk_xfer
    The plain registered slice and the skid buffer side by side, same source and same sink ready: the slice delivers in every other slot, the skid buffer in every slot, and a two-slot stall costs the skid buffer nothing after it ends