Stalls and Bubbles
Every pipeline in this module so far has made one quiet assumption: whatever is downstream can accept a new result every cycle, forever. Module 5 flagged this and promised a proper treatment later. This is it.
Real pipelines rarely get that assumption. A FIFO downstream may be full. A shared bus may be busy. A memory may need an extra cycle. When any of that happens to a pipeline with no way to pause, the pipeline does not wait; it keeps pushing data forward every cycle and something gets overwritten.
The enable pattern applied to the whole pipeline
You met the clock-enable pattern in Section 5.5 for a single register: if (en) q <= d;. A stall is that pattern applied to every register in the pipeline at once. While the stall condition is active, every stage holds its current value.
`timescale 1ns / 1ps
`default_nettype none
module stallable_pipeline (
input wire clk, rst_n,
input wire stall, // when high, the ENTIRE pipeline freezes
input wire [15:0] data_in,
input wire valid_in,
output reg [15:0] data_out,
output reg valid_out
);
reg [15:0] stage1_data, stage2_data;
reg stage1_valid, stage2_valid;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
stage1_data <= 16'b0; stage1_valid <= 1'b0;
stage2_data <= 16'b0; stage2_valid <= 1'b0;
data_out <= 16'b0; valid_out <= 1'b0;
end else if (!stall) begin
// Only advance the pipeline when NOT stalled
stage1_data <= data_in; stage1_valid <= valid_in;
stage2_data <= stage1_data + 1; stage2_valid <= stage1_valid;
data_out <= stage2_data; valid_out <= stage2_valid;
end
// else: stall is high and every register above holds its value.
// Nothing needs to be written here; doing nothing IS the stall.
end
endmodule
There is no separate freeze logic. The stall falls out of the else if (!stall) structure: when stall is high, none of the assignments execute and every register keeps its value, the same way an if without an else in a clocked block infers a register with an enable, as in Section 5.5.
Stall versus bubble
A stall freezes the whole pipeline. Nothing moves. It is the right response when the pipeline cannot accept new input or when something downstream cannot take the output right now.
A bubble is different. The pipeline keeps advancing, but one slot is marked invalid. This is what you use to discard exactly one in-flight operation (a mispredicted branch, say) without stopping everything else that is already in the pipeline behind it.
A bubble is made by forcing that slot’s valid bit to 0 for one cycle while every stage continues to advance. The data in the bubbled slot does not matter, because valid being low tells every downstream consumer to ignore it.
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
stage1_valid <= 1'b0;
end else if (flush) begin
stage1_valid <= 1'b0; // inject a bubble: this slot is now invalid...
stage1_data <= data_in;
// ...but the pipeline still ADVANCES this cycle. Compare the stall case,
// where nothing advances at all.
end else if (!stall) begin
stage1_valid <= valid_in;
stage1_data <= data_in;
end
end
Backpressure: letting downstream request a stall
The stall signal usually does not come from inside the pipeline. It is driven by whatever is downstream, telling the pipeline it cannot take the output this cycle. This is the ready/valid handshake that appears in every real bus protocol, and that you will meet formally again in the AXI4-Lite work in Module 10:
wire downstream_ready; // asserted by the consumer when it can accept data
wire stall = valid_out && !downstream_ready; // stall only if we HAVE valid output that can't be taken
The valid_out && term matters. There is no reason to stall on a cycle where the pipeline is not producing anything meaningful. Stalling is only needed when a real result is ready to leave and the consumer cannot take it.
Every CPU pipeline uses both mechanisms at once. A cache miss stalls fetch, decode and execute until the data arrives. A mispredicted branch injects bubbles behind the correct path, discarding the wrongly fetched instructions without stopping the ones that were already correctly in flight. A large share of the performance bugs in a pipelined CPU come down to one of these two being slightly wrong.
Practice Exercise
Extend stallable_pipeline with a flush input that injects a bubble into stage 1 only, without stalling stage 2 or the output. Confirm with a testbench that a flushed cycle’s data never reaches data_out with valid_out asserted.
Solution
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
stage1_data <= 16'b0; stage1_valid <= 1'b0;
stage2_data <= 16'b0; stage2_valid <= 1'b0;
data_out <= 16'b0; valid_out <= 1'b0;
end else if (!stall) begin
stage1_data <= data_in;
stage1_valid <= flush ? 1'b0 : valid_in; // bubble: force invalid on flush, but still advance
stage2_data <= stage1_data + 1;
stage2_valid <= stage1_valid;
data_out <= stage2_data;
valid_out <= stage2_valid;
end
end
The testbench drives valid_in = 1 with flush = 1 on one cycle, tracks that value through the pipeline over the next two cycles, and confirms valid_out is 0 on the cycle it would otherwise have reached the output, while every value pushed through on unflushed cycles arrives with valid_out = 1.
A pipeline that assumes it can always push forward will lose data the first time something downstream is not ready, which is why every real pipeline needs a stall built from the clock-enable pattern applied to every stage at once. A bubble is a different tool: it invalidates one slot without stopping the rest. In the next article we put throughput, balancing and stalling together in a complete pipelined multiply-accumulate unit.