Stalls, backpressure and the skid buffer
Make a pipeline stage hold its item while the next stage is stalled, see why a registered ready loses a beat, and build the skid buffer that keeps it.
- Lesson
- R4.4
- Time
- 30 min
- Runs on
- Your browser
- Check
- 5 questions
A pipeline stage registered its ready output, because the combinational ready path had grown too long. The consumer stopped for one edge. The stage learned of the stop one edge late, took one more beat and wrote it over the beat that was waiting: beat 3 of 6 vanished.
out_ready for edge 5 only. Each column shows the values just before an edge, which decide what moves at it.- Cycle 4
Steady flow: beat 3 enters while beat 2 leaves. One beat per edge.
- Cycle 5
The consumer stops, so beat 3 must stay. But
in_readyis still 1, copied from the edge before, and beat 4 comes in. - Cycle 6
Beat 4 sits where beat 3 was. Beat 3 is gone. Only now does
in_readyshow the stall. - Cycle 9
Beats 1, 2, 4, 5 and 6 came out: six in, five out.
Measured on the lab bench. The consumer stopped for one edge; the stage heard about it one edge later.
Holding an item through a stall
A stall is the consumer saying "not this edge" by holding ready low. A stage that holds an item must keep it until the next stage takes it. It may take a new item only when it is empty or its own item leaves at the same edge. Backpressure is that rule running upstream, stage by stage.
The direct way writes the rule as logic: in_ready = out_ready || !out_valid. It loses nothing, because the stage sees the stall in the same cycle. But each stage's in_ready now depends on the next stage's, so the ready signal is one combinational path through the whole pipe.
| Stages in a chain | Combinational ready | Registered ready, no skid | Skid buffer |
|---|---|---|---|
| 1 | 2 | 2 | 4 |
| 2 | 3 | 2 | 4 |
| 4 | 5 | 2 | 4 |
| 8 | 9 | 2 | 4 |
Levels of logic on this flow for a chain of stages, measured with Yosys 0.70. The combinational ready path grows by one level per stage. Both registered versions stay flat.
In a real pipeline each stage has its own datapath logic too, and the ready chain runs beside it. Bus specifications forbid the problem outright: AXI allows no combinational path from an input to an output of an interface.
The skid buffer catches the late beat
Register ready and the long path is gone, but the hook's failure appears. The producer sees the stall one edge late, so exactly one beat arrives after the main register has filled. A skid buffer is a second register that catches it. The stage tells the producer it is ready only while the skid is empty.
in_ready and out_ready, which carry the ready flags. in_ready is !skid_valid, a flop output.- Normal flow
With the skid empty, a beat goes straight into the main register while the previous beat leaves.
Follow the calls, values and waits
- data beat 3: producer → main register
- The stall
The consumer drops
out_ready, so beat 3 stays in the main register.in_readyis still 1, and beat 4 arrives.Follow the calls, values and waits
- call out_ready 0: consumer → main register
- data beat 4: producer → skid register
- Ready falls
The skid is full, so
in_readyis 0 from the next edge. The producer holds beat 5.Follow the calls, values and waits
- call in_ready 0: skid register → producer
- Drain
The consumer takes beat 3. At the same edge beat 4 moves from the skid into the main register, so order is kept, and
in_readyreturns.Follow the calls, values and waits
- data beat 3: main register → consumer
- data beat 4: skid register → main register
Read the diagram as text
- producer: offers a beat until it is taken
- skid register: skid_valid, skid_data
- main register: out_valid, out_data
- consumer: drops out_ready to stall
- producer → main register: beat, normal flow
- producer → skid register: beat during a stall
- skid register → main register: drain
- main register → consumer: beat out
- consumer → main register: out_ready
- skid register → producer: in_ready = !skid_valid
The stage holds up to two beats, and no combinational path runs through it from out_ready to in_ready.
A lit wire carries a 1. A pill is a transfer in that cycle; slots are the entries held.
- Cycle 5
The consumer stops. Beat 3 stays in the main register and beat 4 comes in anyway: it goes to the skid.
- Cycle 6
The skid holds beat 4, so
in_readyis 0 and beat 5 waits. Beat 3 leaves; beat 4 moves up behind it. - Cycle 7
The skid is empty again and
in_readyis back. Beat 5 enters as beat 4 leaves. - Cycle 9
Six in, six out, in order. The stall cost the producer one edge.
Measured on the lab bench with the skid added (skid_valid and skid_data read from inside the stage).
Add the skid register
Registered ready, one register
in_ready <= out_ready; // one edge late
if (take) out_data <= in_data;
Beat 3 of 6 lost. 12 cells, 10 flops, 2 levels.
Registered ready, main and skid
assign in_ready = !skid_valid;
// main free: drain the skid first,
// else take the new beat
// main stalled: a new beat goes to the skid
Six of six, in order. 34 cells, 18 flops, 4 levels at any chain length.
The skid costs a second data register and a multiplexer in front of the main one. In exchange the stage has no combinational path from out_ready to in_ready, and it still passes one beat per edge in steady flow. AXI register slices and most bus pipelines are built from such stages. The Protocols lesson [P1.6](P1-6-protocol-flow-control-backpressure-credits.html) shows backpressure from the bus side.
logic skid_valid;
logic [7:0] skid_data;
assign in_ready = !skid_valid; // from a flop: ready while the skid is empty
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
out_valid <= 1'b0;
skid_valid <= 1'b0;
end else if (out_ready || !out_valid) begin // the main register can move
if (skid_valid) begin // drain the skid first
out_valid <= 1'b1;
skid_valid <= 1'b0;
end else begin
out_valid <= take;
end
end else if (take) begin // main is stalled: the beat lands in the skid
skid_valid <= 1'b1;
end
always_ff @(posedge clk)
if (out_ready || !out_valid) begin
if (skid_valid) out_data <= skid_data;
else if (take) out_data <= in_data;
end else if (take) begin
skid_data <= in_data;
end| Port | Direction | Width | Meaning |
|---|---|---|---|
| clk | input | 1 | clock |
| rst_n | input | 1 | reset, asynchronous, active-low |
| in_valid | input | 1 | the producer has a beat |
| in_data | input | [7:0] | the beat in; every beat taken at the input leaves at the output, once, in order |
| in_ready | output | 1 | comes from a flop: no combinational path from out_ready |
| out_valid | output | 1 | the stage has a beat |
| out_data | output | [7:0] | the beat out |
| out_ready | input | 1 | the consumer is ready |
- Throughput
- one beat per edge while the consumer is ready
- Corner cases
- a one-edge stall while beats stream in; a stall with the stage empty
The lab ships the stage without a skid. Run it: beats in 6, beats out 5, beat 3 missing. Replace the two always_ff blocks with the skid code above and run again: six out, none missing. Synthesize: 18 flops and 4 levels.
Asked in interviews
What is a skid buffer, and when do you need one?
A second register in a pipeline stage that catches the beat arriving after a stall began. You need it whenever ready is registered, because the producer learns of the stall one cycle late. The stage holds two beats.
What is backpressure?
A downstream block holding ready low so that upstream blocks keep their data. Each stage holds its item and stops accepting until its item moves on, so the stall spreads upstream stage by stage.
Why not make ready combinational?
Then each stage's ready depends on the next stage's, and one path runs from the last consumer through every stage. It grows with the pipeline: 3 levels for two stages, 9 for eight on this flow. A registered ready cuts it at each stage.
Try it
Each line shows what both sides offered just before the edge, then which beats moved at it. At e5 the consumer is not ready, so beat 3 should stay; in_ready is still 1 and beat 4 is taken over it. The summary names the beat that never came out.
With the skid stage, e5 shows in 4 again, but the beat lands in the skid. e6 shows in_ready=0 and out 3, and the summary reads beats out 6, missing none.
Synthesize the design to measure its cells and inspect the mapped gates.
e2 in_valid=1 in_data=1 in_ready=1 out_valid=0 out_data=x out_ready=1 in 1 e3 in_valid=1 in_data=2 in_ready=1 out_valid=1 out_data=1 out_ready=1 in 2 out 1 e4 in_valid=1 in_data=3 in_ready=1 out_valid=1 out_data=2 out_ready=1 in 3 out 2 e5 in_valid=1 in_data=4 in_ready=1 out_valid=1 out_data=3 out_ready=0 in 4 e6 in_valid=1 in_data=5 in_ready=0 out_valid=1 out_data=4 out_ready=1 out 4 e7 in_valid=1 in_data=5 in_ready=1 out_valid=0 out_data=4 out_ready=1 in 5 e8 in_valid=1 in_data=6 in_ready=1 out_valid=1 out_data=5 out_ready=1 in 6 out 5 e9 in_valid=0 in_data=7 in_ready=1 out_valid=1 out_data=6 out_ready=1 out 6 e10 in_valid=0 in_data=7 in_ready=1 out_valid=0 out_data=6 out_ready=1 beats in: 6, beats out: 5, missing: 3, out of order: 0Original design · 12 cells · 10 flip-flops · 0 latches · 2 logic levels. Synthesis uses Yosys; counts depend on the toolchain.
Read included file: tb.sv
`timescale 1ns/1ps
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
logic rst_n = 1; // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
int edge_n = -1;
always @(posedge clk) edge_n = edge_n + 1;
logic in_valid = 0, out_ready = 1;
logic [7:0] in_data = 0;
logic in_ready, out_valid;
logic [7:0] out_data;
int stall_at = 5; // the consumer holds out_ready low for this one edge
int n_in = 0, n_out = 0, last_out = 0, order = 0, e = -1;
bit seen [1:6];
// At each edge: what both sides offered just before it, and which beats moved.
always @(posedge clk) begin
string note;
e = e + 1;
note = "";
if (rst_n && in_valid && in_ready) begin
n_in++;
note = {note, $sformatf(" in %0d", in_data)};
end
if (rst_n && out_valid && out_ready) begin
n_out++;
note = {note, $sformatf(" out %0d", out_data)};
if (out_data <= last_out) order++;
if (out_data >= 1 && out_data <= 6) seen[out_data] = 1;
last_out = out_data;
end
if (e >= 2) $display("e%0d in_valid=%b in_data=%0d in_ready=%b out_valid=%b out_data=%0d out_ready=%b%s",
e, in_valid, in_data, in_ready, out_valid, out_data, out_ready, note);
end
stage dut (.clk(clk), .rst_n(rst_n), .in_valid(in_valid), .in_data(in_data), .in_ready(in_ready),
.out_valid(out_valid), .out_data(out_data), .out_ready(out_ready));
// Advance to the next rising edge, wait for the design to settle, then print one line.
task automatic tick();
@(posedge clk); #1;
// the lines are printed at each edge by the always block above
endtask
initial begin
#1 rst_n = 0; tick(); tick(); rst_n = 1; // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
// The producer offers beats 1 to 6, each until it is taken; the consumer is always
// ready except at one edge.
repeat (9) begin
in_valid = (n_in < 6);
in_data = n_in + 1;
out_ready = (e + 1 != stall_at);
tick();
end
begin
string missing;
missing = "";
for (int k = 1; k <= 6; k++) if (!seen[k]) missing = {missing, $sformatf(" %0d", k)};
if (missing == "") missing = " none";
$display("beats in: %0d, beats out: %0d, missing:%s, out of order: %0d", n_in, n_out, missing, order);
end
$finish(0); // quiet: no "$finish called" line
end
endmodule
Check yourself
Remember
- A stalled stage keeps its item and takes a new one only when its own item leaves; backpressure is that rule running upstream.
- A combinational ready loses nothing but makes one path through every stage: 3 levels for two stages, 9 for eight on this flow.
- A registered ready lets one extra beat in on the first edge of a stall; without room for it, a beat is overwritten.
- A skid buffer is the second register that catches that beat: ready from a flop, capacity two, drain the skid first, one beat per edge in steady flow.