Skip to content

RTL Design Patterns · Chapter 9 · Handshake & Flow Control

Ready/Valid Pipelining

A long valid/ready datapath will not close timing when both the forward path, the valid signal and its data, and the backward ready path run combinationally from end to end, because each grows with the length of the chain. You cut those long paths with registered handshake stages, but not every register is a handshake stage. Drop a plain flop on valid and data with ready wired through and you re-introduce the dropped-word bug; build a stage that cannot accept and offer in one cycle and you insert a bubble on every transfer and halve throughput. This lesson names the three register-slice flavors, a forward valid register, a backward ready register, and the full skid buffer that breaks both, does the latency-versus-throughput accounting for a chain, and builds a depth-N chain plus the naive-flop bug in SystemVerilog, Verilog, and VHDL.

Intermediate14 min readRTL Design PatternsReady ValidPipeliningSkid BufferRegister SliceThroughput

Chapter 9 · Section 9.6 · Handshake & Flow Control

1. The Engineering Problem

You have built a valid/ready streaming datapath: a source hands words to stage A, A processes and hands to B, B to C, and C to a sink. Every link obeys the 9.1 contract — a word transfers on exactly the cycles where valid && ready are both high. Functionally it is correct. Then you run static timing, and it fails, and re-arranging the boolean logic inside any single stage does not fix it, because the problem is not inside a stage — it is in the two long wires that run through all of them.

Look at the two directions of the handshake as one clock period sees them. The forward pathvalid and the data it qualifies — starts at the source, threads the combinational logic of A, then B, then C, and only lands in a flop at the sink; its logic depth grows with every stage you add. The backward pathready — starts at the sink, threads C's stall logic, then B's, then A's, back to the source; it too grows with every stage. If both of these run combinationally end to end, then the longest register-to-register path in your design is the whole chain, forward or backward, and no amount of stage-internal cleverness shortens it. This is exactly the backpressure chain 9.2 warned about, now made concrete on a real multi-stage datapath and joined by its forward twin.

The remedy is to cut those long paths with registers — the same move a datapath uses everywhere. But a handshake link is not a bare bus, and you cannot cut it with a bare flop. valid/data and ready carry a contract: a word is neither dropped nor duplicated, order is preserved, and (if you care about bandwidth) one word still moves every cycle. Inserting the wrong register between two stages breaks that contract — a plain flop on valid/data with ready wired straight through drops the word in flight when ready falls (the 9.3 failure, §7), and a stage that cannot accept and offer in the same cycle inserts a bubble on every transfer and halves throughput. Pipelining a ready/valid path means inserting registered handshake stages — slices that preserve the contract while cutting the path — and choosing the right flavor for the path you need to break.

the-need.v — the long forward and backward paths, and the wrong way to cut them
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // Bare handshake chain: forward valid/data and backward ready both run
   // COMBINATIONALLY end to end. Both grow with chain length -> fails timing.
   //   stageA -> stageB -> stageC : valid/data ripples A..C in one period,
   //                                 ready ripples C..A in the same period.
 
   // WRONG cut #1 - a plain flop on valid/data, ready wired straight through:
   //   always_ff @(posedge clk) begin out_valid <= in_valid; out_data <= in_data; end
   //   assign in_ready = out_ready;          // ready still combinational -> no cut backward,
   //                                         // and the in-flight word is LOST when ready falls.
 
   // WRONG cut #2 - a stage that can only EITHER accept OR offer per cycle:
   //   inserts a BUBBLE every transfer -> throughput halves.
 
   // RIGHT cut - a registered handshake STAGE (skid slice) that registers BOTH
   // directions and still moves 1 word/cycle. Chain N of them to pipeline the path.

2. Mental Model

3. Pattern Anatomy

The pattern is composition: you already have the stage (the skid buffer, 9.3); pipelining is chaining it and choosing flavors. Everything hard is in knowing which path each slice cuts, why a naive cut breaks the contract, and how latency and throughput accumulate along the chain.

Why a naive register between two handshake stages breaks the contract. A bare bus can be pipelined with a plain flop because it carries no acceptance semantics — a value that arrives late is simply late. A handshake link cannot, because a plain flop on valid/data with ready wired straight through violates the contract in one of two ways. If ready runs through combinationally, the flop registers the forward direction but the upstream still sees the downstream ready this cycle; so when out_ready falls on a cycle a word was already committed off the previous cycle's high ready, that word lands in the flop and overwrites the word the flop was presenting — a word is dropped (this is precisely the 9.3 dropped-word bug, re-introduced as a pipeline slice; §7 dramatizes it). Alternatively, if the stage is made data-safe by refusing to accept in the same cycle it offers, it can never do both at once, so it inserts a bubble on every transfer and halves throughput. A plain flop is not a handshake stage; only a slice that registers the direction it cuts and preserves accept/offer semantics is.

The forward / backward / both taxonomy — which rope each slice cuts.

  • Forward (valid) register slice. Registers valid and data on the output; ready passes through combinationally (in_ready = out_ready, or gated by the slot's emptiness). It cuts the forward path — out_valid/out_data come out of flops — but leaves the backward ready path running through it. Correct and full-throughput on the forward side, but it does not break the long ready run. Use it when only the forward path is failing timing.
  • Backward (ready) register slice. Registers ready (the accept signal comes out of a flop); valid/data pass through combinationally. It cuts the backward path — in_ready comes out of a flop, so a downstream stall reaches only the nearest ready flop — but leaves the forward valid/data running through. Use it when only the backward path is long. (A pure backward slice still needs a landing spot for the in-flight word, which is why in practice the robust full-throughput registered-ready stage is the skid buffer.)
  • Full skid buffer (registers BOTH). The 9.3 two-slot stage: out_valid/out_data are registered and in_ready = !skid_valid is registered, and the extra skid slot lets it accept-and-offer in the same cycle. It cuts both ropes and sustains one word per cycle. This is the default pipeline slice on a timing-critical handshake, and it is the one this page chains.

Three register-slice flavors — which path each one breaks

data flow
Three register-slice flavors — which path each one breakscut forwardcut backwardcut bothcut bothchainNbare handshakelinkvalid/data AND ready both combinational -> both longforward (valid)sliceregisters valid/data; ready passes throughbackward (ready)sliceregisters ready; valid/data pass throughfull skid bufferregisters BOTH; accepts+offers same cyclechain of N skidslicespipelined handshake: +1 latency/slice, rate 1/cycle
Start from a bare handshake link where both the forward valid/data path and the backward ready path run combinationally and both grow with chain length. A forward (valid) slice registers valid/data and cuts only the forward path; a backward (ready) slice registers ready and cuts only the backward path; a full skid buffer registers BOTH and, thanks to its extra skid slot, accepts and offers in the same cycle so it cuts both paths at full throughput. Chaining N full skid slices pipelines the whole datapath: each slice adds one cycle of latency and cuts zero throughput, so the chain runs at one word per cycle. This taxonomy is identical in SystemVerilog, Verilog, and VHDL.

Composing N stages — the register-slice pipeline. A pipelined handshake is a chain of these slices, each obeying the valid/ready contract on both faces: the output valid/ready/data of slice k wires directly to the input valid/ready/data of slice k+1, with no glue. Because each slice is a self-contained registered stage, the long forward run is now broken into per-slice hops (each valid/data segment is one slice deep) and the long backward run is broken the same way (each ready segment reaches only the nearest ready flop). This is the classic cut the wire with a register move, applied to a handshake link instead of a bare bus — the difference being that the register must be a skid slice, not a plain flop.

The latency-vs-throughput accounting (8.2 revisited). Each proper slice inserts one pipeline register into the path, so a word takes one additional cycle to traverse it: a chain of N slices adds N cycles of latency. But the throughput is unchanged — each full skid slice accepts a new word and offers its held word in the same cycle (its RUNNING state), so the chain moves one word per cycle in steady state whenever both ends can sustain it. You are trading latency for frequency: the pipeline lets you clock faster (shorter register-to-register paths) at the cost of a longer fill/drain time, exactly the pipelining trade of 8.2, now on a flow-controlled link. A slice that is not a full skid buffer — a naive stage that bubbles — breaks this accounting by also costing throughput, which is the failure mode you must avoid.

Where you place slices to meet a target period. Slicing is a timing-driven placement problem. Find the longest valid/data combinational run and the longest ready combinational run in the chain; a slice must sit on each so that no register-to-register segment — forward or backward — exceeds the clock period. Because a full skid buffer cuts both directions at one point, one well-placed skid slice can break a long forward and a long backward run at the same spot; if the two long runs are in different places, you place slices at each. The goal is uniform: after slicing, every path between two flops fits in one period, and throughput is still one word per cycle.

4. Real RTL Implementation

This is the core of the page. We build two things — (a) a WIDTH-generic proper skid-buffer stage chained to depth STAGES (a full-throughput registered handshake slice, instantiated in a chain, with a self-checking clocked scoreboard testbench that applies random end backpressure and asserts in-order exactly-once delivery and one-word-per-cycle steady-state throughput), and (b) the naive-flop-slice dropped-word bug beside the proper-skid-slice fix — and each is shown in SystemVerilog, Verilog, and VHDL with an RTL block and a self-checking clocked testbench. The stage is the 9.3 skid buffer; the new content is the chaining and the bug that hides inside an incorrect slice.

One discipline runs through every RTL block, inherited from 9.1/9.3: each slice registers both interface signals (out_valid/out_data and in_ready = !skid_valid), so there is no combinational path from the far-downstream out_ready back to the far-upstream in_ready, nor from the far-upstream in_valid/in_data to the far-downstream out_valid/out_data; a word is accepted only on in_valid && in_ready, presented on out_valid, consumed on out_valid && out_ready; reset is synchronous, active-high.

4a. A skid slice chained to depth STAGES — three ways

The building block is one full-throughput skid slice (identical to 9.3's skid_buffer), and the pipeline is a generate chain of STAGES copies wired output-to-input. Because each slice registers both directions, the chain breaks the long forward and backward runs into per-slice hops while sustaining one word per cycle.

rv_slice.sv — one full-throughput registered handshake slice (skid buffer)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rv_slice #(
       parameter int WIDTH = 8                        // datapath width
   )(
       input  logic             clk,
       input  logic             rst,                  // synchronous, active-high
       input  logic             in_valid,
       output logic             in_ready,             // REGISTERED: = !skid_valid
       input  logic [WIDTH-1:0] in_data,
       output logic             out_valid,            // REGISTERED: primary occupancy
       input  logic             out_ready,
       output logic [WIDTH-1:0] out_data
   );
       logic [WIDTH-1:0] data_reg;                    // PRIMARY: presented downstream
       logic             valid_reg;                   // primary occupancy -> out_valid
       logic [WIDTH-1:0] skid_data;                   // SKID: catches the in-flight word
       logic             skid_valid;                  // skid occupancy -> !in_ready
 
       // Both interface signals are pure registered reads -> no comb path across the slice.
       assign out_valid = valid_reg;
       assign out_data  = data_reg;
       assign in_ready  = ~skid_valid;                // accept iff the skid slot is free
 
       wire accept  = in_valid  & in_ready;           // a word is taken this cycle
       wire consume = valid_reg & out_ready;          // the primary word leaves this cycle
 
       always_ff @(posedge clk) begin
           if (rst) begin
               valid_reg  <= 1'b0; skid_valid <= 1'b0;
               data_reg   <= '0;   skid_data  <= '0;
           end else if (skid_valid) begin
               // STALLED-FULL: drain skid into primary when the downstream takes primary.
               if (out_ready) begin data_reg <= skid_data; skid_valid <= 1'b0; end
           end else begin
               if      (consume && !accept)                 valid_reg <= 1'b0;     // primary empties
               else if (accept && (consume || !valid_reg)) begin
                   data_reg  <= in_data; valid_reg <= 1'b1;                        // accept+offer / first fill
               end else if (accept && valid_reg && !consume) begin
                   skid_data <= in_data; skid_valid <= 1'b1;                       // land in-flight -> STALLED-FULL
               end
           end
       end
   endmodule

The pipeline chains STAGES slices with a generate loop: the output handshake of slice k feeds the input handshake of slice k+1. The whole chain presents one valid/ready contract at each end, so it drops into a datapath exactly where a bare link used to be.

rv_pipeline.sv — chain STAGES skid slices into a pipelined handshake
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rv_pipeline #(
       parameter int WIDTH  = 8,
       parameter int STAGES = 3                        // number of registered handshake slices
   )(
       input  logic             clk, rst,
       input  logic             in_valid,
       output logic             in_ready,
       input  logic [WIDTH-1:0] in_data,
       output logic             out_valid,
       input  logic             out_ready,
       output logic [WIDTH-1:0] out_data
   );
       // Inter-slice handshake wires: index s is the link BETWEEN slice s-1 and slice s.
       logic             v [STAGES+1];
       logic             r [STAGES+1];
       logic [WIDTH-1:0] d [STAGES+1];
 
       // The chain ends: link 0 is the pipeline input, link STAGES is the output.
       assign v[0] = in_valid;  assign d[0] = in_data;  assign in_ready = r[0];
       assign out_valid = v[STAGES]; assign out_data = d[STAGES]; assign r[STAGES] = out_ready;
 
       genvar s;
       generate
           for (s = 0; s < STAGES; s++) begin : g_slice
               rv_slice #(.WIDTH(WIDTH)) u_slice (
                   .clk(clk), .rst(rst),
                   .in_valid (v[s]),   .in_ready (r[s]),   .in_data (d[s]),     // upstream face
                   .out_valid(v[s+1]), .out_ready(r[s+1]), .out_data(d[s+1]));  // downstream face
           end
       endgenerate
   endmodule

The clocked scoreboard testbench instantiates the depth-STAGES chain, streams a numbered sequence in, randomly de-asserts out_ready at the far end (including on transfer cycles), and asserts every accepted word is delivered in order, exactly once. It also counts transfers over a both-ends-ready window to check the steady-state rate is one word per cycle (a chain of proper slices holds this; a chain of bubble-slices would fail it).

rv_pipeline_tb.sv — scoreboard: random end backpressure, in-order exactly-once, 1/cycle throughput
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rv_pipeline_tb;
       localparam int WIDTH  = 8;
       localparam int STAGES = 3;
       localparam int NWORDS = 300;
       logic clk = 1'b0, rst;
       logic in_valid, in_ready;   logic [WIDTH-1:0] in_data;
       logic out_valid, out_ready; logic [WIDTH-1:0] out_data;
       int   errors = 0;
 
       rv_pipeline #(.WIDTH(WIDTH), .STAGES(STAGES)) dut (
           .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));
 
       always #5 clk = ~clk;
 
       logic [WIDTH-1:0] sb [$];                       // expected words, in acceptance order
       int   sent = 0, got = 0;
       logic [WIDTH-1:0] next_word = 8'h00;
 
       // Producer: offer a fresh numbered word until NWORDS have been accepted.
       always @(negedge clk) begin
           if (rst) in_valid <= 1'b0;
           else begin in_valid <= (sent < NWORDS); in_data <= next_word; end
       end
       always @(posedge clk) if (!rst && in_valid && in_ready) begin
           sb.push_back(in_data); sent++; next_word <= next_word + 8'd1;
       end
 
       // Consumer: random stalls at the far end, INCLUDING on transfer cycles.
       always @(negedge clk) out_ready <= ($urandom_range(0, 3) != 0);   // ~75% ready
 
       // Every delivered word: in order, exactly once, correct value.
       always @(posedge clk) if (!rst && out_valid && out_ready) begin
           if (sb.size() == 0) begin $error("delivered a never-accepted word: %h", out_data); errors++; end
           else begin
               automatic logic [WIDTH-1:0] exp = sb.pop_front();
               if (out_data !== exp) begin $error("ORDER/VALUE got %h exp %h", out_data, exp); errors++; end
               got++;
           end
       end
 
       // Throughput: over a window where BOTH ends are ready, one word/cycle out of the chain.
       int both_xfers = 0, both_cycles = 0;
       always @(posedge clk) if (!rst) begin
           if (out_valid && out_ready) both_xfers++;
           if (in_valid  && out_ready) both_cycles++;
       end
 
       initial begin
           rst = 1'b1; in_valid = 1'b0; out_ready = 1'b0; in_data = '0;
           repeat (2) @(posedge clk); #1; rst = 1'b0;
 
           wait (got == NWORDS);
           repeat (STAGES + 4) @(posedge clk);
 
           if (sb.size() != 0) begin $error("%0d accepted words never delivered", sb.size()); errors++; end
           if (got != NWORDS)  begin $error("delivered %0d of %0d words", got, NWORDS); errors++; end
 
           if (errors == 0)
               $display("PASS: %0d words through %0d slices in-order exactly-once; ~1 word/cycle when both ready", NWORDS, STAGES);
           else
               $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

The Verilog form is the same slice and the same generate chain with reg/wire typing; the scoreboard uses a ring buffer with head/tail indices and $random in place of $urandom.

rv_slice.v — one full-throughput registered handshake slice in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rv_slice #(
       parameter WIDTH = 8
   )(
       input  wire             clk,
       input  wire             rst,
       input  wire             in_valid,
       output wire             in_ready,
       input  wire [WIDTH-1:0] in_data,
       output wire             out_valid,
       input  wire             out_ready,
       output wire [WIDTH-1:0] out_data
   );
       reg  [WIDTH-1:0] data_reg;                     // PRIMARY
       reg              valid_reg;                    // primary occupancy
       reg  [WIDTH-1:0] skid_data;                    // SKID
       reg              skid_valid;                   // skid occupancy
 
       assign out_valid = valid_reg;
       assign out_data  = data_reg;
       assign in_ready  = ~skid_valid;                // registered accept: skid free
 
       wire accept  = in_valid  & in_ready;
       wire consume = valid_reg & out_ready;
 
       always @(posedge clk) begin
           if (rst) begin
               valid_reg <= 1'b0; skid_valid <= 1'b0;
               data_reg  <= {WIDTH{1'b0}}; skid_data <= {WIDTH{1'b0}};
           end else if (skid_valid) begin
               if (out_ready) begin data_reg <= skid_data; skid_valid <= 1'b0; end
           end else begin
               if      (consume & ~accept)                 valid_reg <= 1'b0;
               else if (accept & (consume | ~valid_reg)) begin data_reg  <= in_data; valid_reg <= 1'b1; end
               else if (accept & valid_reg & ~consume)   begin skid_data <= in_data; skid_valid <= 1'b1; end
           end
       end
   endmodule
rv_pipeline.v — chain STAGES slices in Verilog (generate)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rv_pipeline #(
       parameter WIDTH  = 8,
       parameter STAGES = 3
   )(
       input  wire             clk, rst,
       input  wire             in_valid,
       output wire             in_ready,
       input  wire [WIDTH-1:0] in_data,
       output wire             out_valid,
       input  wire             out_ready,
       output wire [WIDTH-1:0] out_data
   );
       // Inter-slice buses: index s is the link between slice s-1 and slice s.
       wire             v [0:STAGES];
       wire             r [0:STAGES];
       wire [WIDTH-1:0] d [0:STAGES];
 
       assign v[0] = in_valid;  assign d[0] = in_data;  assign in_ready = r[0];
       assign out_valid = v[STAGES]; assign out_data = d[STAGES]; assign r[STAGES] = out_ready;
 
       genvar s;
       generate
           for (s = 0; s < STAGES; s = s + 1) begin : g_slice
               rv_slice #(.WIDTH(WIDTH)) u_slice (
                   .clk(clk), .rst(rst),
                   .in_valid (v[s]),   .in_ready (r[s]),   .in_data (d[s]),
                   .out_valid(v[s+1]), .out_ready(r[s+1]), .out_data(d[s+1]));
           end
       endgenerate
   endmodule
rv_pipeline_tb.v — self-checking scoreboard; random end backpressure; PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rv_pipeline_tb;
       parameter WIDTH  = 8;
       parameter STAGES = 3;
       parameter NWORDS = 300;
       reg  clk, rst, in_valid, out_ready;
       reg  [WIDTH-1:0] in_data;
       wire in_ready, out_valid;
       wire [WIDTH-1:0] out_data;
       integer errors, sent, got;
       reg  [WIDTH-1:0] next_word;
 
       reg  [WIDTH-1:0] sb [0:2047];                  // scoreboard ring
       integer head, tail;
 
       rv_pipeline #(.WIDTH(WIDTH), .STAGES(STAGES)) dut (
           .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));
 
       always #5 clk = ~clk;
 
       always @(negedge clk) begin
           if (rst) in_valid <= 1'b0;
           else begin in_valid <= (sent < NWORDS); in_data <= next_word; end
       end
       always @(posedge clk) if (!rst && in_valid && in_ready) begin
           sb[tail] <= in_data; tail <= tail + 1; sent <= sent + 1;
           next_word <= next_word + 8'd1;
       end
 
       always @(negedge clk) out_ready <= ($random % 4 != 0);
 
       always @(posedge clk) if (!rst && out_valid && out_ready) begin
           if (head == tail) begin $display("FAIL: delivered never-accepted word: %h", out_data); errors = errors + 1; end
           else begin
               if (out_data !== sb[head]) begin
                   $display("FAIL: ORDER/VALUE got %h exp %h", out_data, sb[head]); errors = errors + 1;
               end
               head <= head + 1; got <= got + 1;
           end
       end
 
       initial begin
           errors = 0; sent = 0; got = 0; head = 0; tail = 0; next_word = 8'h00;
           clk = 1'b0; rst = 1'b1; in_valid = 1'b0; out_ready = 1'b0; in_data = 8'h00;
           repeat (2) @(posedge clk); #1; rst = 1'b0;
 
           wait (got == NWORDS);
           repeat (STAGES + 4) @(posedge clk);
 
           if (head != tail)  begin $display("FAIL: accepted words never delivered"); errors = errors + 1; end
           if (got != NWORDS) begin $display("FAIL: delivered %0d of %0d", got, NWORDS); errors = errors + 1; end
           if (errors == 0) $display("PASS: %0d words through %0d slices in-order exactly-once", NWORDS, STAGES);
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

In VHDL the slice is the 9.3 skid buffer, and the chain is a for … generate that wires an array of inter-slice handshake signals. The interface signals are concurrent assignments from the registered occupancy bits; the controller is one clocked process.

rv_slice.vhd — one full-throughput registered handshake slice in VHDL (numeric_std)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity rv_slice is
       generic ( WIDTH : positive := 8 );
       port (
           clk       : in  std_logic;
           rst       : in  std_logic;                 -- synchronous, active-high
           in_valid  : in  std_logic;
           in_ready  : out std_logic;                 -- registered: = not skid_valid
           in_data   : in  std_logic_vector(WIDTH-1 downto 0);
           out_valid : out std_logic;                 -- registered: primary occupancy
           out_ready : in  std_logic;
           out_data  : out std_logic_vector(WIDTH-1 downto 0)
       );
   end entity;
 
   architecture rtl of rv_slice is
       signal data_reg   : std_logic_vector(WIDTH-1 downto 0) := (others => '0');  -- PRIMARY
       signal valid_reg  : std_logic := '0';                                       -- primary occ
       signal skid_data  : std_logic_vector(WIDTH-1 downto 0) := (others => '0');  -- SKID
       signal skid_valid : std_logic := '0';                                       -- skid occ
       signal accept_i   : std_logic;
       signal consume_i  : std_logic;
   begin
       out_valid <= valid_reg;
       out_data  <= data_reg;
       in_ready  <= not skid_valid;                   -- accept iff skid slot free
       accept_i  <= in_valid  and (not skid_valid);
       consume_i <= valid_reg and out_ready;
 
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   valid_reg <= '0'; skid_valid <= '0';
                   data_reg  <= (others => '0'); skid_data <= (others => '0');
               elsif skid_valid = '1' then
                   if out_ready = '1' then data_reg <= skid_data; skid_valid <= '0'; end if;
               else
                   if consume_i = '1' and accept_i = '0' then
                       valid_reg <= '0';
                   elsif accept_i = '1' and (consume_i = '1' or valid_reg = '0') then
                       data_reg  <= in_data; valid_reg <= '1';
                   elsif accept_i = '1' and valid_reg = '1' and consume_i = '0' then
                       skid_data <= in_data; skid_valid <= '1';
                   end if;
               end if;
           end if;
       end process;
   end architecture;
rv_pipeline.vhd — chain STAGES slices in VHDL (for-generate over a link array)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity rv_pipeline is
       generic ( WIDTH : positive := 8; STAGES : positive := 3 );
       port (
           clk, rst  : in  std_logic;
           in_valid  : in  std_logic;
           in_ready  : out std_logic;
           in_data   : in  std_logic_vector(WIDTH-1 downto 0);
           out_valid : out std_logic;
           out_ready : in  std_logic;
           out_data  : out std_logic_vector(WIDTH-1 downto 0)
       );
   end entity;
 
   architecture rtl of rv_pipeline is
       type slv_arr is array (natural range <>) of std_logic_vector(WIDTH-1 downto 0);
       signal v : std_logic_vector(0 to STAGES);      -- link s is between slice s-1 and s
       signal r : std_logic_vector(0 to STAGES);
       signal d : slv_arr(0 to STAGES);
   begin
       v(0) <= in_valid;  d(0) <= in_data;  in_ready <= r(0);
       out_valid <= v(STAGES); out_data <= d(STAGES); r(STAGES) <= out_ready;
 
       g_slice : for s in 0 to STAGES-1 generate
           u_slice : entity work.rv_slice
               generic map (WIDTH => WIDTH)
               port map (clk => clk, rst => rst,
                         in_valid => v(s),   in_ready => r(s),   in_data => d(s),
                         out_valid => v(s+1), out_ready => r(s+1), out_data => d(s+1));
       end generate;
   end architecture;
rv_pipeline_tb.vhd — self-checking scoreboard; random end backpressure; assert severity
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity rv_pipeline_tb is
   end entity;
 
   architecture sim of rv_pipeline_tb is
       constant WIDTH  : positive := 8;
       constant STAGES : positive := 3;
       constant NWORDS : integer  := 300;
       signal clk : std_logic := '0';
       signal rst, in_valid, in_ready, out_valid, out_ready : std_logic := '0';
       signal in_data, out_data : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
 
       type   sb_t is array (0 to 2047) of std_logic_vector(WIDTH-1 downto 0);
       signal sb : sb_t;
       signal head, tail, sent, got : integer := 0;
       signal next_word : unsigned(WIDTH-1 downto 0) := (others => '0');
       signal lfsr : unsigned(7 downto 0) := x"3B";
   begin
       dut : entity work.rv_pipeline
           generic map (WIDTH => WIDTH, STAGES => STAGES)
           port map (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);
 
       clkgen : process begin clk <= '0'; wait for 5 ns; clk <= '1'; wait for 5 ns; end process;
 
       drive : process (clk)
       begin
           if falling_edge(clk) then
               if rst = '1' then in_valid <= '0'; out_ready <= '0';
               else
                   if sent < NWORDS then in_valid <= '1'; else in_valid <= '0'; end if;
                   in_data <= std_logic_vector(next_word);
                   lfsr    <= lfsr(6 downto 0) & (lfsr(7) xor lfsr(5) xor lfsr(4) xor lfsr(3));
                   if lfsr(1 downto 0) /= "00" then out_ready <= '1'; else out_ready <= '0'; end if;
               end if;
           end if;
       end process;
 
       check : process (clk)
       begin
           if rising_edge(clk) then
               if rst = '0' and in_valid = '1' and in_ready = '1' then
                   sb(tail)  <= in_data; tail <= tail + 1; sent <= sent + 1;
                   next_word <= next_word + 1;
               end if;
               if rst = '0' and out_valid = '1' and out_ready = '1' then
                   assert head /= tail report "delivered a word that was never accepted" severity error;
                   assert out_data = sb(head) report "ORDER/VALUE mismatch on delivered word" severity error;
                   head <= head + 1; got <= got + 1;
               end if;
           end if;
       end process;
 
       stim : process
       begin
           rst <= '1'; wait until rising_edge(clk); wait until rising_edge(clk);
           wait for 1 ns; rst <= '0';
           wait until got = NWORDS;
           assert head = tail report "accepted words never delivered" severity error;
           report "rv_pipeline self-check complete: in-order exactly-once through the slice chain" severity note;
           wait;
       end process;
   end architecture;

4b. The naive-flop slice vs the proper-skid slice — buggy vs fixed, in all three HDLs

Pattern (b) is the signature failure of pipelining a handshake: the engineer pipelines a valid/ready link by putting a plain register on valid and data and wiring ready straight through combinationally. It looks pipelined and passes whenever the sink never stalls, but the instant out_ready falls with a word registered in the slice, the upstream — seeing the still-combinational ready — advances a new word over the held one, and a word is lost. It is the 9.3 dropped-word failure, re-introduced by an incorrect pipeline slice. The fix is to replace the naive flop with a proper skid slice that registers both directions.

rv_slice_bug.sv — BUGGY naive-flop slice (word lost) vs FIXED proper skid slice
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: pipelines the link with a PLAIN flop on valid/data and wires ready straight
   //        through. Registers the FORWARD direction only; ready stays combinational and
   //        there is no skid slot -> when out_ready falls on a cycle a word was accepted,
   //        the new word OVERWRITES the held one and a word is LOST.
   module rv_slice_bad #(parameter int WIDTH = 8) (
       input  logic clk, rst,
       input  logic in_valid, output logic in_ready, input logic [WIDTH-1:0] in_data,
       output logic out_valid, input logic out_ready, output logic [WIDTH-1:0] out_data
   );
       logic [WIDTH-1:0] data_reg;
       logic             valid_reg;
       assign out_valid = valid_reg;
       assign out_data  = data_reg;
       assign in_ready  = out_ready;                  // COMBINATIONAL ready, plain forward flop
       always_ff @(posedge clk) begin
           if (rst) begin valid_reg <= 1'b0; data_reg <= '0; end
           else begin
               if (in_valid && in_ready) begin
                   data_reg  <= in_data;              // advances a new word OVER the held one
                   valid_reg <= 1'b1;
               end else if (valid_reg && out_ready) begin
                   valid_reg <= 1'b0;
               end
           end
       end
   endmodule
 
   // FIXED: a proper skid slice. in_ready = !skid_valid (registered), and the in-flight
   //        word lands in the skid slot instead of overwriting the held word. Registers
   //        BOTH directions -> a real pipelined handshake stage, full throughput.
   module rv_slice_good #(parameter int WIDTH = 8) (
       input  logic clk, rst,
       input  logic in_valid, output logic in_ready, input logic [WIDTH-1:0] in_data,
       output logic out_valid, input logic out_ready, output logic [WIDTH-1:0] out_data
   );
       logic [WIDTH-1:0] data_reg, skid_data;
       logic             valid_reg, skid_valid;
       assign out_valid = valid_reg;
       assign out_data  = data_reg;
       assign in_ready  = ~skid_valid;                // REGISTERED: accept iff skid free
       wire   accept  = in_valid  & in_ready;
       wire   consume = valid_reg & out_ready;
       always_ff @(posedge clk) begin
           if (rst) begin valid_reg <= 1'b0; skid_valid <= 1'b0; data_reg <= '0; skid_data <= '0; end
           else if (skid_valid) begin
               if (out_ready) begin data_reg <= skid_data; skid_valid <= 1'b0; end
           end else begin
               if      (consume && !accept)                 valid_reg <= 1'b0;
               else if (accept && (consume || !valid_reg)) begin data_reg  <= in_data; valid_reg <= 1'b1; end
               else if (accept && valid_reg && !consume)   begin skid_data <= in_data; skid_valid <= 1'b1; end
           end
       end
   endmodule
rv_slice_bug_tb.sv — stall on the transfer cycle; the naive-flop slice loses a word
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rv_slice_bug_tb;
       localparam int WIDTH = 8;
       logic clk = 1'b0, rst;
       logic in_valid, in_ready;   logic [WIDTH-1:0] in_data;
       logic out_valid, out_ready; logic [WIDTH-1:0] out_data;
       int   errors = 0;
       logic [WIDTH-1:0] sb [$];
 
       // Bind to rv_slice_good to PASS. rv_slice_bad drops the word accepted on the stall edge.
       rv_slice_good #(.WIDTH(WIDTH)) dut (
           .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));
 
       always #5 clk = ~clk;
 
       always @(posedge clk) if (!rst && in_valid && in_ready) sb.push_back(in_data);
       always @(posedge clk) if (!rst && out_valid && out_ready) begin
           if (sb.size() == 0) begin $error("delivered never-accepted word"); errors++; end
           else begin
               automatic logic [WIDTH-1:0] e = sb.pop_front();
               if (out_data !== e) begin $error("lost/reordered: got %h exp %h", out_data, e); errors++; end
           end
       end
 
       initial begin
           rst = 1'b1; in_valid = 0; out_ready = 0; in_data = '0;
           repeat (2) @(posedge clk); #1; rst = 1'b0;
 
           // Force the hazard: hold in_valid high, de-assert out_ready on the accept cycle.
           for (int i = 0; i < 8; i++) begin
               @(negedge clk); in_valid = 1'b1; in_data = 8'h50 + i[7:0];
               out_ready = (i % 2 == 0) ? 1'b0 : 1'b1;    // stall on the transfer boundary
               @(posedge clk); #1;
           end
           in_valid = 1'b0; out_ready = 1'b1;
           repeat (8) @(posedge clk);
 
           if (sb.size() != 0) begin $error("%0d accepted words never delivered (dropped)", sb.size()); errors++; end
           if (errors == 0) $display("PASS: no word lost through the slice across the stall boundary");
           else             $display("FAIL: %0d errors (naive-flop slice dropped an in-flight word)", errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair tells the same story with reg/wire typing; the whole difference is the presence of the skid register and whether in_ready is registered (!skid_valid) or combinational (out_ready).

rv_slice_bug.v — BUGGY naive-flop slice vs FIXED proper skid slice in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: plain forward flop, combinational ready, no skid slot -> in-flight word lost.
   module rv_slice_bad #(parameter WIDTH = 8) (
       input wire clk, rst,
       input wire in_valid, output wire in_ready, input wire [WIDTH-1:0] in_data,
       output wire out_valid, input wire out_ready, output wire [WIDTH-1:0] out_data
   );
       reg [WIDTH-1:0] data_reg;
       reg             valid_reg;
       assign out_valid = valid_reg;
       assign out_data  = data_reg;
       assign in_ready  = out_ready;                  // COMBINATIONAL ready, single slot
       always @(posedge clk) begin
           if (rst) begin valid_reg <= 1'b0; data_reg <= {WIDTH{1'b0}}; end
           else begin
               if (in_valid && in_ready) begin
                   data_reg  <= in_data;              // overwrites the held, unconsumed word
                   valid_reg <= 1'b1;
               end else if (valid_reg && out_ready) begin
                   valid_reg <= 1'b0;
               end
           end
       end
   endmodule
 
   // FIXED: proper skid slice; in_ready = !skid_valid (registered), in-flight word lands in skid.
   module rv_slice_good #(parameter WIDTH = 8) (
       input wire clk, rst,
       input wire in_valid, output wire in_ready, input wire [WIDTH-1:0] in_data,
       output wire out_valid, input wire out_ready, output wire [WIDTH-1:0] out_data
   );
       reg [WIDTH-1:0] data_reg, skid_data;
       reg             valid_reg, skid_valid;
       assign out_valid = valid_reg;
       assign out_data  = data_reg;
       assign in_ready  = ~skid_valid;                // registered accept: skid free
       wire accept  = in_valid  & in_ready;
       wire consume = valid_reg & out_ready;
       always @(posedge clk) begin
           if (rst) begin valid_reg <= 1'b0; skid_valid <= 1'b0; data_reg <= {WIDTH{1'b0}}; skid_data <= {WIDTH{1'b0}}; end
           else if (skid_valid) begin
               if (out_ready) begin data_reg <= skid_data; skid_valid <= 1'b0; end
           end else begin
               if      (consume & ~accept)                 valid_reg <= 1'b0;
               else if (accept & (consume | ~valid_reg)) begin data_reg  <= in_data; valid_reg <= 1'b1; end
               else if (accept & valid_reg & ~consume)   begin skid_data <= in_data; skid_valid <= 1'b1; end
           end
       end
   endmodule
rv_slice_bug_tb.v — self-checking; stall on the transfer cycle; require no dropped word
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rv_slice_bug_tb;
       parameter WIDTH = 8;
       reg  clk, rst, in_valid, out_ready;
       reg  [WIDTH-1:0] in_data;
       wire in_ready, out_valid;
       wire [WIDTH-1:0] out_data;
       integer errors, i;
       reg  [WIDTH-1:0] sb [0:63];
       integer head, tail;
 
       // Bind to rv_slice_good to PASS; rv_slice_bad drops the in-flight word.
       rv_slice_good #(.WIDTH(WIDTH)) dut (
           .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));
 
       always #5 clk = ~clk;
 
       always @(posedge clk) if (!rst && in_valid && in_ready) begin
           sb[tail] <= in_data; tail <= tail + 1;
       end
       always @(posedge clk) if (!rst && out_valid && out_ready) begin
           if (head == tail) begin $display("FAIL: delivered never-accepted word"); errors = errors + 1; end
           else begin
               if (out_data !== sb[head]) begin
                   $display("FAIL: lost/reordered got %h exp %h", out_data, sb[head]); errors = errors + 1;
               end
               head <= head + 1;
           end
       end
 
       initial begin
           errors = 0; head = 0; tail = 0;
           clk = 1'b0; rst = 1'b1; in_valid = 0; out_ready = 0; in_data = 8'h00;
           repeat (2) @(posedge clk); #1; rst = 1'b0;
 
           for (i = 0; i < 8; i = i + 1) begin
               @(negedge clk); in_valid = 1'b1; in_data = 8'h50 + i;
               out_ready = (i % 2 == 0) ? 1'b0 : 1'b1;    // stall on the transfer boundary
               @(posedge clk); #1;
           end
           in_valid = 1'b0; out_ready = 1'b1;
           repeat (8) @(posedge clk);
 
           if (head != tail) begin $display("FAIL: accepted words never delivered (dropped)"); errors = errors + 1; end
           if (errors == 0) $display("PASS: no word lost through the slice across the stall boundary");
           else             $display("FAIL: %0d errors (naive-flop slice dropped an in-flight word)", errors);
           $finish;
       end
   endmodule

In VHDL the same bug appears when the slice has a single data_reg and drives in_ready <= out_ready; the fix adds skid_data/skid_valid and drives in_ready <= not skid_valid.

rv_slice_bug.vhd — BUGGY naive-flop slice vs FIXED proper skid slice in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: plain forward flop, combinational ready, single slot -> in-flight word lost.
   entity rv_slice_bad is
       generic ( WIDTH : positive := 8 );
       port ( clk, rst, in_valid : in std_logic; in_ready : out std_logic;
              in_data : in std_logic_vector(WIDTH-1 downto 0);
              out_valid : out std_logic; out_ready : in std_logic;
              out_data : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of rv_slice_bad is
       signal data_reg  : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal valid_reg : std_logic := '0';
   begin
       out_valid <= valid_reg;
       out_data  <= data_reg;
       in_ready  <= out_ready;                        -- COMBINATIONAL ready, single slot
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then valid_reg <= '0'; data_reg <= (others => '0');
               else
                   if in_valid = '1' and out_ready = '1' then
                       data_reg  <= in_data;          -- overwrites the held, unconsumed word
                       valid_reg <= '1';
                   elsif valid_reg = '1' and out_ready = '1' then
                       valid_reg <= '0';
                   end if;
               end if;
           end if;
       end process;
   end architecture;
 
   -- FIXED: proper skid slice; in_ready = not skid_valid (registered).
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   entity rv_slice_good is
       generic ( WIDTH : positive := 8 );
       port ( clk, rst, in_valid : in std_logic; in_ready : out std_logic;
              in_data : in std_logic_vector(WIDTH-1 downto 0);
              out_valid : out std_logic; out_ready : in std_logic;
              out_data : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of rv_slice_good is
       signal data_reg, skid_data : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal valid_reg, skid_valid : std_logic := '0';
       signal accept_i, consume_i : std_logic;
   begin
       out_valid <= valid_reg;
       out_data  <= data_reg;
       in_ready  <= not skid_valid;                   -- registered accept: skid free
       accept_i  <= in_valid  and (not skid_valid);
       consume_i <= valid_reg and out_ready;
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then
                   valid_reg <= '0'; skid_valid <= '0';
                   data_reg <= (others => '0'); skid_data <= (others => '0');
               elsif skid_valid = '1' then
                   if out_ready = '1' then data_reg <= skid_data; skid_valid <= '0'; end if;
               else
                   if consume_i = '1' and accept_i = '0' then
                       valid_reg <= '0';
                   elsif accept_i = '1' and (consume_i = '1' or valid_reg = '0') then
                       data_reg  <= in_data; valid_reg <= '1';
                   elsif accept_i = '1' and valid_reg = '1' and consume_i = '0' then
                       skid_data <= in_data; skid_valid <= '1';
                   end if;
               end if;
           end if;
       end process;
   end architecture;
rv_slice_bug_tb.vhd — self-checking; stall on transfer cycle; require no dropped word
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity rv_slice_bug_tb is
   end entity;
 
   architecture sim of rv_slice_bug_tb is
       constant WIDTH : positive := 8;
       signal clk : std_logic := '0';
       signal rst, in_valid, in_ready, out_valid, out_ready : std_logic := '0';
       signal in_data, out_data : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       type   sb_t is array (0 to 63) of std_logic_vector(WIDTH-1 downto 0);
       signal sb : sb_t;
       signal head, tail : integer := 0;
   begin
       -- Bind to rv_slice_good to PASS; rv_slice_bad drops the in-flight word.
       dut : entity work.rv_slice_good
           generic map (WIDTH => WIDTH)
           port map (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);
 
       clkgen : process begin clk <= '0'; wait for 5 ns; clk <= '1'; wait for 5 ns; end process;
 
       check : process (clk)
       begin
           if rising_edge(clk) then
               if rst = '0' and in_valid = '1' and in_ready = '1' then
                   sb(tail) <= in_data; tail <= tail + 1;
               end if;
               if rst = '0' and out_valid = '1' and out_ready = '1' then
                   assert head /= tail report "delivered a never-accepted word" severity error;
                   assert out_data = sb(head) report "lost/reordered word" severity error;
                   head <= head + 1;
               end if;
           end if;
       end process;
 
       stim : process
       begin
           rst <= '1'; wait until rising_edge(clk); wait until rising_edge(clk);
           wait for 1 ns; rst <= '0';
           for i in 0 to 7 loop
               wait until falling_edge(clk);
               in_valid <= '1'; in_data <= std_logic_vector(to_unsigned(16#50# + i, WIDTH));
               if (i mod 2) = 0 then out_ready <= '0'; else out_ready <= '1'; end if;  -- stall on boundary
               wait until rising_edge(clk); wait for 1 ns;
           end loop;
           in_valid <= '0'; out_ready <= '1';
           for i in 0 to 7 loop wait until rising_edge(clk); end loop;
           assert head = tail report "accepted words never delivered (dropped)" severity error;
           report "rv_slice_bug self-check complete: no word lost across the stall boundary" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: pipelining a handshake means inserting slices that preserve the contract — a plain flop on valid/data with ready wired through is not a handshake stage; use a full skid slice that registers both directions so no word is dropped and throughput stays one word per cycle.

5. Verification Strategy

A pipelined handshake's correctness is a data-integrity-and-rate-under-backpressure property along a chain of stages, so verification connects a producer and consumer through the full depth-N chain and proves that every accepted word is delivered in order, exactly once, under arbitrary end backpressure; that steady-state throughput is one word per cycle when both ends are ready; and that no combinational path runs from the far-upstream valid/data to the far-downstream, nor from the far-downstream out_ready to the far-upstream in_ready — each slice registers its cut.

Every word accepted on in_valid && in_ready at the head of the chain is delivered on out_valid && out_ready at the tail exactly once and in FIFO order under ALL backpressure patterns; steady-state throughput is one word per cycle when both ends are ready; and every slice registers the direction it cuts, so there is no combinational path threading the whole chain in either direction.

Everything below makes that invariant checkable and maps onto the testbenches in §4.

  • Build a chain of at least three slices and scoreboard it. Instantiate STAGES >= 3 proper skid slices (the §4a rv_pipeline), push every word accepted at the head onto a reference queue, and pop-and-compare on every delivery at the tail. This proves in-order, exactly-once delivery across the whole chain, not just one stage — reordering or duplication introduced by any slice, or by the way slices compose, shows up here.
  • Randomized end backpressure, including on transfer cycles. Drive out_ready at the tail from a random source that de-asserts at every phase, explicitly including the cycle a word transfers — that stall boundary, propagated back through the chain one slice at a time, is where a dropped-word slice fails. The §4a scoreboards do exactly this; a chain of naive-flop slices would drop words at every internal stall boundary.
  • Full 1/cycle throughput when both ends are ready. Hold in_valid and out_ready continuously high and count deliveries over the window; after the fixed fill latency of STAGES cycles, deliveries must equal the cycle count. A chain of proper slices holds this (each accepts-and-offers in the same cycle); a chain where any slice is a half-throughput register slice delivers a word only every other cycle and fails — this is the check that separates a real pipelined handshake from a bubbling one.
  • Assert no combinational path across the whole chain (each slice registers its cut). The whole point is that in_ready at the head is a registered function that reaches back only one slice, and out_valid/out_data at the tail come out of flops — neither may wiggle combinationally in response to a signal at the far end within one cycle. Verify structurally (each slice drives its interface from flops) and in simulation by confirming the interface signals change only at clock edges. A testbench that finds in_ready at the head responding to out_ready at the tail in the same cycle has caught a chain that did not actually cut the path.
  • Compare a proper-slice chain against a half-throughput-slice chain. Run the throughput check on the §4a chain (full rate) and on a chain of single-register bubble slices (half rate) and confirm the accounting: same data-correctness, different bandwidth. This makes concrete why the slice flavor matters even when both are data-safe — the naive stage silently halves throughput.
  • Corner cases along the chain. A long tail stall (backpressure propagates upstream slice by slice until the head's in_ready falls); the head idle mid-stream (in_valid low — the chain drains what it holds); reset mid-flight (every slice clears, all out_valid/skid_valid return to zero, in_ready returns high); and out_ready toggling every cycle at the tail, the harshest ordering test as each slice repeatedly crosses its RUNNING to STALLED-FULL boundary. The expected waveform on a correct run: with both ends ready, a word appears at the tail STAGES cycles after it entered the head (the fill latency) and one word transfers per cycle thereafter; when the tail stalls, backpressure walks upstream one slice per cycle and no word in the chain is lost. The visual signature of a naive-flop slice is a delivered stream missing exactly the words caught in flight at an internal stall boundary.

6. Common Mistakes

Inserting a naive flop between handshake stages. The signature failure. To pipeline a link, the engineer drops a plain register on valid and data and wires ready straight through — registering only the forward direction with no skid slot. Because ready stays combinational and there is no landing pad, when out_ready falls on a cycle a word was accepted, the new word overwrites the held one and is dropped: the 9.3 dropped-word failure, now embedded in a pipeline slice. It passes whenever the sink never stalls mid-stream and corrupts exactly at the stall boundary. The fix is a proper skid slice that registers both directions and holds the in-flight word. (Post-mortem in §7.)

A half-throughput register slice where full throughput was required. A data-safe but over-conservative slice — a single register that accepts only when empty (in_ready = !valid_reg) — never drops a word but cannot accept and offer in the same cycle, so it inserts a bubble on every transfer and runs at half rate. Chain several of these and the whole pipeline runs at 0.5 words/cycle where the budget assumed 1.0. It is a fine choice only when the link can tolerate half rate; reaching for it when you needed full throughput silently halves your bandwidth. Use the full skid slice, whose RUNNING state accepts-and-offers in one cycle, wherever bandwidth matters.

Not actually breaking the ready chain. A datapath that looks pipelined — registers on the data path between stages — but whose ready still runs combinationally across all stages. You registered the forward path and left the backward path exactly as long as before, so the long combinational ready from 9.2 is untouched and the pipeline still fails timing on the backward path. Registering the data without registering the ready is doing half the work and keeping the timing bug. A pipeline slice must break the direction that is actually long; on a deep pipe that is usually both, which is why the full skid buffer (registers both) is the default slice.

Mismatched latency between parallel pipelined paths. When two ready/valid paths run in parallel and must recombine downstream (a datapath split then merged, or a control path that must stay aligned with data), inserting a different number of slices on each path shifts them by different amounts of latency, so words that were aligned at the split arrive misaligned at the merge. Each slice adds exactly one cycle of latency; if one branch gets two slices and the other gets three, the branches are one cycle out of step at the join and the merge samples mismatched words. Balance the slice count on parallel paths, or re-align explicitly at the merge — pipelining a handshake changes latency, and latency across parallel paths must stay matched.

Placing slices where the path is not actually long. Slicing is timing-driven: a slice pays one cycle of latency and one register per bit for the privilege of cutting a path. Inserting slices where no register-to-register segment exceeds the period buys nothing but latency and area; the useful slice sits on the longest valid/data run and the longest ready run so that after slicing no segment exceeds the clock period. Slice from timing reports, not by reflex — an extra slice that does not shorten the critical path is pure latency you gave away for free.

7. DebugLab

The pipeline stage that eats a word — a naive flop pretending to be a handshake slice

The engineering lesson: pipelining a handshake is not the same as pipelining a bus — a plain flop cuts a bare wire fine, but on a valid/ready link it either drops the word in flight (forward flop, combinational ready) or bubbles the stream (a stage that cannot accept-and-offer), because the link carries a contract, not just a value. The tell for the dropped-word slice is data loss that appears only when the downstream stalls mid-stream and tracks the stall pattern rather than the data — a 'register slice' that is correct under light or idle-aligned stalls but eats a word exactly on the stall boundary is a naive flop masquerading as a handshake stage, not a link fault. The rule that makes it impossible is identical across SystemVerilog, Verilog, and VHDL: a pipeline slice on a handshake link must be a proper registered handshake stage — register the direction it cuts and give the in-flight word a landing pad (the skid slot) — and a full skid slice, which registers both directions and accepts-and-offers in one cycle, is the default because it cuts both the long forward and long backward paths at full throughput. Then verify across a chain of at least three slices by stalling on the exact transfer cycle (no word lost) and counting transfers over a both-ends-ready window (one per cycle). Chaining proper skid slices is the standard way real chips pipeline a timing-critical streaming interface.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the slice-flavor / latency-vs-throughput / contract-preserving reasoning behind pipelining a handshake.

Exercise 1 — Pick the slice flavor for the long path

For each situation, say whether you would insert a forward (valid) register slice, a backward (ready) register slice, or a full skid buffer, and why in one line: (a) static timing shows the long valid/data run failing but the ready path meets timing comfortably; (b) the backward ready run through five stages is the critical path and the forward path is fine; (c) a deep, fast pipeline where both the forward and backward runs exceed the period; (d) a bandwidth-critical AXI-stream link between two blocks that must stay at full rate. Then state which flavor you would standardize on as the default pipeline slice and why.

Exercise 2 — Do the latency-vs-throughput accounting

A ready/valid datapath is sliced with four full skid stages between source and sink. With both ends continuously ready, (i) how many cycles after a word enters the head does it appear at the tail, and (ii) what is the steady-state throughput in words per cycle? Now replace two of the four with half-throughput register slices and answer (i) and (ii) again, explaining exactly where the bubbles come from. State in one line the single controller capability that gives the full skid slice its extra throughput.

Exercise 3 — Prove the naive-flop slice drops a word on paper

Take a single naive-flop slice (in_ready = out_ready, one payload register, plain flop on valid/data). Hand-trace data_reg, valid_reg, in_ready, and out_valid for this sequence: two words arrive back-to-back (in_valid high) while out_ready is high for the first and low for the second, then high again. Mark the exact cycle a word is overwritten and lost, and explain in one line why a proper skid slice would have caught that word instead. Then state which single deterministic stall pattern in a testbench directly exposes this bug.

Exercise 4 — Balance parallel pipelined paths

A datapath splits a stream into two branches that a downstream merge block recombines word-by-word, expecting the two branches to stay time-aligned. Timing forces you to insert three full skid slices on branch A and only one on branch B. (i) By how many cycles are the branches misaligned at the merge, and what does the merge sample as a result? (ii) Give two ways to restore alignment, and name the cost of each. (iii) State the general rule about slice counts on parallel paths that must stay aligned.

Continue in RTL Design Patterns (sibling and forward links unlock as they ship):

  • Credit-Based Flow Control — Chapter 9.4; the alternative to registered ready on long links — decouple the ends with credits so the backward path is a counter, not a combinational run to slice.
  • Handshake Bugs & Deadlocks — Chapter 9.5; the classic valid/ready failures a correctly-sliced pipeline must still avoid — combinational loops, valid dropped before transfer, deadlock.
  • Case Study — Streaming FIFO — Chapter 9.7; a full streaming block where skid slices pipeline the timing-critical interfaces around a central FIFO — this page's chain in a real design.

Backward / in-track dependencies:

  • Skid Buffers — Chapter 9.3; the full-throughput registered handshake stage this page chains — the two-slot slice that registers both directions and never drops a word.
  • Backpressure & Stalls — Chapter 9.2; the long combinational ready chain this page breaks by inserting registered slices.
  • The valid/ready Handshake — Chapter 9.1; the two-wire contract every slice preserves on both faces — the composability that lets slices chain with no glue.
  • Latency vs Throughput — Chapter 8.2; the accounting this page revisits — each slice adds one cycle of latency and cuts zero throughput.
  • Pipelined Datapath — Chapter 8; cutting a combinational datapath with registers to raise frequency — the same move, here on a flow-controlled link.
  • The Pipeline Valid Bit — Chapter 8.1; the valid qualifier that travels with data through stages — the forward half of what each slice registers.
  • Synchronous FIFO Architecture — Chapter 7.1; the FIFO a skid slice is a depth-2 special case of, and the buffering to reach for when you need rate-matching, not just path-cutting.
  • The RTL Design Mindset — Chapter 0.2; the Datapath and Verification lenses this page applies — slices are pure timing structure, and the scoreboard is how you prove the chain never drops a word.
  • What Is an RTL Design Pattern? — Chapter 0.1; why chaining registered handshake slices earns the name 'pattern' — a recurring timing problem, a reusable composition, a synthesis reality, and a signature failure.

Verilog v1 prerequisites (the grammar this pattern transcribes into):

  • Physical Data Types — the reg/wire vector widths (WIDTH) the payload, skid, and inter-slice buses are sized in.
  • Blocking and Non-Blocking Assignments — why every slice's payload registers and occupancy bits update with non-blocking (<=) in their clocked block.
  • If-Else Statements — the mutually-exclusive if / else if chain each slice's three-state controller is written as.
  • Case Statements — the selection construct the slice controller and the chain wiring compose from.

11. Summary

  • A long handshake path fails timing because both directions run combinationally. In a bare valid/ready chain the forward valid/data path runs from source to sink and the backward ready path runs from sink to source, both in one clock period and both growing with chain length. Pipelining cuts those long runs with registered handshake slices — but a handshake link carries a contract, so you cut it with a slice, not a plain flop.
  • Three slice flavors, by which path they break. A forward (valid) slice registers valid/data and cuts only the forward path; a backward (ready) slice registers ready and cuts only the backward path; a full skid buffer registers both and, thanks to its skid slot, accepts-and-offers in the same cycle so it cuts both at full throughput. Match the flavor to the direction that is long; reach for the full skid buffer when both are, which on a deep pipe is the usual case.
  • Chaining slices pipelines the handshake — latency up, throughput unchanged. Each proper slice adds one cycle of latency and one pipeline register, so N slices add N cycles of latency; but each full skid slice accepts-and-offers in one cycle, so the chain still moves one word per cycle in steady state. This is 8.2's pipelining trade on a flow-controlled link — spend latency to buy frequency. Place slices from timing reports, on the longest valid and ready runs, so every register-to-register segment fits the period.
  • The signature bug is a naive flop pretending to be a slice. A plain register on valid/data with ready wired straight through registers only the forward direction with no landing pad, so it drops the word in flight when out_ready falls on a transfer cycle — the 9.3 dropped-word failure re-introduced as a pipeline slice — and it does not even cut the backward path. A half-throughput register slice never drops a word but bubbles every transfer and halves the rate. The full skid slice, which registers both directions and accepts-and-offers in one cycle, avoids both.
  • Verify across a chain of at least three slices. Scoreboard for in-order, exactly-once delivery under random end backpressure; de-assert out_ready on the exact transfer cycle to force the in-flight-word hazard at every internal boundary; count transfers over a both-ends-ready window to confirm one word per cycle (catching any bubbling slice); and check that each slice registers its cut so no combinational path threads the whole chain. Built and self-check-verified here in SystemVerilog, Verilog, and VHDL.