Skip to content

AMBA AXI · Module 15

The Skid Buffer

Build the workhorse of pipelined AXI — a two-entry skid buffer that fully registers a VALID/READY handshake for timing closure while sustaining one-beat-per-cycle throughput. Why a single register stalls, how the skid (overflow) register absorbs the bubble, and the FSM that runs it.

Every AXI channel is a VALID/READY handshake, and at speed those handshakes must be registered on both sides to close timing — an unregistered path lets the consumer's READY ripple combinationally back to the producer, creating a long, often critical, path across a chip. But naively registering the handshake either breaks throughput (you drop to one beat every two cycles) or breaks the protocol (you lose data when the downstream stalls). The skid buffer is the standard solution: a small, two-entry buffer that registers both VALID and READY, breaks the combinational path in both directions, and still sustains full one-beat-per-cycle throughput. It's the most reused piece of AXI RTL there is — in interconnect ports, FSM outputs, FIFO front-ends, and pipeline stages. This chapter builds it and explains exactly why the "skid" (overflow) register is necessary.

1. The Problem: Registering a Handshake Naively

You want to insert a pipeline register in an AXI channel so neither side sees a combinational path through the other. The naive options both fail:

  • Register only the data + VALID (a "pipe" stage), pass READY straight through combinationally. This breaks the forward path but leaves READY rippling backward — timing isn't fully closed.
  • Register READY too, with a single data register. Now both directions are registered, but there's a hazard: in the cycle the downstream asserts READY after a stall, the upstream — which only just saw READY go high (it's registered, so one cycle late) — may have already placed a new beat on the bus. With only one register, you have nowhere to put the beat that's "in flight" when READY toggles. You either drop it (data loss) or you must have held off accepting it, costing a bubble every time READY deasserts — half throughput.

The fix is a second register — the skid or overflow register — to catch exactly that one in-flight beat. Two registers are both necessary and sufficient for a fully-registered, full-throughput handshake.

Upstream sees registered READY one cycle late; an in-flight beat needs the skid register; main plus skid register absorb it for full throughput.Upstreamsees READY lateIn-flight beatplaced before stall seenMain registercurrent beatSkid registercatches overflowDownstreamdrives registered READYFull throughputno bubble, no loss12
Figure 1 — why one register isn't enough. With registered READY, the upstream learns of a stall one cycle late, so a beat can already be in flight when the buffer fills. A single data register has nowhere to hold it — forcing either data loss or a one-cycle bubble per stall (half throughput). A second 'skid' register catches that in-flight beat, restoring full throughput.

2. The Two-Entry Structure

A skid buffer holds up to two beats: a main register that feeds the output, and a skid register that captures the overflow beat that arrives in the cycle the buffer becomes full. Upstream READY is asserted whenever the skid register is empty (i.e. there's room for one more in-flight beat), so the upstream never sees a combinational READY and is never starved while space exists.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module skid_buffer #(parameter int W = 32) (
  input  logic         clk, rstn,
  // upstream (sink)
  input  logic [W-1:0] s_data,
  input  logic         s_valid,
  output logic         s_ready,
  // downstream (source)
  output logic [W-1:0] m_data,
  output logic         m_valid,
  input  logic         m_ready
);
  logic [W-1:0] skid_data;
  logic         skid_valid;
 
  // Accept upstream while we have room for the overflow beat
  assign s_ready = !skid_valid;
 
  always_ff @(posedge clk) begin
    if (!rstn) begin
      m_valid <= 1'b0; skid_valid <= 1'b0;
    end else begin
      if (s_ready) begin
        // room: capture incoming beat into skid if output is busy,
        // else straight into the main register
        if (m_valid && !m_ready) begin
          // output stalled and we just took a beat → it skids
          if (s_valid) begin skid_data <= s_data; skid_valid <= 1'b1; end
        end else begin
          // output free (or being consumed): load main directly
          m_data  <= s_valid ? s_data : m_data;
          m_valid <= s_valid ? 1'b1 : (m_ready ? 1'b0 : m_valid);
        end
      end
      // when output is consumed and a skid beat waits, promote it
      if (m_valid && m_ready) begin
        if (skid_valid) begin
          m_data <= skid_data; m_valid <= 1'b1; skid_valid <= 1'b0;
        end else if (!s_ready || !s_valid) begin
          m_valid <= 1'b0;
        end
      end
    end
  end
endmodule

(The exact coding varies — many equivalent forms exist — but the invariant is the same: s_ready = !skid_valid, the skid register catches the overflow beat, and a consumed output promotes the skid beat.)

Upstream into main or skid register; skid promotes to main on consumption; main drives downstream output; s_ready when skid empty.Upstream beats_data/s_validMain registerdrives outputDownstreamm_data/m_validSkid registeroverflow slots_ready =!skid_validregistered backpressurePromoteskid → main on consume12
Figure 2 — the two-entry skid buffer datapath. The main register drives the output (m_data/m_valid); the skid register holds one overflow beat. Upstream is accepted (s_ready) whenever the skid is empty. When the output is consumed (m_valid && m_ready), a waiting skid beat is promoted into the main register. Two slots are exactly enough to fully register both directions without bubbles.

3. The State Machine

The skid buffer's occupancy is a tiny FSM over three states by fill level: EMPTY (no beat; s_ready=1, m_valid=0), ONE (main holds a beat; s_ready=1, m_valid=1 — still room in skid), and FULL (main + skid both hold beats; s_ready=0, m_valid=1). Throughput is full because in the common streaming case the buffer sits in ONE, accepting and emitting a beat every cycle.

Skid FSM: EMPTY to ONE on accept, ONE self-loop streaming, ONE to FULL on stall-with-incoming, FULL to ONE on consume.accept beatin & out(stream)stall + incomingdownstreamconsumesdrain, no inputEMPTYONEFULL
Figure 3 — the skid buffer occupancy FSM. EMPTY accepts a beat into main (→ ONE). ONE emits when downstream is ready and can simultaneously accept a new beat (self-loop = steady-state streaming); if downstream stalls while a new beat arrives, it fills the skid (→ FULL). FULL deasserts s_ready; when downstream consumes, the skid beat promotes to main (→ ONE). The steady state is ONE with a self-loop — one in, one out, every cycle.

4. The Timing: Full Throughput Through a Stall

The waveform shows the payoff: a stream of beats flows one-per-cycle; when the downstream drops m_ready for a cycle, the skid register catches the in-flight beat so no data is lost and no upstream bubble is created — when m_ready returns, the skid beat is emitted and streaming resumes at full rate.

Skid buffer absorbing a one-cycle stall

9 cycles
Beats stream in and out one per cycle; downstream stalls one cycle, skid catches the beat, s_ready deasserts one cycle, then full-rate streaming resumes.stream 1/cycstall: skid catchesresume 1/cycm_ready lowm_ready lowskid emittedskid emittedCLKs_valids_readys_data.A B C D m_validm_readym_data..A B C Ct0t1t2t3t4t5t6t7t8
Figure 4 — full throughput across a downstream stall. Beats stream in (s_valid/s_ready) and out (m_valid/m_ready) one per cycle. At cycle 4 the downstream drops m_ready; the buffer goes FULL and the in-flight beat lands in the skid register — s_ready deasserts for exactly one cycle to hold the upstream. When m_ready returns (cycle 5), the skid beat is emitted and the pipeline resumes at one beat per cycle. No beat is dropped; the only backpressure is the single registered s_ready pulse.

5. Common Misconceptions

6. Debugging Insight

7. Verification Insight

8. Interview Questions

9. Summary

A skid buffer fully registers an AXI VALID/READY handshake — flopping both the forward (VALID/data) and backward (READY) directions — to close timing, while sustaining one beat per cycle. The key insight is that registering the backpressure makes the upstream see READY one cycle late, so a beat can be in flight when the buffer fills; a single register can't hold it (forcing data loss or a half-throughput bubble), so a second register — the skid/overflow slot — catches exactly that one in-flight beat. The canonical design sets s_ready = !skid_valid (registered backpressure: accept while the overflow slot is free), drives the output from a main register, and promotes the skid beat into main when the output is consumed. Occupancy is a tiny EMPTY/ONE/FULL FSM whose steady state is ONE with a self-loop (one in, one out, every cycle).

The cost is one cycle of latency and two registers; throughput is untouched. Two entries are provably the exact minimum for a fully-registered, full-throughput handshake. Verification centers on the throughput-through-a-stall test (continuous input, m_ready toggled) plus formal data-conservation/ordering and payload-stability properties — small enough to prove exhaustively. The skid buffer is the most-reused AXI primitive: interconnect ports, FSM outputs, FIFO front-ends, and pipeline stages all rely on it. Next, we generalize it into pipelining a handshaked path across multiple stages.

10. What Comes Next

You've built the timing-closure workhorse; next we use it to pipeline a handshaked path:

  • 15.6 — Ready/Valid Pipelining (coming next) — chaining registered handshakes into a multi-stage pipeline without losing data or throughput, the general form of what the skid buffer does for one stage.

Previous: 15.4 — AXI Read FSM. Related: 3.1 — The VALID/READY Handshake for the contract the skid buffer registers, 3.3 — Backpressure & Stalls for the stall behavior it absorbs, and 3.5 — Handshake Dependency & Deadlock Rules for why VALID must not depend on READY.