Skip to content

RTL Design Patterns · Chapter 9 · Handshake & Flow Control

Skid Buffers

A skid buffer is a small two-slot pipeline stage that lets you register the ready signal without ever dropping data. When you apply backpressure by chaining ready straight through several stages, that signal becomes one long combinational path from the far end back to the front, and on a fast pipeline it fails timing. Registering ready fixes the path, but a naive version loses any word already launched upstream on the cycle ready falls. The skid buffer adds one extra register so that in-flight word always has somewhere to land. This lesson builds the two-slot datapath and its three-state control, proves it never drops a word and still moves one word per cycle in steady state, and codes it in SystemVerilog, Verilog, and VHDL.

Advanced16 min readRTL Design PatternsSkid BufferValid ReadyBackpressureRegistered ReadyPipeline Timing

Chapter 9 · Section 9.3 · Handshake & Flow Control

1. The Engineering Problem

In 9.1 you built the valid/ready handshake: a word transfers on exactly the cycles where valid && ready are both high, and the two wires point in opposite directions — valid flows downstream with the data, ready flows upstream as permission. In 9.2 you used ready to apply backpressure: when a stage cannot accept more data, it drops its out_ready, and to propagate that stall upstream it drives its own in_ready from the downstream out_ready. The simplest correct way to do that is combinational: assign in_ready = out_ready (or in_ready = out_ready || !holding_a_word). It is correct, and for one isolated stage it is fine.

The trouble starts when stages chain, which is the normal case. Each stage's in_ready depends combinationally on the next stage's out_ready, which depends on the stage after that, and so on. A stall that originates at the far downstream end of the pipeline must ripple, in a single clock period, all the way back to the front so the source knows to hold. That backward ready becomes one long combinational path threading every stage's stall logic between two flops — and its logic depth grows with the number of stages. On a deep pipeline, or a shallow one clocked fast, that path is the critical path and it does not close timing. You cannot fix it by re-arranging the boolean logic; the depth is inherent to letting one cycle's ready see the whole pipeline.

The standard remedy is to cut the path with a register: make in_ready come directly out of a flop so a downstream stall only has to reach the nearest stage's ready flop in one cycle, not the whole chain. But registering ready changes its meaning. A combinational ready says 'I can take a word this cycle, having just looked at everything downstream.' A registered ready says 'based on what I knew last cycle, I can take a word this cycle.' That one-cycle staleness is the whole problem: an upstream source that saw your in_ready high last cycle will launch a word this cycle — it has already committed — and if this is exactly the cycle your in_ready needed to fall, that word is already on the wire with nowhere to go. If your stage holds only one word, the newcomer overwrites the word you were presenting downstream, and a word is silently lost. You need a landing spot for exactly that one in-flight word. That landing spot is the skid buffer.

the-need.v — registering ready breaks timing, but a naive register loses the in-flight word
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // From 9.2: combinational backpressure. Correct, but the ready path is LONG.
   //   assign in_ready = out_ready;   // ties far-downstream out_ready to far-upstream in_ready
 
   // We want to REGISTER in_ready (and out_valid) to cut that path:
   //   always_ff @(posedge clk) in_ready <= <can I take one next cycle?>;
   //   always_ff @(posedge clk) out_valid <= <do I have a word to present?>;
 
   // But a registered ready is a promise one cycle stale. Upstream saw in_ready HIGH
   // last cycle, so it LAUNCHES a word this cycle -- even if in_ready must fall now.
   // With ONE payload register there is nowhere to put that in-flight word:
   //   data_reg <= in_data;   // OVERWRITES the word we were presenting -> LOST.
 
   // The need: ONE extra 'skid' register so the in-flight word has somewhere to land,
   // so we can register BOTH out_valid and in_ready and still move 1 word/cycle.

2. Mental Model

3. Pattern Anatomy

The structure is two payload registers and a small controller. Everything hard is in how the controller decides when to fill the skid slot and when to drain it, and in keeping both interface signals registered.

The two-slot datapath. There are exactly two payload storage elements. The primary register holds the word currently presented downstream — it drives out_data, and out_valid is the registered bit that says it is occupied. The skid register (skid_data, with an occupancy bit skid_valid) is the landing pad: it holds the one extra word that arrived while the downstream was stalled. In steady flow the skid register is empty and the buffer behaves like a single registered stage; it fills only when the downstream de-asserts out_ready while a fresh word is arriving, and it empties again the moment the downstream re-asserts out_ready. out_data always comes from the primary register; the skid register never drives the output directly — it only reloads the primary register on the cycle after a stall clears.

The registered accept — in_ready = !skid_valid. The stage accepts a word whenever its skid slot is free, because a free skid slot guarantees a place to land the one word that could be in flight if the downstream stalls next cycle. So in_ready is driven directly by the registered skid_valid bit — no combinational path from out_ready to in_ready. When a word arrives and the downstream is not ready to take the primary word, the arriving word goes into the skid register and in_ready drops for one cycle (skid now full). When the downstream re-asserts out_ready, the primary register is consumed, the skid word slides into the primary register, the skid empties, and in_ready rises again. That fill-then-drain is the 'skid.'

The two-slot skid datapath — primary register presents, skid register catches the in-flight word

data flow
The two-slot skid datapath — primary register presents, skid register catches the in-flight wordacceptskiddrainreloadupstream in_valid/ in_datain_ready = !skid_valid (registered)PRIMARY registerdrives out_data; out_valid = occupiedSKID registercatches the word in flight during a stallreload selectnext primary = skid word if skid fulldownstream out_valid / out_datadownstreamout_valid /…out_ready stalls -> skid fills
Two payload registers. The PRIMARY register always drives out_data and its occupancy is the registered out_valid. The SKID register is a landing pad for the one word upstream launches while the downstream is stalled — it never drives the output, it only reloads the primary register the cycle after out_ready re-asserts. in_ready is driven straight from the registered skid_valid bit (in_ready = !skid_valid), so there is NO combinational path from out_ready back to in_ready — that is the timing win. One skid slot is exactly enough because a registered in_ready can only ever have one word in flight. This structure is identical in SystemVerilog, Verilog, and VHDL.

The three-state control. The controller has exactly three reachable states, and naming them makes the whole thing legible:

  • EMPTY — neither register holds a word. out_valid = 0, in_ready = 1 (skid free). An arriving word loads the primary register and moves to RUNNING.
  • RUNNING — the primary register holds a word (out_valid = 1), the skid register is free (in_ready = 1). This is the steady state, and it moves one word per cycle: if the downstream takes the primary word (out_ready = 1) and a new word arrives, the new word loads the primary register and you stay in RUNNING — accept and offer in the same cycle, which is what keeps throughput at 1/cycle. If a new word arrives but the downstream is not ready, the new word goes to the skid register and you move to STALLED-FULL.
  • STALLED-FULL — both registers hold a word: primary presented downstream, skid holding the in-flight word (out_valid = 1, in_ready = 0 because the skid slot is taken). You stay here until the downstream asserts out_ready; on that cycle the primary word leaves, the skid word reloads the primary register, the skid empties, and you drop back to RUNNING with in_ready high again.

The skid-buffer control FSM — EMPTY / RUNNING / STALLED-FULL

data flow
The skid-buffer control FSM — EMPTY / RUNNING / STALLED-FULLword arrivesaccept+offersame cycleword arrives, out stalledwordarrives, ou…out_ready: skiddrainsdrains, nothing arrivesdrains,nothing…EMPTYout_valid=0, in_ready=1; no data heldRUNNINGout_valid=1, in_ready=1; primary held, skid freeSTALLED-FULLout_valid=1, in_ready=0; primary + skid both held
Three reachable states. EMPTY holds nothing and offers in_ready. RUNNING holds one word in the primary register with the skid free, offering in_ready — the steady state that accepts a new word and presents the held word in the SAME cycle, sustaining one transfer per cycle. STALLED-FULL holds two words (primary presented, skid caught the in-flight word) and de-asserts in_ready because the skid slot is taken; the instant out_ready re-asserts, the primary word leaves, the skid word reloads the primary register, and control returns to RUNNING. in_ready is exactly 'not in STALLED-FULL', i.e. in_ready = !skid_valid — a registered bit, never a combinational function of out_ready. STALLED-FULL is the only state where in_ready is low, which is why one skid slot suffices.

Why depth-2 is exactly enough, and the relation to a FIFO. A skid buffer is, in spirit, a depth-2 FIFO (7.1): a small circular buffer where the pointers can never be more than two entries apart. But you do not need the general FIFO machinery, because the interface is a single registered handshake stage: with in_ready registered, upstream can launch at most one unexpected word before it sees in_ready fall, so the maximum overflow is one word — hence two slots (the one being presented plus the one in flight). A depth-1 buffer has no room for the in-flight word (the dropped-word bug, §6/§7); a depth-3+ buffer stores words that a registered in_ready can never actually put in flight, so it is pure wasted area for this purpose. Depth-2 is the exact size of a registered skid stage.

The throughput proof — drains as fast as it fills. The skid slot can gain at most one word per cycle (upstream can launch one word per cycle) and can shed one word per cycle (the primary consumes one, the skid reloads it). So whenever the downstream is ready, the skid drains one word while at most one arrives — occupancy never runs away, and the moment both sides are ready the buffer is in RUNNING accepting and offering in the same cycle: one word in, one word out, every cycle. The skid buffer therefore costs one cycle of pipeline latency (a word passes through the primary register) and one payload register of area, but it does not cost throughput — steady-state rate is 1 word/cycle whenever both sides can sustain it. That is the invariant every testbench checks in §5.

4. Real RTL Implementation

This is the core of the page. We build two things — (a) a full-throughput two-slot skid buffer (registered in_ready, out_valid, out_data; a primary register plus a skid register; the three-state control; WIDTH-generic) with a self-checking clocked scoreboard testbench that applies random out_ready de-assertions (including on transfer cycles) and asserts in-order exactly-once delivery, one-word-per-cycle steady-state throughput, and that in_ready/out_valid are registered; and (b) the single-register dropped-word bug beside the two-slot fix — and each is shown in SystemVerilog, Verilog, and VHDL with an RTL block and a self-checking clocked testbench. The reasoning is identical in all three; only the syntax differs.

One discipline runs through every RTL block, inherited from the register pattern (2.1) and 9.1/9.2: both payload registers and both occupancy bits update on the clock with non-blocking assignments; reset is explicit (synchronous, active-high rst here); a word is accepted only when in_valid && in_ready, presented downstream only when out_valid, and consumed only when out_valid && out_ready. The interface signals in_ready and out_valid are driven from registered bits, never combinationally from out_ready.

4a. The full-throughput two-slot skid buffer — three ways

The buffer stores at most two WIDTH-bit words: one in the primary register (presented downstream) and at most one in the skid register (caught during a stall). out_valid is the primary register's occupancy bit; in_ready is the inverse of the skid register's occupancy bit (in_ready = !skid_valid). The single always/process block below is the whole controller.

skid_buffer.sv — full-throughput two-slot skid buffer (registered valid AND ready)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module skid_buffer #(
       parameter int WIDTH = 8                        // datapath width
   )(
       input  logic             clk,
       input  logic             rst,                  // synchronous, active-high
       // upstream (producer) side
       input  logic             in_valid,
       output logic             in_ready,             // REGISTERED: = !skid_valid
       input  logic [WIDTH-1:0] in_data,
       // downstream (consumer) side
       output logic             out_valid,            // REGISTERED: primary occupancy
       input  logic             out_ready,
       output logic [WIDTH-1:0] out_data              // primary register
   );
       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
 
       // Interface signals are pure registered reads -> no comb path out_ready->in_ready.
       assign out_valid = valid_reg;
       assign out_data  = data_reg;
       assign in_ready  = ~skid_valid;                // accept iff the skid slot is free
 
       // A word is accepted this cycle iff upstream offers and we are ready.
       wire   accept   = in_valid  & in_ready;        // in_valid && !skid_valid
       // The primary word is consumed this cycle iff we present and downstream takes it.
       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 begin
               if (skid_valid) begin
                   // STALLED-FULL: both slots held. Wait for the downstream to take the
                   // primary word; then the skid word slides into the primary register.
                   if (out_ready) begin
                       data_reg   <= skid_data;        // drain skid -> primary
                       skid_valid <= 1'b0;             // skid now free -> in_ready rises
                       // valid_reg stays 1: primary is immediately reloaded from skid.
                   end
               end else begin
                   // EMPTY or RUNNING (skid free -> in_ready high).
                   if (consume && !accept) begin
                       // Primary leaves, nothing new arrives -> primary empties.
                       valid_reg <= 1'b0;
                   end else if (accept && (consume || !valid_reg)) begin
                       // Accept-and-offer in the SAME cycle (RUNNING throughput = 1/cycle),
                       // or first fill from EMPTY: new word loads the primary register.
                       data_reg  <= in_data;
                       valid_reg <= 1'b1;
                   end else if (accept && valid_reg && !consume) begin
                       // Primary busy and NOT consumed, but a word arrived -> SKID it.
                       skid_data  <= in_data;          // land the in-flight word
                       skid_valid <= 1'b1;             // move to STALLED-FULL -> in_ready falls
                   end
               end
           end
       end
   endmodule

The clocked scoreboard testbench is where the flagship claim is proven: it streams a known sequence in, randomly de-asserts out_ready (including on the exact cycle a word transfers in), pushes every accepted word onto a reference queue and pops it when a word transfers out, and asserts the outputs match in order, exactly once. It also counts transfers over a both-sides-ready window to check the steady-state rate is one word per cycle, and checks in_ready/out_valid are registered by sampling them just after the clock edge.

skid_buffer_tb.sv — scoreboard: random out_ready, in-order exactly-once, 1/cycle throughput
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module skid_buffer_tb;
       localparam int WIDTH = 8;
       localparam int NWORDS = 200;
       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;
 
       skid_buffer #(.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;
 
       // Scoreboard: expected words in the order they were accepted.
       logic [WIDTH-1:0] sb [$];
       int sent = 0, got = 0;
       logic [WIDTH-1:0] next_word = 8'h00;
 
       // Producer: offer a fresh, monotonically-numbered word whenever we can.
       always @(negedge clk) begin
           if (rst) begin in_valid <= 1'b0; end
           else begin
               in_valid <= (sent < NWORDS);
               in_data  <= next_word;
           end
       end
       // When a word is actually ACCEPTED, record it and advance the source number.
       always @(posedge clk) if (!rst && in_valid && in_ready) begin
           sb.push_back(in_data); sent++; next_word <= next_word + 8'd1;
       end
 
       // Consumer: randomly stall, INCLUDING on cycles where a transfer would occur.
       always @(negedge clk) out_ready <= ($urandom_range(0, 3) != 0); // ~75% ready
 
       // Check every word that transfers out: in order, exactly once, correct value.
       always @(posedge clk) if (!rst && out_valid && out_ready) begin
           if (sb.size() == 0) begin
               $error("delivered a word that was never accepted: %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
 
       // Registered-interface check: sampled right after the edge, in_ready must equal
       // !skid_valid and out_valid must equal the primary occupancy — pure flop outputs,
       // never wiggling combinationally within the cycle from out_ready.
       logic in_ready_q, out_valid_q;
       always @(posedge clk) begin in_ready_q <= in_ready; out_valid_q <= out_valid; end
 
       // Throughput: over a long window where BOTH sides are ready, one word/cycle.
       int both_ready_cycles = 0, both_ready_xfers = 0;
       always @(posedge clk) if (!rst) begin
           if (in_valid && in_ready && out_valid && out_ready) both_ready_xfers++;
           if (in_valid && out_ready)                          both_ready_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;
 
           // Run until every word has been accepted AND drained out.
           wait (got == NWORDS);
           repeat (4) @(posedge clk);
 
           if (sb.size() != 0) begin $error("%0d words accepted but 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 in-order exactly-once; registered ready/valid; ~1 word/cycle when both ready", NWORDS);
           else
               $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

The Verilog form is the same controller with reg/wire typing; the scoreboard uses a simple array + head/tail indices instead of a SystemVerilog queue, and $random in place of $urandom.

skid_buffer.v — full-throughput two-slot skid buffer in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module skid_buffer #(
       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 begin
               if (skid_valid) begin
                   // STALLED-FULL: drain skid into primary when downstream takes primary.
                   if (out_ready) begin
                       data_reg   <= skid_data;
                       skid_valid <= 1'b0;
                   end
               end else begin
                   if (consume & ~accept) begin
                       valid_reg <= 1'b0;             // primary empties
                   end else if (accept & (consume | ~valid_reg)) begin
                       data_reg  <= in_data;          // accept-and-offer / first fill
                       valid_reg <= 1'b1;
                   end else if (accept & valid_reg & ~consume) begin
                       skid_data  <= in_data;         // land in-flight word
                       skid_valid <= 1'b1;            // -> STALLED-FULL
                   end
               end
           end
       end
   endmodule
skid_buffer_tb.v — self-checking scoreboard; random out_ready; PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module skid_buffer_tb;
       parameter WIDTH  = 8;
       parameter NWORDS = 200;
       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;
 
       // Scoreboard as a ring of expected words.
       reg  [WIDTH-1:0] sb [0:1023];
       integer head, tail;
 
       skid_buffer #(.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;
 
       // Producer.
       always @(negedge clk) begin
           if (rst) in_valid <= 1'b0;
           else begin
               in_valid <= (sent < NWORDS);
               in_data  <= next_word;
           end
       end
       // Record on actual acceptance.
       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
 
       // Consumer: random stalls, including on transfer cycles.
       always @(negedge clk) out_ready <= ($random % 4 != 0);
 
       // Check each delivered word: in order, exactly once.
       always @(posedge clk) if (!rst && out_valid && out_ready) begin
           if (head == tail) begin
               $display("FAIL: delivered a word never accepted: %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 (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 in-order exactly-once; registered ready/valid", NWORDS);
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

In VHDL the two payload registers are std_logic_vector signals with unsigned-free control bits; the interface signals are concurrent assignments from the registered occupancy bits, and the controller is a single clocked process.

skid_buffer.vhd — full-throughput two-slot skid buffer 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 skid_buffer 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 skid_buffer 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
       -- Interface signals: pure registered reads -> no comb path out_ready -> in_ready.
       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');
               else
                   if skid_valid = '1' then
                       -- STALLED-FULL: drain skid into primary when downstream takes it.
                       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';           -- primary empties
                       elsif accept_i = '1' and (consume_i = '1' or valid_reg = '0') then
                           data_reg  <= in_data;       -- accept-and-offer / first fill
                           valid_reg <= '1';
                       elsif accept_i = '1' and valid_reg = '1' and consume_i = '0' then
                           skid_data  <= in_data;      -- land in-flight word
                           skid_valid <= '1';          -- -> STALLED-FULL
                       end if;
                   end if;
               end if;
           end if;
       end process;
   end architecture;
skid_buffer_tb.vhd — self-checking scoreboard; random out_ready; assert severity
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity skid_buffer_tb is
   end entity;
 
   architecture sim of skid_buffer_tb is
       constant WIDTH  : positive := 8;
       constant NWORDS : integer  := 200;
       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');
 
       -- Scoreboard ring of expected words.
       type   sb_t is array (0 to 1023) 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');
       -- simple LFSR for a random stall pattern
       signal lfsr : unsigned(7 downto 0) := x"A5";
   begin
       dut : entity work.skid_buffer
           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;
 
       -- Producer + random consumer stall, driven off the falling edge.
       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;
 
       -- Record accepted words; check delivered words in order, exactly once.
       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 "skid_buffer self-check complete: in-order exactly-once, registered ready/valid" severity note;
           wait;
       end process;
   end architecture;

4b. The single-register dropped-word bug — buggy vs fixed, in all three HDLs

Pattern (b) is the signature failure, shown here as buggy vs fixed RTL in each language and dramatized in §7. The buggy design registers out_valid/out_data and drives in_ready combinationally from out_ready, but keeps only one payload register — no skid slot. When out_ready de-asserts on a cycle where a word was accepted, that just-accepted word overwrites the currently-held output and is lost. The fix is the two-slot skid buffer from 4a.

skid_bug.sv — BUGGY (one register, in-flight word lost) vs FIXED (two-slot skid)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: registers out_valid/out_data but has NO skid slot, and drives in_ready
   //        combinationally from out_ready. When out_ready falls on a cycle where a
   //        word was accepted, the new word OVERWRITES the held output -> word LOST.
   module skid_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, and only one slot
       always_ff @(posedge clk) begin
           if (rst) begin valid_reg <= 1'b0; data_reg <= '0; end
           else begin
               // Accept whenever in_ready (= out_ready) is high...
               if (in_valid && in_ready) begin
                   data_reg  <= in_data;              // OVERWRITES a held, unconsumed word
                   valid_reg <= 1'b1;
               end else if (valid_reg && out_ready) begin
                   valid_reg <= 1'b0;
               end
           end
       end
   endmodule
 
   // FIXED: add the SKID register. in_ready = !skid_valid (registered). The in-flight
   //        word lands in the skid slot instead of overwriting the primary word.
   module skid_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
skid_bug_tb.sv — stall on a transfer cycle; the fixed core loses no word
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module skid_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 skid_good to PASS. skid_bad drops the word accepted on the stall edge.
       skid_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;
 
       // Record every accepted word; check every delivered word in order, exactly once.
       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;
 
           // Drive the EXACT hazard: hold in_valid high, then de-assert out_ready on the
           // cycle a word is accepted, so the buggy DUT would overwrite the held word.
           out_ready = 1'b1;
           for (int i = 0; i < 8; i++) begin
               @(negedge clk); in_valid = 1'b1; in_data = 8'h30 + i[7:0];
               out_ready = (i % 2 == 0) ? 1'b0 : 1'b1;   // stall on even cycles (transfer boundary)
               @(posedge clk); #1;
           end
           in_valid = 1'b0;
           // Drain everything still buffered.
           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 across the stall boundary; every accepted word delivered once");
           else             $display("FAIL: %0d errors (an in-flight word was dropped)", errors);
           $finish;
       end
   endmodule

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

skid_bug.v — BUGGY (one register) vs FIXED (two-slot skid) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: one payload register, combinational in_ready -> in-flight word overwritten.
   module skid_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, 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 a held, unconsumed word
                   valid_reg <= 1'b1;
               end else if (valid_reg && out_ready) begin
                   valid_reg <= 1'b0;
               end
           end
       end
   endmodule
 
   // FIXED: add the skid register; in_ready = !skid_valid (registered).
   module skid_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
skid_bug_tb.v — self-checking; stall on the transfer cycle; require no dropped word
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module skid_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 skid_good to PASS; skid_bad drops the in-flight word.
       skid_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'h30 + 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 across the stall boundary");
           else             $display("FAIL: %0d errors (an in-flight word was dropped)", errors);
           $finish;
       end
   endmodule

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

skid_bug.vhd — BUGGY (one register) vs FIXED (two-slot skid) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: one payload register, combinational in_ready -> in-flight word overwritten.
   entity skid_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 skid_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, 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 a 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: add the skid register; in_ready = not skid_valid (registered).
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   entity skid_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 skid_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;
skid_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 skid_bug_tb is
   end entity;
 
   architecture sim of skid_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 skid_good to PASS; skid_bad drops the in-flight word.
       dut : entity work.skid_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#30# + 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 "skid_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: register both out_valid and in_ready, drive in_ready from the skid slot's emptiness (in_ready = !skid_valid), and add exactly one skid register so the word already in flight when ready falls has somewhere to land — then no word is lost and throughput stays at one word per cycle.

5. Verification Strategy

A skid buffer's correctness is a data-integrity-under-backpressure property, so verification lives at the stall boundary: connect through the buffer, drive backpressure that de-asserts out_ready at every phase — including the exact cycle a word transfers in — and prove that every accepted word comes out in order, exactly once, that steady-state throughput is one word per cycle, and that the interface signals are registered.

Every word accepted on in_valid && in_ready is delivered on out_valid && out_ready exactly once and in FIFO order under ALL backpressure patterns; steady-state throughput is one word per cycle when both sides are ready; and in_ready and out_valid are registered — there is no combinational path from out_ready to in_ready.

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

  • Scoreboard: in-order, exactly-once delivery under random backpressure. Push every word accepted on in_valid && in_ready onto a reference queue; pop and compare on every out_valid && out_ready. Drive out_ready from a random source that de-asserts at every phase, explicitly including the cycle on which a word is accepted — that stall boundary is where the dropped-word bug lives. At the end assert the queue is empty (no accepted word was dropped) and every delivered word matched (no reorder, no duplication). The §4 scoreboards do exactly this: SV queue push_back/pop_front with assert, Verilog ring buffer with if (…) $display("FAIL"), VHDL ring with assert … severity error.
  • The exact hazard: stall on a transfer cycle. Beyond random stalls, drive the deterministic worst case directly — hold in_valid high and de-assert out_ready on precisely the cycle a word is accepted, so a single-register design would overwrite the held word. A correct skid buffer lands the in-flight word in the skid slot and delivers it later; the §4b bug testbench forces this pattern and requires the earliest words to survive.
  • Throughput: one word per cycle when both sides are ready. Count transfers over a window where in_valid and out_ready are continuously high; the delivered count must equal the number of cycles (minus the fixed one-cycle fill latency). A design that inserts a bubble every transfer (the half-throughput register slice, §6) delivers a word only every other cycle and fails this check — this is what separates a true skid buffer from a naive registered stage.
  • Registered-interface check. in_ready must be a pure function of the registered skid_valid bit and out_valid of the registered primary occupancy — neither may wiggle combinationally within a cycle in response to out_ready. Verify structurally (the RTL drives them from flops) and in simulation by confirming they only change at clock edges. The whole point of the pattern is that this registration breaks the long backpressure path from 9.2; a testbench that finds in_ready responding to out_ready in the same cycle has caught a design that did not actually cut the path.
  • Corner cases to exercise. Downstream stalls for a long run then releases (skid holds one word the whole time, drains on release); upstream idles mid-stream (in_valid low — the buffer holds and delivers what it has); simultaneous fill-and-drain in RUNNING (accept and offer in the same cycle — the throughput case); reset asserted mid-flight (both registers clear, out_valid and skid_valid return to zero, in_ready returns high); and back-to-back stall/release toggling every cycle (out_ready alternating), which repeatedly crosses the RUNNING to STALLED-FULL boundary and is the harshest ordering test.
  • Expected waveform. On a correct run, as words stream in with out_ready high, each word appears on out_data one cycle after acceptance (the fill latency) and out_valid/out_ready transfer one per cycle. When out_ready drops mid-stream, out_valid stays high holding the primary word, the next accepted word loads the skid register, and in_ready falls for exactly the cycles the skid is full; when out_ready re-asserts, the primary word transfers, the skid word slides into the primary register, and in_ready rises again — no gap in the delivered sequence, no lost word. The visual signature of the single-register bug is a delivered stream that is missing the word accepted on a stall edge; the signature of the half-throughput slice is a delivered word only every other cycle even with both sides ready.

6. Common Mistakes

The single-register 'skid' that drops the in-flight word. The signature failure. A designer registers out_valid/out_data and drives in_ready = out_ready combinationally but keeps only one payload register — no skid slot. Because in_ready tracks out_ready, on the cycle out_ready (hence in_ready) falls, an upstream that committed to a word off the previous cycle's high in_ready still drives it in, and the block's single register overwrites the currently-held, unconsumed output — the word it was presenting is destroyed and the newcomer takes its place, so one word is silently lost at the stall boundary. It passes when the downstream never stalls mid-stream (nothing is ever in flight when ready falls) and corrupts exactly on the stall boundary. The fix is the second register: in_ready = !skid_valid, land the in-flight word in the skid slot. (Post-mortem in §7.)

A correct-but-half-throughput register slice. A legitimate, data-safe design that pays for its safety with throughput: a single-register stage that refuses to accept a new word in the same cycle it presents its held word, so it can only either offer or accept per cycle, never both. It never drops a word — but it inserts a bubble on every transfer, so a stream that should move at one word per cycle moves at one word every two cycles: half throughput. It is a fine choice when area is scarce and you can tolerate half rate, but it is not a skid buffer, and reaching for it when you needed full throughput silently halves your bandwidth. The true skid buffer's RUNNING state accepts-and-offers in the same cycle precisely to avoid this bubble. (Second DebugRow in §7.)

Combinational in_ready that defeats the timing purpose. The entire reason to build a skid buffer is to register in_ready so a downstream stall reaches only the nearest ready flop, cutting the long combinational backpressure path from 9.2. If you leave in_ready = out_ready (or otherwise make in_ready a combinational function of out_ready) — even with a correct two-slot datapath — you have re-introduced the very path you set out to break, and the pipeline still fails timing. in_ready must come from the registered skid_valid bit. Registering the data without registering the ready is doing the work and keeping the bug.

Off-by-one in the skid control — draining or accepting one cycle wrong. The subtle failures cluster at the STALLED-FULL to RUNNING transition. Draining the skid one cycle early (before the downstream actually took the primary word) loses the primary word; draining one cycle late wastes a throughput slot and can wedge if a new word is arriving. Accepting into the skid when the primary is being consumed the same cycle (so the primary slot is actually about to free) needlessly fills the skid and drops in_ready a cycle too long. The condition set must be exact: skid the new word only when the primary is held and not being consumed; drain the skid on the cycle the primary is consumed; accept-and-offer in the same cycle when the primary is consumed and a word arrives. The §4a controller encodes precisely these.

Making the skid register drive the output. A tempting but wrong shortcut is to assign out_data = skid_valid ? skid_data : data_reg — presenting the skid word directly. That reintroduces a mux (and often a combinational out_valid path) on the output, and it breaks the invariant that out_data is a clean registered value. The skid register must never drive the output; it only reloads the primary register on the drain cycle. Keeping the output a pure register read is part of what makes the interface clean and the timing predictable.

7. DebugLab

The pipeline that eats a word at the stall boundary — a single-register 'skid' with no landing pad

The engineering lesson: a registered ready is a promise made one cycle stale, so an upstream that saw it high has always committed one word you must be able to catch — give that in-flight word exactly one landing pad (the skid register) or you lose it, and let RUNNING accept-and-offer in the same cycle or you halve your throughput. The tell for the dropped-word bug is data loss that appears only when the downstream stalls mid-stream and tracks the stall pattern rather than the data — a stage correct under light or idle-aligned stalls that eats a word exactly on the stall boundary is a missing skid slot, not a link fault. The tell for the throughput bug is a data-perfect stage that nonetheless runs at half rate with both sides ready. Two rules make both impossible, and they are identical across SystemVerilog, Verilog, and VHDL: register both out_valid and in_ready, drive in_ready = !skid_valid, and add one skid register so the in-flight word lands instead of overwriting, and let the RUNNING state accept a new word and present the held word in the same cycle so no bubble is inserted. Then verify by stalling on the exact transfer cycle (no word lost) and by counting transfers over a both-ready window (one per cycle). The skid buffer is the pattern to internalize because it is the standard registered-handshake buffer that appears at nearly every timing-critical interface on a real chip.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the in-flight-word / registered-ready reasoning and the boundary discipline behind it.

Exercise 1 — Trace the stall boundary and prove no word is lost

For an 8-bit skid buffer, hand-trace data_reg, valid_reg, skid_data, skid_valid, in_ready, and out_valid cycle by cycle for this sequence: three words arrive back-to-back (in_valid high) while out_ready is high for the first, low for the second and third, then high again. Mark the exact cycle the skid register fills, the cycle in_ready falls, and the cycle the skid drains into the primary register. Confirm in one line each that no accepted word is overwritten and that every accepted word eventually leaves on out_valid && out_ready.

Exercise 2 — Prove one word per cycle

With both in_valid and out_ready held continuously high, hand-trace the buffer from EMPTY through five words and show that after the one-cycle fill latency, exactly one word transfers out per cycle. Then repeat the trace for the half-throughput register slice (in_ready = !valid_reg, one register) and show it delivers a word only every other cycle. State the single controller condition that gives the skid buffer its extra throughput.

Exercise 3 — Choose the buffer

For each requirement, pick a full-throughput skid buffer, a half-throughput register slice, or plain combinational backpressure, and justify in one line: (a) a deep pipeline whose backward ready path fails timing and must run at full bandwidth; (b) a single, isolated stage where the ready path already meets timing and area is at a premium; (c) a timing-critical interface that can tolerate half rate but not the extra register; (d) a bandwidth-sensitive AXI-stream register slice between two clock-gated blocks. Then state which one you would standardize on for a generic registered-handshake buffer and why.

Exercise 4 — Verify without shipping the bug

Write the test plan (not the RTL) that would have caught the §7 dropped-word bug before tape-out. Specify: the scoreboard mechanism and what you push/pop; the exact deterministic stall pattern (which cycle you de-assert out_ready) that forces the in-flight-word hazard; the throughput check that would catch a half-throughput slice; the registered-interface check that confirms the backpressure path was actually cut; and the end-of-test assertions that prove no accepted word was dropped. State which single check is the direct detector for the dropped-word bug.

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

  • Valid/Ready Handshake — Chapter 9.1; the two-wire contract this page buffers — a word transfers on valid && ready, valid flowing downstream and ready upstream, the interface the skid buffer registers on both sides.
  • Backpressure & Stalls — Chapter 9.2; the stall propagation that drives in_ready from out_ready and exposes the long combinational backpressure path this page cuts by registering ready.
  • Credit-Based Flow Control — Chapter 9.4; the alternative to ready/backpressure that decouples the two ends with credits, removing the round-trip ready path entirely for long links.
  • Handshake Bugs & Deadlocks — Chapter 9.5; the classic valid/ready failures — combinational loops, valid dropped before transfer, deadlock — that a correctly registered skid buffer avoids.
  • Ready/Valid Pipelining — Chapter 9.6; chaining registered handshake stages (skid buffers and register slices) to pipeline a ready/valid datapath without losing throughput.
  • Case Study — Streaming FIFO — Chapter 9.7; a full streaming block where skid buffers sit at the timing-critical interfaces around a central FIFO.

Backward / in-track dependencies:

  • Synchronous FIFO Architecture — Chapter 7.1; the circular-buffer FIFO the skid buffer is a depth-2 special case of — same push/pop discipline, bounded to two entries.
  • First-Word Fall-Through FIFO — Chapter 7.5; the read-timing style where data is valid before rd_en, closely related to how the skid buffer presents out_data a registered cycle ahead.
  • The Register Pattern (D-FF) — Chapter 2.1; the flops the primary and skid registers are built from, and the reset-first clocked-update discipline every block here follows.
  • Register with Enable / Load — Chapter 2.2; the enable-gated load the primary and skid registers use — a register that captures only on its accept/drain condition.
  • The Pipeline Valid Bit — Chapter 8.1; the valid bit that travels with data through pipeline stages — the same occupancy semantics valid_reg and skid_valid carry here.
  • Moore vs Mealy Machines — Chapter 4.1; the state-machine lens on the EMPTY/RUNNING/STALLED-FULL controller — a small Moore FSM whose outputs (in_ready, out_valid) are registered functions of the state.
  • The RTL Design Mindset — Chapter 0.2; the State and Verification lenses this page applies — the two registers are the buffer's state, and the scoreboard is how you prove it never drops a word.
  • What Is an RTL Design Pattern? — Chapter 0.1; why the skid buffer earns the name 'pattern' — a recurring timing problem, a reusable two-slot form, 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 and skid registers are sized in.
  • Blocking and Non-Blocking Assignments — why the payload registers and occupancy bits update with non-blocking (<=) in their clocked block.
  • If-Else Statements — the mutually-exclusive if / else if chain the three-state skid controller is written as.
  • Initial & Always Blocks — the clocked always block the controller lives in and the initial block the testbenches drive stimulus from.

11. Summary

  • Registering ready breaks timing but risks losing a word. Combinational backpressure (9.2) ties the far-downstream out_ready to the far-upstream in_ready through a long path that fails timing on deep or fast pipelines. Registering in_ready cuts that path — but a registered ready is a promise one cycle stale, so an upstream that saw it high launches a word on the cycle it falls, and a single-register stage overwrites its held output and drops that in-flight word.
  • The skid buffer adds exactly one landing pad. Two payload registers: a primary register that drives out_data (occupancy out_valid) and one skid register that catches the in-flight word during a stall. Drive in_ready = !skid_valid (registered) and out_valid = valid_reg (registered), so both interface signals come out of flops and there is no combinational path from out_ready to in_ready. One skid slot is exactly enough because a registered in_ready can only ever have one word in flight when it falls — depth-2, the primary plus one skid.
  • Three states, full throughput. EMPTY (nothing held), RUNNING (primary held, skid free — the steady state that accepts a new word and presents the held word in the same cycle, sustaining one word/cycle), and STALLED-FULL (both held, in_ready low). The buffer skids one entry when the downstream stalls and drains it the instant out_ready returns, so it costs one cycle of latency and one payload register but no throughput.
  • The signature bug is the dropped in-flight word. A single-register stage with combinational in_ready = out_ready overwrites its held word when out_ready falls on a transfer cycle — silent, intermittent loss that tracks the stall pattern and passes any test without mid-stream stalls. A correct-but-half-throughput register slice (in_ready = !valid_reg) never drops a word but inserts a bubble every transfer and runs at half rate. The skid buffer avoids both.
  • Verify at the stall boundary. Scoreboard for in-order, exactly-once delivery under random backpressure; de-assert out_ready on the exact transfer cycle to force the in-flight-word hazard; count transfers over a both-ready window to confirm one word per cycle; and check in_ready/out_valid are registered so the backpressure path was truly cut. Built and self-check-verified here in SystemVerilog, Verilog, and VHDL.