In a valid/ready chain, in_ready = !out_valid || out_ready makes ready travel backwards through every stage combinationally. A 10-stage chain has a 10-gate-deep ready path.
A skid buffer (a two-slot register slice here) cuts this dependency while preserving the handshake contract. This implementation derives in_ready = !skid_v from registered occupancy: it advertises a spare slot, rather than delaying a copy of out_ready. Ordinary register movement alone is not a substitute for designing and checking this protocol boundary.
When main is blocked and skid is empty, in_ready remains high because the skid can accept one more item. After that acceptance fills skid, in_ready drops. If main leaves while skid is full, skid moves to main; upstream sees ready return after that edge. See the primary Pipeline Skid Buffer derivation for the two-slot capacity argument.
Read it from the left: data enters on the top path, and the skid register is the side pocket below it.
In normal flow the item goes straight through the mux into the main register.
When main is blocked, the next accepted item is parked in the skid register.
in_ready is driven by the skid_v flag alone, which is a register output.
out_ready controls the mux and the registers but has no path to in_ready; this is what cuts the ready chain.
Edge / state before edge | Source / out_ready | in_ready / out_valid | Accepted / delivered | State after edge |
|---|---|---|---|---|
0: empty | A=0 / 0 | 1 / 0 | A / none | main=A, skid empty |
1: main=A | B=1 / 0 | 1 / 1 | B / none | main=A, skid=B |
2: main=A, skid=B | C=2 / 0 | 0 / 1 | none / none | Both hold; source retains C |
3: main=A, skid=B | C=2 / 1 | 0 / 1 | none / A | main=B, skid empty; in_ready rises after edge |
4: main=B | C=2 / 1 | 1 / 1 | C / B | main=C, skid empty |
5: main=C | none / 1 | 1 / 1 | none / C | Empty |
Each slot is one clock edge and shows the values that edge sees, so slot k matches table row k. Slot 6 is the empty state after the last edge. The data lanes show a value only while their valid is high.
in_ready stays high through slot 1 even though out_ready is low, because the skid slot is free.
in_ready falls in slot 2, after B parked in the skid register, and stays low in slot 3 while A leaves.
C is offered in slots 2, 3 and 4 and only accepted at edge 4, when in_ready is high again.
Deliveries at edges 3, 4 and 5 are consecutive: the output carries A, B, then C with no gap.
A and B occupy the two available slots, so C cannot be accepted at edge 3 even though A departs then: ready depends only on the old skid state. Deliveries at edges 3, 4 and 5 remain consecutive. Sustained unstalled flow supports one item per cycle, but recovery from full occupancy can pause input acceptance for a cycle. The directed check below follows these exact transitions before random stalls.
A skid buffer doubles the registers but cuts the ready chain. Throughput stays at one item per cycle.
Plain stage | Skid buffer | |
|---|---|---|
|
| A register ( |
Registers | 1 data + valid | 2 data + 2 valid |
Throughput | 1 item / cycle | 1 item / cycle |
Timing | Ready chain grows with depth | Chain is cut at every skid buffer |
Only the main register. When main is blocked, the spare skid slot catches one further accepted item. It moves into main when main is consumed; ready then returns from the updated occupancy.
The skid buffer plus a testbench with random stalls, a scoreboard and input/output stability monitors. Shutdown preserves a pending source offer, then drains with the sink continuously ready.
module skid_buffer #(parameter int W = 8) (
input logic clk, rst_n,
input logic in_valid, output logic in_ready, input logic [W-1:0] in_data,
output logic out_valid, input logic out_ready, output logic [W-1:0] out_data
);
// main output register
logic main_v; logic [W-1:0] main_d;
// skid register: holds the one item accepted just after downstream stalled
logic skid_v; logic [W-1:0] skid_d;
assign in_ready = !skid_v; // depends ONLY on a register, not on out_ready
assign out_valid = main_v;
assign out_data = main_d;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin main_v <= 0; skid_v <= 0; end
else begin
if (!skid_v) begin
// normal operation: take input if main is free or leaving
if (!main_v || out_ready) begin
main_v <= in_valid;
end else if (in_valid) begin
skid_v <= 1; // downstream stalled: park the new item
end
end else if (out_ready) begin
// skid occupied and main leaves: skid moves into main
main_v <= 1;
skid_v <= 0;
end
end
always_ff @(posedge clk)
if (!skid_v) begin
if (in_valid && (!main_v || out_ready)) main_d <= in_data;
else if (in_valid) skid_d <= in_data;
end else if (out_ready) main_d <= skid_d;
endmodule