An interviewer draws a pipeline stage with a registered ready output and asks how many entries of storage it needs. One candidate says one: it is a pipeline register. Another says it depends on how long the stall lasts. The interviewer's answer is two, regardless of how long the stall lasts, and the candidates are asked to explain why.
Why does registering ready need exactly one extra entry, and how does a skid buffer use it?
The obvious stage with a registered ready has one data register, and drives ready from a flip-flop that says "I'm not full". Walk through a stall. In cycle t the stage is full and its output is blocked. It decides "I'm full" and registers ready = 0, but that only appears at its input in cycle t + 1. During cycle t the sender still sees the old ready = 1 and sends a word. The stage has nowhere to put it. A one-entry stage with a registered ready loses a word on every stall.
The word that arrives after the stall began is the only one that can arrive, because from the next cycle on the sender sees ready = 0. So exactly one extra entry is needed, however long the stall lasts: the sender's view of ready is late by one cycle, never more. That spare entry is the skid register: the stage "skids" one word past the point where it wanted to stop.
Normal flow: the spare entry is empty, ready = 1, words pass straight into the output register.
Stall begins: the output is blocked; a word arrives anyway (the sender has not seen the stall yet). It goes into the spare entry, and ready drops to 0 from the next cycle.
Stall ends: the output register's word leaves, the spare entry's word moves into the output register, the spare entry empties, and ready returns to 1. The output never goes empty, so no cycle is lost.
Because ready now comes straight from a flip-flop (the inverse of "spare entry full"), the long combinational ready path through the stage is cut. Together with the registered output, the skid buffer cuts every path: it is the full register slice of lesson 3.8.
The listing implements the skid buffer in its usual form: in_rdy is the inverse of the spare entry's valid bit. The testbench feeds it from a source that always has data and drains it into a sink that stalls in a fixed pattern. The checker requires every word in order, none lost or repeated, and a word out in every cycle the sink is ready: full throughput.
// A two-entry skid buffer: both the forward path (valid, data) and the backward path
// (ready) are registered. Because in_rdy is a register, the source learns about a
// stall one cycle late, so one more word can arrive after the output stalls. The
// second entry (the skid register) catches it. Full throughput when the sink is
// ready; no word lost or duplicated when it stalls.
module skid_buffer (
input logic clk, rst_n,
input logic in_vld, output logic in_rdy, input logic [7:0] in_data,
output logic out_vld, input logic out_rdy, output logic [7:0] out_data
);
logic skid_vld; // the spare entry holds a word
logic [7:0] skid_data;
assign in_rdy = !skid_vld; // straight from a flip-flop: the backward path is cut
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
out_vld <= 0; skid_vld <= 0;
end else if (in_rdy) begin // spare entry empty: we may accept a word
if (out_vld && !out_rdy) begin // output stalled: a word that arrives goes to the spare entry
if (in_vld) begin skid_vld <= 1; skid_data <= in_data; end
end else begin // output empty or moving on: the word goes straight to the output
out_vld <= in_vld; out_data <= in_data;
end
end else if (out_rdy) begin // spare entry full and the output moves on: refill from it
out_data <= skid_data; skid_vld <= 0; // out_vld stays 1
end
end
endmodule
module tb;
logic clk = 0;
always #5 clk = ~clk;
logic rst_n = 0;
initial begin
$dumpfile("wave.vcd"); $dumpvars(0, tb);
#12 rst_n = 1;
end
int cyc = 0;
always @(posedge clk) if (rst_n) cyc <= cyc + 1;
logic [7:0] next_word = 0;
wire in_vld = 1;
wire in_rdy, out_vld;
wire [7:0] out_data;
localparam logic [31:0] RDY = 32'b1111_1111_0111_1100_1111_1101_1111_1111;
wire out_rdy = RDY[cyc[4:0]];
skid_buffer u (.clk, .rst_n, .in_vld, .in_rdy, .in_data(next_word), .out_vld, .out_rdy, .out_data);
always @(posedge clk) if (rst_n && in_vld && in_rdy) next_word <= next_word + 1;
wire skid_vld = u.skid_vld; // copy for the waveform
logic [7:0] expect_w = 0; int errors = 0, moved = 0, ready_cycles = 0;
always @(posedge clk) if (rst_n) begin
if (cyc >= 1 && out_rdy) ready_cycles++;
if (out_vld && out_rdy) begin
if (out_data != expect_w) errors++;
expect_w++; moved++;
end
end
initial begin
#340;
if (errors == 0 && moved == ready_cycles)
$display("PASS: %0d words out in order, one in every cycle the sink was ready, with ready registered on both sides", moved);
else $display("FAIL: errors %0d, moved %0d of %0d ready cycles", errors, moved, ready_cycles);
$finish;
end
endmoduleHow to read it, slot by slot:
Slots 0 and 1: words 6 and 7 flow through, one per cycle; the spare entry is empty (skid_vld = 0) and in_rdy = 1.
Slot 2: the sink stalls (out_rdy = 0) with word 8 on the output. The sender still sees in_rdy = 1, so word 9 arrives anyway.
Slot 3: word 9 is in the spare entry (skid_vld = 1), and in_rdy is now 0, so nothing more arrives. Word 8 is still on the output, and leaves at the end of this slot.
Slot 4: word 9 moves from the spare entry to the output, the spare entry empties, and in_rdy returns to 1. out_vld never dropped: no bubble.
Slots 9 to 12: a two-cycle stall. The spare entry fills once (slot 10) and stays full while the stall lasts; in_rdy stays low; word e holds on the output until the sink takes it, and word f follows from the spare entry in slot 12. Two stall cycles, still one spare entry.
Stage | Entries | Ready path | Throughput | Risk |
|---|---|---|---|---|
One entry, ready combinational (forward slice) | 1 | Not cut | Full | Long ready chain through many stages |
One entry, ready = !full (registered by nature) | 1 | Cut | Half (lesson 3.10) | Throughput loss |
One entry, ready through a flip-flop, no spare | 1 | Cut | Full until a stall | Loses one word per stall |
Skid buffer | 2 | Cut | Full | None, at twice the storage |
Deeper FIFO | N | Cut | Full | More area; only needed when ready arrives more than one cycle late |
The last row generalises the lesson. If the sender learns of a stall k cycles late, it can send k extra words, so the stage needs k spare entries. A skid buffer is the k = 1 case. Part 4 turns this into credit flow control, where k is a whole round trip.
Where | Choice |
|---|---|
AXI register slices on long routes | The full register slice is a skid buffer: it registers VALID, data and READY, with two entries per channel. |
DCLink (this course) | Step 2b places a skid buffer between sender and receiver; Part 4 replaces it with credits when the path gets long. |
A skid buffer is built with the spare entry but with in_rdy computed from a condition that also looks ahead at the sink (illustrative): it drops ready one cycle too early "to be safe". Nothing is lost, but every stall now costs an extra idle cycle on the output, and a link with frequent short stalls loses several percent of its throughput. The check that catches it is the one in the listing's testbench: a word must leave in every cycle the sink is ready. Verifying only order and no loss would pass this design.
After the first stall cycle, in_rdy is low, so the sender sends nothing more. Only the one word sent before it saw ready fall needs the spare entry.
A bubble after each stall (the output goes empty for a cycle). Add a check that a word leaves in every cycle the sink is ready.
Three: one output entry plus two spare entries for the two words sent before the sender sees ready fall.
A registered ready reaches the sender one cycle late, so exactly one extra word arrives after a stall begins.
A skid buffer's spare entry catches that word; ready comes from a flip-flop, so all paths are cut and throughput stays full.
In general, k cycles of ready lag need k spare entries: the root of credit sizing in Part 4.