A registered stage learns about backpressure one cycle late. Downstream deasserts ready, but the beat already leaving upstream has nowhere to go.
A naive one-deep skid clears its hold flag on any pop. When a pop coincides with a push, the incoming beat has nowhere to go and is dropped. The design below makes that case impossible: its registered ready is low whenever both registers are full, so a push can never coincide with a full buffer.
Two registers hold at most two beats: an output register and a skid register. A registered ready reports not-full, so no combinational path runs from downstream ready to upstream ready.
A push into a busy output lands in the skid. A pop with a full skid moves the held beat to the output. A pop that coincides with a push passes the new beat straight into the output, with no bubble. The full listing and testbench sit under Testbench below.
Each slot is one clock cycle. Upstream lanes (valid, ready, data) and downstream lanes (m_valid, m_ready, m_data) follow one rule: a beat moves at the rising edge that ends a slot in which its valid and ready are both high.
Slot 0: 0xA1 is offered with ready 1, so it is pushed into the output register at the end of the slot.
Slot 1: 0xA1 is on the output, but downstream stalls (m_ready 0). 0xB2 is offered with ready still 1, so it is pushed into the skid register.
Slot 2: both registers are full, so the registered ready is 0. The new offer 0xC3 must stay put.
Slot 3: the stall holds; 0xC3 stays stable and no beat is lost.
Slot 4: downstream resumes (m_ready 1), so 0xA1 pops at the end of the slot and the held 0xB2 moves to the output.
Slot 5: ready is 1 again. 0xB2 pops while 0xC3 is pushed in the same edge, so 0xC3 goes straight to the output with no bubble.
Slot 6: no new offer; 0xC3 pops.
Stage | Ready path | Throughput under backpressure | Cost |
|---|---|---|---|
Pass-through register | Combinational: | One beat per cycle | One data register; the chained ready path sets the timing limit. |
Plain registered stage | Registered | Half rate: it cannot take a beat while it holds one, so it accepts every other cycle even with no stall (measured 0.50 in the comparison lesson) | One data register plus control. |
Skid buffer | Registered | One beat per cycle; stalls are absorbed | Two data registers plus control. |
Into the skid register. Upstream hears about it one cycle later, when the registered ready falls, and that one cycle of lag is exactly what the extra register covers.
The new beat passes straight into the output, the old one leaves, and ready stays high. No bubble and no skid detour.
The testbench covers simultaneous pop/push, random backpressure, and directed shutdown with both slots full and an upstream offer blocked. Shutdown stops generating new offers, accepts the pending one, then drains within a bound. Input/output stability and accepted-minus-delivered occupancy are checked.
module skid_buffer #(
parameter int DW = 8
) (
input logic clk,
input logic rst_n,
input logic s_valid_i,
output logic s_ready_o,
input logic [DW-1:0] s_data_i,
output logic m_valid_o,
input logic m_ready_i,
output logic [DW-1:0] m_data_o
);
logic [DW-1:0] out_data, skid_data;
logic out_valid, skid_valid, ready_q;
logic next_out_valid, next_skid_valid;
logic [DW-1:0] next_out_data, next_skid_data;
wire push, pop;
assign m_valid_o = out_valid;
assign m_data_o = out_data;
assign s_ready_o = ready_q; // registered: no combinational m_ready_i to s_ready_o path
assign push = s_valid_i && ready_q; // upstream hands a beat over
assign pop = out_valid && m_ready_i; // downstream takes the output beat
always_comb begin
next_out_valid = out_valid;
next_skid_valid = skid_valid;
next_out_data = out_data;
next_skid_data = skid_data;
if (pop) begin
if (skid_valid) begin
next_out_data = skid_data; // held beat moves to the output
if (push) next_skid_data = s_data_i; // unreachable with registered ready, kept for safety
else next_skid_valid = 1'b0;
end else if (push) begin
next_out_data = s_data_i; // pass straight through, no bubble
next_out_valid = 1'b1;
end else begin
next_out_valid = 1'b0; // draining
end
end else if (push) begin
if (!out_valid) begin
next_out_data = s_data_i; // output empty: fill it directly
next_out_valid = 1'b1;
end else begin
next_skid_data = s_data_i; // output busy: absorb into the skid
next_skid_valid = 1'b1;
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
out_valid <= 1'b0;
skid_valid <= 1'b0;
ready_q <= 1'b1;
out_data <= '0;
skid_data <= '0;
end else begin
out_valid <= next_out_valid;
skid_valid <= next_skid_valid;
out_data <= next_out_data;
skid_data <= next_skid_data;
ready_q <= !(next_out_valid && next_skid_valid);
end
end
endmodule
Source checker, run with Icarus Verilog 13.0 (2026-10-16): prints PASS. It covers simultaneous pop and push, random backpressure, and a directed shutdown with both slots full and an upstream offer blocked. Shutdown stops generating new offers, accepts the pending one, then drains within a bound; input and output stability and accepted-minus-delivered occupancy are checked. Timing closure and correctness for arbitrary traffic need separate evidence.