Skid Buffer in Verilog: Valid-Ready Pipeline Register with Code

A skid buffer is a two-entry pipeline register that registers a valid/ready handshake (the same contract used by AXI-Stream). It lets you insert a flop on both the data path and the ready path between a producer and a consumer without losing a single data beat when the consumer back-pressures. Because it keeps a spare “skid” slot for the beat already in flight, it sustains full throughput: one transfer per cycle even while cutting the long combinational ready chain.

This page gives you runnable, simulator-verified skid buffer Verilog code: a width-parameterized skid buffer and a simpler one-entry register slice, each with a self-checking testbench that streams data through downstream stalls and proves every beat arrives exactly once, in order.

The standard skid buffer: a registered output stage plus a one-deep “skid” register that catches the beat still being offered on the cycle the consumer stalls. The testbench streams 20 beats through a repeating ready/stall pattern and checks each output against a golden counter, so a dropped, duplicated, or reordered beat prints FAIL.

// Width-parameterized skid buffer (valid/ready AXI-Stream register slice)
module skid_buffer #(
    parameter WIDTH = 8
) (
    input  wire              clk,
    input  wire              rst,
    // upstream (slave) side
    input  wire              s_valid,
    input  wire [WIDTH-1:0]  s_data,
    output wire              s_ready,
    // downstream (master) side
    output reg               m_valid,
    output reg  [WIDTH-1:0]  m_data,
    input  wire              m_ready
);
    // skid storage holds one extra beat when downstream stalls
    reg              skid_valid;
    reg [WIDTH-1:0]  skid_data;

    // Ready to accept new input while the skid slot is empty.
    assign s_ready = ~skid_valid;

    wire s_fire = s_valid & s_ready;   // input beat accepted
    wire m_fire = m_valid & m_ready;   // output beat leaves

    always @(posedge clk) begin
        if (rst) begin
            m_valid    <= 1'b0;
            m_data     <= {WIDTH{1'b0}};
            skid_valid <= 1'b0;
            skid_data  <= {WIDTH{1'b0}};
        end else begin
            // Output register loads a new beat when it drains or is empty.
            if (m_fire || !m_valid) begin
                if (skid_valid) begin
                    // Prefer the buffered (older) beat to preserve order.
                    m_valid    <= 1'b1;
                    m_data     <= skid_data;
                    skid_valid <= 1'b0;
                end else if (s_fire) begin
                    m_valid <= 1'b1;
                    m_data  <= s_data;
                end else begin
                    m_valid <= 1'b0;
                end
            end else if (s_fire) begin
                // Output stalled and full: park the new beat in the skid slot.
                skid_valid <= 1'b1;
                skid_data  <= s_data;
            end
        end
    end
endmodule

module tb;
    localparam WIDTH = 8;
    localparam N     = 20;

    reg              clk = 1'b0;
    reg              rst = 1'b1;
    reg              s_valid = 1'b0;
    reg  [WIDTH-1:0] s_data  = 8'd0;
    wire             s_ready;
    wire             m_valid;
    wire [WIDTH-1:0] m_data;
    reg              m_ready = 1'b0;

    skid_buffer #(.WIDTH(WIDTH)) dut (
        .clk(clk), .rst(rst),
        .s_valid(s_valid), .s_data(s_data), .s_ready(s_ready),
        .m_valid(m_valid), .m_data(m_data), .m_ready(m_ready)
    );

    always #5 clk = ~clk;

    integer in_idx;   // next input value to present / count accepted
    integer out_idx;  // next expected output value (golden model)
    integer cyc;      // free-running cycle counter for stall pattern
    integer errors;

    // Drive stimulus on the negative edge so it is stable at each posedge.
    always @(negedge clk) begin
        if (rst) begin
            s_valid <= 1'b0;
            m_ready <= 1'b0;
        end else begin
            s_valid <= (in_idx < N);
            s_data  <= in_idx[WIDTH-1:0];
            // Deterministic back-pressure: accept 2 cycles, stall 1.
            m_ready <= ((cyc % 3) != 2);
        end
    end

    // Monitor / golden check on the positive edge.
    always @(posedge clk) begin
        if (!rst) begin
            cyc <= cyc + 1;
            if (s_valid && s_ready && (in_idx < N))
                in_idx <= in_idx + 1;
            if (m_valid && m_ready) begin
                if (m_data !== out_idx[WIDTH-1:0]) begin
                    $display("FAIL: output beat %0d expected %0d got %0d",
                             out_idx, out_idx, m_data);
                    errors <= errors + 1;
                end else begin
                    $display("PASS: beat %0d delivered in order (data=%0d)",
                             out_idx, m_data);
                end
                if (out_idx == N-1) begin
                    if (errors == 0)
                        $display("ALL TESTS PASSED: %0d beats, no drop, no duplicate, in order", N);
                    else
                        $display("FAIL: errors=%0d", errors);
                    $finish;
                end
                out_idx <= out_idx + 1;
            end
        end
    end

    initial begin
        in_idx = 0; out_idx = 0; cyc = 0; errors = 0;
        @(negedge clk); @(negedge clk);
        rst = 1'b0;
        #5000;
        $display("FAIL: timeout, delivered=%0d", out_idx);
        $finish;
    end
endmodule
PASS: beat 0 delivered in order (data=0)
PASS: beat 1 delivered in order (data=1)
PASS: beat 2 delivered in order (data=2)
PASS: beat 3 delivered in order (data=3)
PASS: beat 4 delivered in order (data=4)
PASS: beat 5 delivered in order (data=5)
PASS: beat 6 delivered in order (data=6)
PASS: beat 7 delivered in order (data=7)
PASS: beat 8 delivered in order (data=8)
PASS: beat 9 delivered in order (data=9)
PASS: beat 10 delivered in order (data=10)
PASS: beat 11 delivered in order (data=11)
PASS: beat 12 delivered in order (data=12)
PASS: beat 13 delivered in order (data=13)
PASS: beat 14 delivered in order (data=14)
PASS: beat 15 delivered in order (data=15)
PASS: beat 16 delivered in order (data=16)
PASS: beat 17 delivered in order (data=17)
PASS: beat 18 delivered in order (data=18)
PASS: beat 19 delivered in order (data=19)
ALL TESTS PASSED: 20 beats, no drop, no duplicate, in order

The log shows all 20 beats delivered strictly in order (0,1,2,...,19) even though the consumer asserts m_ready only two cycles out of three. That proves the skid slot absorbs the in-flight beat during each stall so nothing is lost, and ALL TESTS PASSED confirms no drop, no duplicate, and correct ordering under back-pressure.

A simpler variant: a single-entry register slice driven by a registered full flag. It still breaks the combinational ready path (s_ready comes straight off a flop), but trades the skid slot away for at most 50% throughput. The testbench uses a non-trivial golden stream 3i+7 and heavier, uneven stalls on both sides.

// Single-entry register slice using a registered fullness flag.
// Breaks the combinational ready path (s_ready depends only on a flop),
// at the cost of at most 50% throughput (one bubble between beats).
module reg_slice #(
    parameter WIDTH = 8
) (
    input  wire              clk,
    input  wire              rst,
    input  wire              s_valid,
    input  wire [WIDTH-1:0]  s_data,
    output wire              s_ready,
    output wire              m_valid,
    output wire [WIDTH-1:0]  m_data,
    input  wire              m_ready
);
    reg              full;
    reg [WIDTH-1:0]  data;

    assign s_ready = ~full;        // registered flag -> no comb path from m_ready
    assign m_valid = full;
    assign m_data  = data;

    wire accept = s_valid & ~full; // can only load when empty
    wire emit   = full    & m_ready;

    always @(posedge clk) begin
        if (rst) begin
            full <= 1'b0;
            data <= {WIDTH{1'b0}};
        end else begin
            if (accept) begin
                full <= 1'b1;
                data <= s_data;
            end else if (emit) begin
                full <= 1'b0;
            end
        end
    end
endmodule

module tb;
    localparam WIDTH = 8;
    localparam N     = 24;

    reg              clk = 1'b0;
    reg              rst = 1'b1;
    reg              s_valid = 1'b0;
    reg  [WIDTH-1:0] s_data  = 8'd0;
    wire             s_ready;
    wire             m_valid;
    wire [WIDTH-1:0] m_data;
    reg              m_ready = 1'b0;

    reg_slice #(.WIDTH(WIDTH)) dut (
        .clk(clk), .rst(rst),
        .s_valid(s_valid), .s_data(s_data), .s_ready(s_ready),
        .m_valid(m_valid), .m_data(m_data), .m_ready(m_ready)
    );

    always #5 clk = ~clk;

    integer in_idx, out_idx, cyc, errors;

    // Golden-model data stream = 3*i+7 (mod 256) to catch mis-ordering.
    function [WIDTH-1:0] expval;
        input integer i;
        begin
            expval = (3*i + 7);
        end
    endfunction

    always @(negedge clk) begin
        if (rst) begin
            s_valid <= 1'b0;
            m_ready <= 1'b0;
        end else begin
            // Upstream almost always offering data; pause input every 4th cycle.
            s_valid <= (in_idx < N) && ((cyc % 4) != 3);
            s_data  <= expval(in_idx);
            // Aggressive downstream stall: ready only 1 cycle in 2, plus a gap.
            m_ready <= ((cyc % 2) == 0) && ((cyc % 5) != 4);
        end
    end

    always @(posedge clk) begin
        if (!rst) begin
            cyc <= cyc + 1;
            if (s_valid && s_ready && (in_idx < N))
                in_idx <= in_idx + 1;
            if (m_valid && m_ready) begin
                if (m_data !== expval(out_idx)) begin
                    $display("FAIL: beat %0d expected %0d got %0d",
                             out_idx, expval(out_idx), m_data);
                    errors <= errors + 1;
                end else begin
                    $display("PASS: beat %0d ok (data=%0d)", out_idx, m_data);
                end
                if (out_idx == N-1) begin
                    if (errors == 0)
                        $display("ALL TESTS PASSED: %0d beats through register slice, in order, no loss", N);
                    else
                        $display("FAIL: errors=%0d", errors);
                    $finish;
                end
                out_idx <= out_idx + 1;
            end
        end
    end

    initial begin
        in_idx = 0; out_idx = 0; cyc = 0; errors = 0;
        @(negedge clk); @(negedge clk);
        rst = 1'b0;
        #8000;
        $display("FAIL: timeout, delivered=%0d", out_idx);
        $finish;
    end
endmodule
PASS: beat 0 ok (data=7)
PASS: beat 1 ok (data=10)
PASS: beat 2 ok (data=13)
PASS: beat 3 ok (data=16)
PASS: beat 4 ok (data=19)
PASS: beat 5 ok (data=22)
PASS: beat 6 ok (data=25)
PASS: beat 7 ok (data=28)
PASS: beat 8 ok (data=31)
PASS: beat 9 ok (data=34)
PASS: beat 10 ok (data=37)
PASS: beat 11 ok (data=40)
PASS: beat 12 ok (data=43)
PASS: beat 13 ok (data=46)
PASS: beat 14 ok (data=49)
PASS: beat 15 ok (data=52)
PASS: beat 16 ok (data=55)
PASS: beat 17 ok (data=58)
PASS: beat 18 ok (data=61)
PASS: beat 19 ok (data=64)
PASS: beat 20 ok (data=67)
PASS: beat 21 ok (data=70)
PASS: beat 22 ok (data=73)
PASS: beat 23 ok (data=76)
ALL TESTS PASSED: 24 beats through register slice, in order, no loss

Every one of the 24 beats emerges matching the golden value 3i+7 in order, confirming the slice never loses or reorders data even when upstream pauses and downstream stalls irregularly. The pass also implicitly shows the design correctly refuses new input while full is set, which is why no beat is overwritten.

Why a combinational ready path is a timing problem

In a valid/ready pipeline, the simplest glue logic makes each stage's s_ready depend combinationally on its downstream m_ready. Chain several stages and the ready signal becomes one long combinational path that ripples backward from the final consumer to the first producer in a single cycle. That path has no flop to break it, so its delay grows with pipeline depth and quickly becomes the critical path, capping your clock frequency.

A register slice (of which the skid buffer is the full-throughput form) inserts a flip-flop into that path. The producer now sees a ready that is driven by a local register instead of by logic reaching all the way downstream, so the timing arc is cut at every slice you insert.

The valid/ready contract the skid buffer must honor

The handshake has three rules, and a correct skid buffer never breaks them:

  • Transfer on both high. A beat moves only on a cycle where valid and ready are both asserted.
  • No drop, no duplicate. Every accepted input beat must appear on the output exactly once, in the same order.
  • Data stable while stalled. Once a source asserts valid, it must hold valid and the data payload steady until the sink accepts it; the source may not retract an offer.

How the skid buffer upholds them

The producer is allowed to present a new beat in the same cycle the output register is stalled. Without storage that beat would be lost. The skid register captures it, and the buffer de-asserts s_ready on the next cycle so no further beat is accepted until the slot drains. When the output finally moves, the buffer drains the skid beat first, preserving order.

Skid buffer vs simple register slice: throughput

Both designs break the ready timing path; they differ in bandwidth.

  • Skid buffer (Example 1): two storage elements (output register + skid). It can accept a beat every cycle while also emitting a beat every cycle, so it sustains 100% throughput — one transfer per clock when both sides keep up.
  • One-entry register slice (Example 2): a single storage element gated by a registered full flag. Because s_ready falls for a cycle after each accept, it inserts a bubble and tops out near 50% throughput.

Rule of thumb: use a skid buffer when the link is throughput-critical and you cannot afford bubbles; use the cheaper half-rate slice when area matters more than bandwidth and the data rate already has headroom.

Common mistakes

  • Making s_ready combinational in m_ready. That defeats the whole purpose — you have not actually broken the path. In the skid buffer, s_ready depends only on the registered skid_valid flag.
  • Dropping the in-flight beat. If you stall without a place to put the beat the producer is offering that cycle, it is lost. The skid register exists precisely for this one beat.
  • Draining the new beat before the buffered one. Always emit the older (skid) beat first, or you reorder the stream.
  • Retracting valid. A source that lowers valid before acceptance violates the contract and will desync any downstream buffer.
  • Accepting while full. In the single-entry slice, new data must be refused whenever full is set, or the stored beat is overwritten and lost.

Synthesis and integration notes

The skid buffer costs 2×WIDTH data flops plus two control flops, which is cheap relative to the timing it buys. Keep the module purely synchronous with a single clock and a synchronous reset as shown; avoid any latch by making sure every register is assigned in the clocked block. Parameterize WIDTH so the same module serves any payload, and instantiate one slice wherever a long valid/ready link crosses a floorplan boundary or a slow interconnect span.

FAQ

Is a skid buffer the same as an AXI-Stream register slice? Yes — the full-throughput register slice shipped in many AXI libraries is a skid buffer; the signal names map to TVALID/TREADY/TDATA.

Does it add latency? It adds one cycle of pipeline latency per slice, but does not reduce throughput in the two-entry form.

Can I register only the forward (data/valid) path? Yes, but a forward-only slice still leaves the ready path combinational. To break both directions with full throughput you need the skid (two-entry) design.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD