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 orderThe log shows all 20 beats delivered strictly in order (0,1,2,...,19) even though the consumer asserts
m_readyonly two cycles out of three. That proves the skid slot absorbs the in-flight beat during each stall so nothing is lost, andALL TESTS PASSEDconfirms no drop, no duplicate, and correct ordering under back-pressure.A simpler variant: a single-entry register slice driven by a registered
fullflag. It still breaks the combinationalreadypath (s_readycomes straight off a flop), but trades the skid slot away for at most 50% throughput. The testbench uses a non-trivial golden stream3i+7and 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 endmodulePASS: 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 lossEvery one of the 24 beats emerges matching the golden value
3i+7in 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 whilefullis set, which is why no beat is overwritten.Why a combinational ready path is a timing problem
In a
valid/readypipeline, the simplest glue logic makes each stage'ss_readydepend combinationally on its downstreamm_ready. Chain several stages and thereadysignal 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
readythat 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
validandreadyare 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 holdvalidand 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_readyon 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
readytiming 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
fullflag. Becauses_readyfalls 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_readycombinational inm_ready. That defeats the whole purpose — you have not actually broken the path. In the skid buffer,s_readydepends only on the registeredskid_validflag.- 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 lowersvalidbefore acceptance violates the contract and will desync any downstream buffer.- Accepting while full. In the single-entry slice, new data must be refused whenever
fullis set, or the stored beat is overwritten and lost.Synthesis and integration notes
The skid buffer costs
2×WIDTHdata 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. ParameterizeWIDTHso the same module serves any payload, and instantiate one slice wherever a longvalid/readylink 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
readypath 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 LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
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 Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres