Skip to content

RTL Design Patterns · Chapter 8 · Pipelining

Stalls, Bubbles & Flush

Real pipelines do not flow smoothly, so you need three control actions to keep them correct when the data stops marching one word per cycle. A stall freezes a stage by holding its clock-enable low, and it must freeze every stage upstream on the same cycle, or the word behind advances and overwrites the held one. A bubble is an empty slot, a stage whose valid is forced to zero, injected below the stall so downstream stages keep draining. A flush clears every in-flight valid at once to abort mispredicted branches, exceptions, or cancelled streams. This lesson builds an N-stage stall, bubble, and flush controller, shows the signature partial-stall bug that silently overwrites the held word, and self-checks all of it in SystemVerilog, Verilog, and VHDL.

Intermediate16 min readRTL Design PatternsPipelineStallBubbleFlushClock Enable

Chapter 8 · Section 8.4 · Pipelining

1. The Engineering Problem

In 8.3 you gave every pipeline stage a valid bit paired with its data, reset to zero and pipelined the same number of stages, so that startup cycles, gaps, and aborts pass through as invalid slots and never leak out as a result. That discipline assumes one thing: the pipeline advances every cycle. Every clock edge, each stage latches its predecessor, the whole train moves forward one car, and the valid bits ride along. But real pipelines do not get to advance every cycle. Sooner or later the flow is not smooth, and when it is not, the valid-bit discipline alone is not enough — you need control over the advance itself.

Here is the concrete need. Take the 3-stage arithmetic pipeline from 8.3 — multiply, round, saturate — but now the consumer at the end is a memory that sometimes cannot accept a write this cycle (its port is busy, an arbiter denied it). When the consumer says not this cycle, the last stage cannot hand off its result, so it must hold — and if the last stage holds while the middle stage advances a fresh word into it, the held result is destroyed. So holding one stage forces you to hold the stage feeding it, which forces you to hold the stage feeding that, all the way back to the input. A single point of backpressure ripples upstream and freezes an entire region of the pipe on the same cycle. Meanwhile the stage below the stall must be told there is nothing new coming — it must run a bubble, an empty cycle — or it will re-process the stale word it is still holding.

And there is a second, orthogonal need: sometimes you must not hold the work but destroy it. A branch mispredicts, an exception fires, a stream is cancelled — and every partially-computed word already in flight is now wrong and must never reach the consumer. You do not stall for that; you flush — clear every in-flight valid in a single cycle so the whole pipe empties out as invalid slots, regardless of what the data lines still carry.

the-need.v — the pipe cannot always advance; three actions manage the flow
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // A 3-stage pipeline feeding a consumer that is sometimes NOT ready this cycle.
   wire stall;   // 1 = last stage cannot hand off -> it must HOLD its result
   wire flush;   // 1 = abort: everything in flight is now wrong
 
   // NAIVE -- advance every stage every cycle, ignore stall/flush:
   //   always @(posedge clk) begin
   //       data_pipe[2] <= data_pipe[1];   // last stage overwritten even when stalled
   //       data_pipe[1] <= data_pipe[0];   // upstream marches on -> collides
   //   end
   // Under backpressure this DROPS the stalled result and DUPLICATES its neighbour.
 
   // The need: on stall, HOLD the stalled stage AND every stage upstream, together,
   // and inject a BUBBLE below it; on flush, CLEAR every in-flight valid at once.

2. Mental Model

3. Pattern Anatomy

The structure is the 8.3 valid-bit pipeline — paired data_pipe/valid_pipe registers per stage — with three control overlays: a shared stall enable that gates a contiguous upstream region, a bubble injection at the stall boundary, and a broadcast flush that clears every valid. Everything hard is in the scope of each control: which stages a stall must cover, where the bubble goes, and that the flush reaches all of them.

The stall ripple — the whole upstream region freezes together. When some stage cannot advance — because its consumer is not ready, or a hazard needs a cycle — you must hold not just that stage but every stage upstream of it, on the same cycle. The reason is structural: a pipeline holds exactly one word per stage. If stage k freezes (en_k = 0) but stage k-1 advances (en_{k-1} = 1), then on that edge data_pipe[k] keeps its value while data_pipe[k-1] overwrites itself with data_pipe[k-2] — and the word that was in stage k-1, which had nowhere to go because stage k was full, is simply lost. Worse, when the stall releases, stage k-1's new word and stage k's held word both try to advance, and one is duplicated downstream. The only correct behaviour is a single stall signal that de-asserts the clock-enable of the stalled stage and all upstream stages together, so the entire held region keeps its exact (data, valid) pairs and nothing collides.

The bubble — an empty slot injected below the stall point. While the upstream region is frozen, the stages below the stall are free to keep advancing and draining toward the output. But the boundary between held and free must not feed a real word — the held region is not producing one. So the first free stage below the stall is fed a bubble: its input valid is forced to 0 for that cycle. A bubble is just an invalid slot; it flows down and out as out_valid = 0, and it is what prevents the downstream from re-processing the stale word still sitting in the stalled stage. A stall without a bubble beneath it re-injects the held word every cycle it is frozen — duplication. So de-assert-enable-upstream and insert-bubble-below are two halves of one action.

The flush — a broadcast clear of all valids. To abort in-flight work — a branch mispredict, an exception, a cancelled request — you clear every valid register to 0 in a single cycle, valid_pipe[*] <= 0. The data registers are left untouched: because the output is qualified by valid, every in-flight word instantly becomes an invalid slot and none is consumed, whatever the data lines carry. Only validity matters. The one failure mode is clearing too few stages — a flush that clears stage 0 but not the deeper stages leaks the words already past stage 0 out as valid.

The interaction with reset. Reset and flush do the same thing to the valid chain — clear it all to zero — but for different reasons and at different times. Reset is the power-up return to a known state; flush is a runtime abort. In the RTL they share the clear path and a priority: reset wins over flush, flush wins over a normal enabled advance, and a stall (enable low, no flush) is the fall-through that assigns nothing and holds. That priority — rst > flush > (en ? advance : hold) — is the whole control skeleton.

The stall ripple — freeze the stalled stage and all upstream together, bubble below

data flow
The stall ripple — freeze the stalled stage and all upstream together, bubble belowbackpressurebackpressurebackpressureinsertclkinput (data,valid)held: en = 0 upstream toostage 0 : HELDen = 0 -> pair frozen in placestage 1 : HELDen = 0 -> pair frozen in placestage 2 : STALLEDconsumer not ready -> must holdbubble injectedbelowvalid = 0 fed to the free regiondownstream drainskeeps advancing on the invalid slot
When stage 2 stalls, the freeze propagates BACKWARD instantly: stage 1, stage 0, and the input all hold on the same cycle, gated by one shared enable, so no upstream word advances into a full stage and collides. The boundary below the stall feeds a bubble -- an invalid slot, valid = 0 -- so the free downstream region keeps advancing and draining without re-consuming the stale word held above. De-assert-enable-upstream and insert-bubble-below are two halves of one control action, and this is exactly the mechanism valid/ready backpressure uses in 9.2. It is identical in SystemVerilog, Verilog, and VHDL.

The failure this prevents is worth making concrete. Take DEPTH = 3 and a stall on the last stage. Do it correctly and the three (data, valid) pairs freeze together for as many cycles as the stall lasts; when it releases, each word resumes exactly where it was and every input word appears at the output exactly once, in order. Do it wrong — hold only stage 2, let stages 1 and 0 advance — and on the first stalled cycle stage 1's word overwrites into stage 2's input path while stage 2 holds, and the word that was in stage 1 is lost; when the stall releases, the collision surfaces as a dropped or duplicated result. The second visual makes the decision the control makes each cycle explicit — the same enable, three different verdicts.

One control skeleton — reset, flush, advance, or hold, decided every cycle

data flow
One control skeleton — reset, flush, advance, or hold, decided every cyclehighestthenthenelsecontrol decisionpriority: rst > flush > enrst = 1clear ALL valids (known state)flush = 1clear ALL valids (abort in flight)en = 1, no flushadvance the (data, valid) pairsen = 0, no flushSTALL: assign nothing -> hold pair
Every stage runs the same four-way decision each edge, in strict priority. Reset clears every valid to a known state. Flush clears every valid to abort the in-flight work, leaving the data untouched because validity is what the consumer qualifies on. An enabled advance moves the (data, valid) pair forward one stage. And the fall-through -- enable low, no flush -- is a stall: the block assigns nothing, so the pair holds exactly. Reset beats flush beats advance beats hold. That priority is the entire control skeleton, and it is the same in SystemVerilog, Verilog, and VHDL.

4. Real RTL Implementation

This is the core of the page. We build two things — (a) an N-stage pipeline with stall, bubble, and flush control (per-stage data_pipe/valid_pipe, a single stall that clock-enable-holds the stalled stage and all upstream, a bubble injected below, flush clearing all valids; WIDTH and DEPTH generic); and (b) the partial-stall bug beside its whole-region-stall fix — and each is shown in SystemVerilog, Verilog, and VHDL with an RTL block and a self-checking clocked testbench. The reasoning is identical in all three; only the syntax differs.

One discipline runs through every RTL block, inherited from 8.3: registers update on the clock with non-blocking assignments; reset is synchronous, active-high, and clears the valid chain; and the control priority is fixed at rst > flush > (en ? advance : hold) so a stall is simply the branch that assigns nothing and holds both chains as a unit.

4a. The N-stage stall / bubble / flush pipeline — three ways

The pipeline carries WIDTH-bit data across DEPTH stages. A single input stall freezes all stages together (the whole upstream region — here modelled as backpressure from the output, so the freeze covers the entire pipe) and injects a bubble at stage 0 by forcing the entering valid to 0 while held. flush clears every valid in one cycle. The output is (data_pipe[DEPTH-1], valid_pipe[DEPTH-1]).

pipe_stall.sv — N-stage pipeline with stall (whole region), bubble, flush
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module pipe_stall #(
       parameter int WIDTH = 32,                      // datapath width
       parameter int DEPTH = 3                        // number of pipeline stages
   )(
       input  logic             clk,
       input  logic             rst,                  // synchronous, active-high
       input  logic             stall,                // 1 = freeze the whole held region
       input  logic             flush,                // 1 = clear ALL valids this cycle
       input  logic             in_valid,             // 1 = real operand; 0 = bubble
       input  logic [WIDTH-1:0] in_data,
       output logic             out_valid,            // 1 = out_data is a real result
       output logic [WIDTH-1:0] out_data
   );
       logic [WIDTH-1:0] data_pipe  [DEPTH];
       logic             valid_pipe [DEPTH];
 
       // One shared clock-enable for the held region: en is low on a stall, so EVERY
       // stage freezes on the same cycle -- no upstream word advances into a full stage.
       logic en;
       assign en = ~stall;
 
       always_ff @(posedge clk) begin
           if (rst) begin
               // Reset clears the VALID chain only (data X is harmless while valid = 0).
               for (int i = 0; i < DEPTH; i++) valid_pipe[i] <= 1'b0;
           end else if (flush) begin
               // FLUSH: broadcast-clear every in-flight valid. Data left untouched.
               for (int i = 0; i < DEPTH; i++) valid_pipe[i] <= 1'b0;
           end else if (en) begin
               // Normal advance. Stage 0 latches the input pair; a bubble is in_valid = 0.
               data_pipe[0]  <= in_data;
               valid_pipe[0] <= in_valid;
               for (int i = 1; i < DEPTH; i++) begin
                   data_pipe[i]  <= data_pipe[i-1];
                   valid_pipe[i] <= valid_pipe[i-1];   // valid moves exactly with data
               end
           end
           // else (stall, no flush): assign NOTHING -> hold ALL (data, valid) pairs as a
           // unit. The bubble is implicit: nothing new enters, so the pipe holds in place.
       end
 
       // Qualified output: out_data is meaningful only when out_valid is high.
       assign out_data  = data_pipe[DEPTH-1];
       assign out_valid = valid_pipe[DEPTH-1];
   endmodule

The clocked testbench drives a numbered stream of real words, asserts stall for several cycles in the middle, and self-checks the invariant that stall must never violate: every input word appears at the output exactly once, in order — none lost, none duplicated across the stall. It then fires a flush with the pipe full and asserts the in-flight words all vanish (out_valid low) while words pushed after the flush pass through normally.

pipe_stall_tb.sv — stall loses/duplicates nothing; flush drops in-flight words
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module pipe_stall_tb;
       localparam int WIDTH = 32;
       localparam int DEPTH = 3;
       logic clk = 1'b0, rst, stall, flush, in_valid, out_valid;
       logic [WIDTH-1:0] in_data, out_data;
       int errors = 0;
 
       // Scoreboard: expected output stream, in order. We push tags in; each tag that
       // comes out valid must match the head of the queue -> exactly once, in order.
       int exp_q [$];
       int seen  = 0;
 
       pipe_stall #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
           .clk(clk), .rst(rst), .stall(stall), .flush(flush),
           .in_valid(in_valid), .in_data(in_data), .out_valid(out_valid), .out_data(out_data));
 
       always #5 clk = ~clk;
 
       // Drive one cycle: push a (valid, data) with the given stall/flush controls.
       task automatic step(input logic v, input int tag, input logic s, input logic f);
           @(negedge clk);
           in_valid = v; in_data = tag; stall = s; flush = f;
           if (v && !s && !f) exp_q.push_back(tag);   // a real word we EXPECT to see out
           @(posedge clk); #1;
           if (out_valid) begin
               seen++;
               if (exp_q.size() == 0) begin
                   $error("output %0h with empty expected queue (duplicate/leak)", out_data); errors++;
               end else begin
                   int e = exp_q.pop_front();
                   if (out_data !== e) begin
                       $error("out=%0h exp=%0h (out of order / wrong word)", out_data, e); errors++;
                   end
               end
           end
       endtask
 
       initial begin
           rst = 1'b1; stall = 1'b0; flush = 1'b0; in_valid = 1'b0; in_data = '0;
           @(posedge clk); #1; rst = 1'b0;
 
           // Push words 1..3, then STALL for three cycles, then resume with 4..6.
           step(1'b1, 32'h1, 1'b0, 1'b0);
           step(1'b1, 32'h2, 1'b0, 1'b0);
           step(1'b0, 32'h0, 1'b1, 1'b0);          // STALL -- whole region frozen
           step(1'b0, 32'h0, 1'b1, 1'b0);          // STALL
           step(1'b0, 32'h0, 1'b1, 1'b0);          // STALL
           step(1'b1, 32'h3, 1'b0, 1'b0);          // resume
           step(1'b1, 32'h4, 1'b0, 1'b0);
           step(1'b1, 32'h5, 1'b0, 1'b0);
           step(1'b1, 32'h6, 1'b0, 1'b0);
           // Drain: no new words, let the tail walk out.
           repeat (DEPTH + 2) step(1'b0, 32'h0, 1'b0, 1'b0);
           // Every pushed word must have come out exactly once, in order.
           if (exp_q.size() != 0) begin $error("%0d words never appeared (lost)", exp_q.size()); errors++; end
 
           // Now FLUSH with the pipe full: in-flight words must all be dropped.
           step(1'b1, 32'hA1, 1'b0, 1'b0);
           step(1'b1, 32'hA2, 1'b0, 1'b0);
           // Flushing drops the two words currently in flight -- pop them off the model too.
           void'(exp_q.pop_front()); void'(exp_q.pop_front());
           step(1'b0, 32'h0, 1'b0, 1'b1);          // FLUSH -- clear all valids
           step(1'b0, 32'h0, 1'b0, 1'b0);
           if (out_valid) begin $error("flushed word leaked out as valid"); errors++; end
           step(1'b0, 32'h0, 1'b0, 1'b0);
           if (out_valid) begin $error("second flushed word leaked out"); errors++; end
           // A word pushed AFTER the flush must still pass through cleanly.
           step(1'b1, 32'hB1, 1'b0, 1'b0);
           repeat (DEPTH + 1) step(1'b0, 32'h0, 1'b0, 1'b0);
           if (exp_q.size() != 0) begin $error("post-flush word lost"); errors++; end
 
           if (errors == 0) $display("PASS: stall keeps order & count; flush drops in-flight, post-flush passes");
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

The Verilog form is the same structure with reg/wire typing; because Verilog has no queue, the scoreboard is a small circular array of expected tags with head/tail indices, and the checks print FAIL on any mismatch.

pipe_stall.v — N-stage stall / bubble / flush pipeline in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module pipe_stall #(
       parameter WIDTH = 32,
       parameter DEPTH = 3
   )(
       input  wire             clk,
       input  wire             rst,                  // synchronous, active-high
       input  wire             stall,                // 1 = freeze the whole held region
       input  wire             flush,                // 1 = clear ALL valids
       input  wire             in_valid,
       input  wire [WIDTH-1:0] in_data,
       output wire             out_valid,
       output wire [WIDTH-1:0] out_data
   );
       reg [WIDTH-1:0] data_pipe  [0:DEPTH-1];
       reg             valid_pipe [0:DEPTH-1];
       integer i;
 
       wire en = ~stall;                             // one shared enable for the region
 
       always @(posedge clk) begin
           if (rst) begin
               for (i = 0; i < DEPTH; i = i + 1) valid_pipe[i] <= 1'b0;
           end else if (flush) begin
               for (i = 0; i < DEPTH; i = i + 1) valid_pipe[i] <= 1'b0;   // data untouched
           end else if (en) begin
               data_pipe[0]  <= in_data;
               valid_pipe[0] <= in_valid;            // bubble = in_valid == 0
               for (i = 1; i < DEPTH; i = i + 1) begin
                   data_pipe[i]  <= data_pipe[i-1];
                   valid_pipe[i] <= valid_pipe[i-1]; // valid moves with data
               end
           end
           // else STALL: no assignment holds BOTH chains as a unit for the whole region.
       end
 
       assign out_data  = data_pipe[DEPTH-1];
       assign out_valid = valid_pipe[DEPTH-1];       // qualified output
   endmodule
pipe_stall_tb.v — self-checking; stall keeps order & count, flush drops in-flight
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module pipe_stall_tb;
       parameter WIDTH = 32, DEPTH = 3, QMAX = 64;
       reg  clk, rst, stall, flush, in_valid;
       reg  [WIDTH-1:0] in_data;
       wire out_valid;
       wire [WIDTH-1:0] out_data;
       integer errors;
 
       // Circular scoreboard of expected tags, in order (head..tail).
       reg  [WIDTH-1:0] exp_q [0:QMAX-1];
       integer head, tail;
 
       pipe_stall #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
           .clk(clk), .rst(rst), .stall(stall), .flush(flush),
           .in_valid(in_valid), .in_data(in_data), .out_valid(out_valid), .out_data(out_data));
 
       always #5 clk = ~clk;
 
       task step;
           input v; input [WIDTH-1:0] tag; input s; input f;
           begin
               @(negedge clk);
               in_valid = v; in_data = tag; stall = s; flush = f;
               if (v && !s && !f) begin exp_q[tail] = tag; tail = (tail + 1) % QMAX; end
               @(posedge clk); #1;
               if (out_valid) begin
                   if (head == tail) begin
                       $display("FAIL: output %h with empty queue (duplicate/leak)", out_data);
                       errors = errors + 1;
                   end else begin
                       if (out_data !== exp_q[head]) begin
                           $display("FAIL: out=%h exp=%h (out of order)", out_data, exp_q[head]);
                           errors = errors + 1;
                       end
                       head = (head + 1) % QMAX;
                   end
               end
           end
       endtask
 
       integer k;
       initial begin
           errors = 0; head = 0; tail = 0; clk = 1'b0;
           rst = 1'b1; stall = 1'b0; flush = 1'b0; in_valid = 1'b0; in_data = 0;
           @(posedge clk); #1; rst = 1'b0;
 
           step(1'b1, 32'h1, 1'b0, 1'b0);
           step(1'b1, 32'h2, 1'b0, 1'b0);
           step(1'b0, 0, 1'b1, 1'b0);              // STALL
           step(1'b0, 0, 1'b1, 1'b0);              // STALL
           step(1'b0, 0, 1'b1, 1'b0);              // STALL
           step(1'b1, 32'h3, 1'b0, 1'b0);
           step(1'b1, 32'h4, 1'b0, 1'b0);
           step(1'b1, 32'h5, 1'b0, 1'b0);
           step(1'b1, 32'h6, 1'b0, 1'b0);
           for (k = 0; k < DEPTH + 2; k = k + 1) step(1'b0, 0, 1'b0, 1'b0);
           if (head != tail) begin $display("FAIL: words never appeared (lost)"); errors = errors + 1; end
 
           // FLUSH with the pipe full: drop the two in-flight words from the model too.
           step(1'b1, 32'hA1, 1'b0, 1'b0);
           step(1'b1, 32'hA2, 1'b0, 1'b0);
           tail = (tail - 2 + QMAX) % QMAX;        // un-expect the two flushed words
           step(1'b0, 0, 1'b0, 1'b1);              // FLUSH
           step(1'b0, 0, 1'b0, 1'b0);
           if (out_valid) begin $display("FAIL: flushed word leaked out"); errors = errors + 1; end
           step(1'b0, 0, 1'b0, 1'b0);
           if (out_valid) begin $display("FAIL: second flushed word leaked out"); errors = errors + 1; end
           step(1'b1, 32'hB1, 1'b0, 1'b0);
           for (k = 0; k < DEPTH + 1; k = k + 1) step(1'b0, 0, 1'b0, 1'b0);
           if (head != tail) begin $display("FAIL: post-flush word lost"); errors = errors + 1; end
 
           if (errors == 0) $display("PASS: stall keeps order & count; flush drops in-flight, post-flush passes");
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

In VHDL the two chains are arrays of vector / of std_logic, updated in a clocked process with the rst > flush > en priority; the scoreboard is a small array with head/tail indices, and assert … severity error is the self-check.

pipe_stall.vhd — N-stage stall / bubble / flush pipeline in VHDL (numeric_std)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity pipe_stall is
       generic (
           WIDTH : positive := 32;
           DEPTH : positive := 3
       );
       port (
           clk       : in  std_logic;
           rst       : in  std_logic;                 -- synchronous, active-high
           stall     : in  std_logic;                 -- 1 = freeze the whole region
           flush     : in  std_logic;                 -- 1 = clear ALL valids
           in_valid  : in  std_logic;                 -- 1 = real operand; 0 = bubble
           in_data   : in  std_logic_vector(WIDTH-1 downto 0);
           out_valid : out std_logic;
           out_data  : out std_logic_vector(WIDTH-1 downto 0)
       );
   end entity;
 
   architecture rtl of pipe_stall is
       type data_arr is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
       signal data_pipe  : data_arr;
       signal valid_pipe : std_logic_vector(DEPTH-1 downto 0) := (others => '0');
       signal en         : std_logic;
   begin
       en <= not stall;                               -- one shared enable for the region
 
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   valid_pipe <= (others => '0');     -- clear VALID chain only
               elsif flush = '1' then
                   valid_pipe <= (others => '0');     -- broadcast clear; data untouched
               elsif en = '1' then
                   data_pipe(0)  <= in_data;
                   valid_pipe(0) <= in_valid;         -- bubble = in_valid = '0'
                   for i in 1 to DEPTH-1 loop
                       data_pipe(i)  <= data_pipe(i-1);
                       valid_pipe(i) <= valid_pipe(i-1);   -- valid moves with data
                   end loop;
               end if;
               -- else STALL: no assignment holds BOTH chains as a unit.
           end if;
       end process;
 
       out_data  <= data_pipe(DEPTH-1);
       out_valid <= valid_pipe(DEPTH-1);              -- qualified output
   end architecture;
pipe_stall_tb.vhd — self-checking; stall keeps order & count, flush drops in-flight
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity pipe_stall_tb is
   end entity;
 
   architecture sim of pipe_stall_tb is
       constant WIDTH : positive := 32;
       constant DEPTH : positive := 3;
       constant QMAX  : positive := 64;
       signal clk : std_logic := '0';
       signal rst, stall, flush, in_valid, out_valid : std_logic := '0';
       signal in_data, out_data : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
   begin
       dut : entity work.pipe_stall
           generic map (WIDTH => WIDTH, DEPTH => DEPTH)
           port map (clk => clk, rst => rst, stall => stall, flush => flush,
                     in_valid => in_valid, in_data => in_data,
                     out_valid => out_valid, out_data => out_data);
 
       clkgen : process
       begin
           clk <= '0'; wait for 5 ns;
           clk <= '1'; wait for 5 ns;
       end process;
 
       stim : process
           -- Scoreboard: circular queue of expected tags, in order.
           type q_arr is array (0 to QMAX-1) of std_logic_vector(WIDTH-1 downto 0);
           variable exp_q : q_arr;
           variable head  : integer := 0;
           variable tail  : integer := 0;
           variable errs  : integer := 0;
 
           -- One driven cycle with the given controls, plus the output self-check.
           procedure step(constant v : std_logic; constant tag : std_logic_vector(WIDTH-1 downto 0);
                          constant s : std_logic; constant f : std_logic) is
           begin
               wait until falling_edge(clk);
               in_valid <= v; in_data <= tag; stall <= s; flush <= f;
               if v = '1' and s = '0' and f = '0' then
                   exp_q(tail) := tag; tail := (tail + 1) mod QMAX;
               end if;
               wait until rising_edge(clk); wait for 1 ns;
               if out_valid = '1' then
                   if head = tail then
                       report "output with empty queue (duplicate/leak)" severity error; errs := errs + 1;
                   else
                       assert out_data = exp_q(head)
                           report "out of order / wrong word" severity error;
                       if out_data /= exp_q(head) then errs := errs + 1; end if;
                       head := (head + 1) mod QMAX;
                   end if;
               end if;
           end procedure;
 
           variable k : integer;
       begin
           rst <= '1'; stall <= '0'; flush <= '0'; in_valid <= '0'; in_data <= (others => '0');
           wait until rising_edge(clk); wait for 1 ns; rst <= '0';
 
           step('1', x"00000001", '0', '0');
           step('1', x"00000002", '0', '0');
           step('0', x"00000000", '1', '0');         -- STALL
           step('0', x"00000000", '1', '0');         -- STALL
           step('0', x"00000000", '1', '0');         -- STALL
           step('1', x"00000003", '0', '0');
           step('1', x"00000004", '0', '0');
           step('1', x"00000005", '0', '0');
           step('1', x"00000006", '0', '0');
           for k in 0 to DEPTH + 1 loop step('0', x"00000000", '0', '0'); end loop;
           assert head = tail report "words never appeared (lost)" severity error;
           if head /= tail then errs := errs + 1; end if;
 
           -- FLUSH with the pipe full: un-expect the two in-flight words.
           step('1', x"000000A1", '0', '0');
           step('1', x"000000A2", '0', '0');
           tail := (tail - 2 + QMAX) mod QMAX;
           step('0', x"00000000", '0', '1');         -- FLUSH
           step('0', x"00000000", '0', '0');
           assert out_valid = '0' report "flushed word leaked out as valid" severity error;
           step('0', x"00000000", '0', '0');
           assert out_valid = '0' report "second flushed word leaked out" severity error;
           step('1', x"000000B1", '0', '0');
           for k in 0 to DEPTH loop step('0', x"00000000", '0', '0'); end loop;
           assert head = tail report "post-flush word lost" severity error;
 
           if errs = 0 then
               report "pipe_stall self-check complete: order & count kept, flush drops in-flight" severity note;
           else
               report "pipe_stall self-check FAILED" severity error;
           end if;
           wait;
       end process;
   end architecture;

4b. The partial-stall bug — buggy vs fixed, in all three HDLs

Pattern (b) is the signature failure, shown as buggy vs fixed RTL in each language, then dramatized narratively in §7. The bug is the same everywhere: the designer gates only the stalled stage's enable on stall but leaves the upstream stages free-running, so when the last stage is held, the stage feeding it advances a new word on top of the held one — the held word is lost (or duplicated downstream when the stall releases). The fix is a single stall signal that de-asserts the enable of the stalled stage and all upstream stages together, with a bubble opened below.

pipe_partial.sv — BUGGY (only stalled stage held) vs FIXED (whole region held)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: stall gates ONLY the last stage. Stages 0..DEPTH-2 run free, so when the
   //        last stage is held, its predecessor advances a NEW word into stage
   //        DEPTH-1's input -- but stage DEPTH-1 is frozen, so the predecessor's OWN
   //        held word (which had nowhere to go) is OVERWRITTEN and lost; on release it
   //        surfaces as a dropped/duplicated result. Passes when never stalled.
   module pipe_stall_bad #(parameter int WIDTH = 32, parameter int DEPTH = 3) (
       input  logic clk, rst, stall,
       input  logic in_valid, input logic [WIDTH-1:0] in_data,
       output logic out_valid, output logic [WIDTH-1:0] out_data
   );
       logic [WIDTH-1:0] data_pipe  [DEPTH];
       logic             valid_pipe [DEPTH];
       always_ff @(posedge clk) begin
           if (rst) begin
               for (int i = 0; i < DEPTH; i++) valid_pipe[i] <= 1'b0;
           end else begin
               // Upstream stages ALWAYS advance -- the bug:
               data_pipe[0]  <= in_data;   valid_pipe[0] <= in_valid;
               for (int i = 1; i < DEPTH-1; i++) begin
                   data_pipe[i]  <= data_pipe[i-1];
                   valid_pipe[i] <= valid_pipe[i-1];
               end
               // Only the LAST stage honours stall -> collision at the boundary:
               if (!stall) begin
                   data_pipe[DEPTH-1]  <= data_pipe[DEPTH-2];
                   valid_pipe[DEPTH-1] <= valid_pipe[DEPTH-2];
               end
           end
       end
       assign out_data  = data_pipe[DEPTH-1];
       assign out_valid = valid_pipe[DEPTH-1];
   endmodule
 
   // FIXED: ONE shared enable freezes the stalled stage AND all upstream together, and
   //        nothing new enters (bubble implicit) while held -> no collision, no loss.
   module pipe_stall_good #(parameter int WIDTH = 32, parameter int DEPTH = 3) (
       input  logic clk, rst, stall,
       input  logic in_valid, input logic [WIDTH-1:0] in_data,
       output logic out_valid, output logic [WIDTH-1:0] out_data
   );
       logic [WIDTH-1:0] data_pipe  [DEPTH];
       logic             valid_pipe [DEPTH];
       logic en; assign en = ~stall;                  // whole-region enable
       always_ff @(posedge clk) begin
           if (rst) begin
               for (int i = 0; i < DEPTH; i++) valid_pipe[i] <= 1'b0;
           end else if (en) begin
               data_pipe[0]  <= in_data;   valid_pipe[0] <= in_valid;
               for (int i = 1; i < DEPTH; i++) begin
                   data_pipe[i]  <= data_pipe[i-1];
                   valid_pipe[i] <= valid_pipe[i-1]; // every stage gated by the SAME en
               end
           end
           // else STALL: assign nothing -> the whole region holds; no upstream collision.
       end
       assign out_data  = data_pipe[DEPTH-1];
       assign out_valid = valid_pipe[DEPTH-1];
   endmodule
pipe_partial_tb.sv — a stall in the middle exposes the overwrite (lost/dup word)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module pipe_partial_tb;
       localparam int WIDTH = 32, DEPTH = 3;
       logic clk = 1'b0, rst, stall, in_valid, out_valid;
       logic [WIDTH-1:0] in_data, out_data;
       int exp_q [$];
       int errors = 0;
 
       // Bind to pipe_stall_good to PASS; pipe_stall_bad drops/duplicates across the stall.
       pipe_stall_good #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
           .clk(clk), .rst(rst), .stall(stall),
           .in_valid(in_valid), .in_data(in_data), .out_valid(out_valid), .out_data(out_data));
 
       always #5 clk = ~clk;
 
       task automatic step(input logic v, input int tag, input logic s);
           @(negedge clk);
           in_valid = v; in_data = tag; stall = s;
           if (v && !s) exp_q.push_back(tag);
           @(posedge clk); #1;
           if (out_valid) begin
               if (exp_q.size() == 0) begin $error("out %0h, none expected (dup)", out_data); errors++; end
               else begin
                   int e = exp_q.pop_front();
                   if (out_data !== e) begin $error("out=%0h exp=%0h", out_data, e); errors++; end
               end
           end
       endtask
 
       initial begin
           rst = 1'b1; stall = 1'b0; in_valid = 1'b0; in_data = '0;
           @(posedge clk); #1; rst = 1'b0;
           // Fill the pipe, then stall while words are IN FLIGHT -- the only time the
           // partial-stall collision can happen. A never-stalled run would pass either DUT.
           step(1'b1, 32'h11, 1'b0);
           step(1'b1, 32'h22, 1'b0);
           step(1'b1, 32'h33, 1'b1);               // STALL while 11/22/33 are in flight
           step(1'b1, 32'h44, 1'b1);               // STALL: bad DUT overwrites upstream
           step(1'b1, 32'h55, 1'b0);               // resume
           step(1'b1, 32'h66, 1'b0);
           repeat (DEPTH + 3) step(1'b0, 32'h0, 1'b0);
           if (exp_q.size() != 0) begin $error("%0d words lost across stall", exp_q.size()); errors++; end
           if (errors == 0) $display("PASS: every input word out exactly once, in order, across the stall");
           else             $display("FAIL: %0d errors (partial-stall overwrite)", errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair is the same story with reg/wire typing. Stalling while words are in flight is what exposes the collision: the buggy DUT lets stage DEPTH-2 overwrite while stage DEPTH-1 is frozen, and the scoreboard catches the lost or duplicated word.

pipe_partial.v — BUGGY vs FIXED in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: only the last stage honours stall; upstream stages run free -> overwrite.
   module pipe_stall_bad #(parameter WIDTH = 32, DEPTH = 3) (
       input  wire clk, rst, stall, in_valid,
       input  wire [WIDTH-1:0] in_data,
       output wire out_valid, output wire [WIDTH-1:0] out_data
   );
       reg [WIDTH-1:0] data_pipe  [0:DEPTH-1];
       reg             valid_pipe [0:DEPTH-1];
       integer i;
       always @(posedge clk) begin
           if (rst) begin
               for (i = 0; i < DEPTH; i = i + 1) valid_pipe[i] <= 1'b0;
           end else begin
               data_pipe[0]  <= in_data;   valid_pipe[0] <= in_valid;
               for (i = 1; i < DEPTH-1; i = i + 1) begin      // upstream ALWAYS advances
                   data_pipe[i]  <= data_pipe[i-1];
                   valid_pipe[i] <= valid_pipe[i-1];
               end
               if (!stall) begin                               // only last stage held
                   data_pipe[DEPTH-1]  <= data_pipe[DEPTH-2];
                   valid_pipe[DEPTH-1] <= valid_pipe[DEPTH-2];
               end
           end
       end
       assign out_data  = data_pipe[DEPTH-1];
       assign out_valid = valid_pipe[DEPTH-1];
   endmodule
 
   // FIXED: one shared enable freezes the whole region together; no upstream collision.
   module pipe_stall_good #(parameter WIDTH = 32, DEPTH = 3) (
       input  wire clk, rst, stall, in_valid,
       input  wire [WIDTH-1:0] in_data,
       output wire out_valid, output wire [WIDTH-1:0] out_data
   );
       reg [WIDTH-1:0] data_pipe  [0:DEPTH-1];
       reg             valid_pipe [0:DEPTH-1];
       integer i;
       wire en = ~stall;
       always @(posedge clk) begin
           if (rst) begin
               for (i = 0; i < DEPTH; i = i + 1) valid_pipe[i] <= 1'b0;
           end else if (en) begin
               data_pipe[0]  <= in_data;   valid_pipe[0] <= in_valid;
               for (i = 1; i < DEPTH; i = i + 1) begin        // every stage same enable
                   data_pipe[i]  <= data_pipe[i-1];
                   valid_pipe[i] <= valid_pipe[i-1];
               end
           end
           // else STALL: whole region holds.
       end
       assign out_data  = data_pipe[DEPTH-1];
       assign out_valid = valid_pipe[DEPTH-1];
   endmodule
pipe_partial_tb.v — self-checking; mid-flight stall exposes the overwrite
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module pipe_partial_tb;
       parameter WIDTH = 32, DEPTH = 3, QMAX = 64;
       reg  clk, rst, stall, in_valid;
       reg  [WIDTH-1:0] in_data;
       wire out_valid;
       wire [WIDTH-1:0] out_data;
       reg  [WIDTH-1:0] exp_q [0:QMAX-1];
       integer head, tail, errors, k;
 
       // Bind to pipe_stall_good to PASS; pipe_stall_bad drops/duplicates across the stall.
       pipe_stall_good #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
           .clk(clk), .rst(rst), .stall(stall),
           .in_valid(in_valid), .in_data(in_data), .out_valid(out_valid), .out_data(out_data));
 
       always #5 clk = ~clk;
 
       task step;
           input v; input [WIDTH-1:0] tag; input s;
           begin
               @(negedge clk);
               in_valid = v; in_data = tag; stall = s;
               if (v && !s) begin exp_q[tail] = tag; tail = (tail + 1) % QMAX; end
               @(posedge clk); #1;
               if (out_valid) begin
                   if (head == tail) begin $display("FAIL: out %h, none expected (dup)", out_data); errors = errors + 1; end
                   else begin
                       if (out_data !== exp_q[head]) begin $display("FAIL: out=%h exp=%h", out_data, exp_q[head]); errors = errors + 1; end
                       head = (head + 1) % QMAX;
                   end
               end
           end
       endtask
 
       initial begin
           errors = 0; head = 0; tail = 0; clk = 1'b0;
           rst = 1'b1; stall = 1'b0; in_valid = 1'b0; in_data = 0;
           @(posedge clk); #1; rst = 1'b0;
           step(1'b1, 32'h11, 1'b0);
           step(1'b1, 32'h22, 1'b0);
           step(1'b1, 32'h33, 1'b1);               // STALL while words in flight
           step(1'b1, 32'h44, 1'b1);               // STALL
           step(1'b1, 32'h55, 1'b0);               // resume
           step(1'b1, 32'h66, 1'b0);
           for (k = 0; k < DEPTH + 3; k = k + 1) step(1'b0, 0, 1'b0);
           if (head != tail) begin $display("FAIL: words lost across stall"); errors = errors + 1; end
           if (errors == 0) $display("PASS: every input word out exactly once, in order, across the stall");
           else             $display("FAIL: %0d errors (partial-stall overwrite)", errors);
           $finish;
       end
   endmodule

In VHDL the same bug appears when only the last stage's assignment is guarded by not stall while the upstream loop advances unconditionally; the fix is the single en guarding the whole update.

pipe_partial.vhd — BUGGY (only stalled stage held) vs FIXED (whole region held)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: upstream advances unconditionally; only the last stage honours stall.
   entity pipe_stall_bad is
       generic ( WIDTH : positive := 32; DEPTH : positive := 3 );
       port ( clk, rst, stall, in_valid : in std_logic;
              in_data  : in  std_logic_vector(WIDTH-1 downto 0);
              out_valid : out std_logic;
              out_data  : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of pipe_stall_bad is
       type data_arr is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
       signal data_pipe  : data_arr;
       signal valid_pipe : std_logic_vector(DEPTH-1 downto 0) := (others => '0');
   begin
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then
                   valid_pipe <= (others => '0');
               else
                   data_pipe(0)  <= in_data;  valid_pipe(0) <= in_valid;
                   for i in 1 to DEPTH-2 loop            -- upstream ALWAYS advances
                       data_pipe(i)  <= data_pipe(i-1);
                       valid_pipe(i) <= valid_pipe(i-1);
                   end loop;
                   if stall = '0' then                    -- only last stage held
                       data_pipe(DEPTH-1)  <= data_pipe(DEPTH-2);
                       valid_pipe(DEPTH-1) <= valid_pipe(DEPTH-2);
                   end if;
               end if;
           end if;
       end process;
       out_data  <= data_pipe(DEPTH-1);
       out_valid <= valid_pipe(DEPTH-1);
   end architecture;
 
   -- FIXED: one shared enable freezes the whole region together.
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   entity pipe_stall_good is
       generic ( WIDTH : positive := 32; DEPTH : positive := 3 );
       port ( clk, rst, stall, in_valid : in std_logic;
              in_data  : in  std_logic_vector(WIDTH-1 downto 0);
              out_valid : out std_logic;
              out_data  : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of pipe_stall_good is
       type data_arr is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
       signal data_pipe  : data_arr;
       signal valid_pipe : std_logic_vector(DEPTH-1 downto 0) := (others => '0');
       signal en         : std_logic;
   begin
       en <= not stall;
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then
                   valid_pipe <= (others => '0');
               elsif en = '1' then                        -- whole region gated by en
                   data_pipe(0)  <= in_data;  valid_pipe(0) <= in_valid;
                   for i in 1 to DEPTH-1 loop
                       data_pipe(i)  <= data_pipe(i-1);
                       valid_pipe(i) <= valid_pipe(i-1);
                   end loop;
               end if;
               -- else STALL: whole region holds.
           end if;
       end process;
       out_data  <= data_pipe(DEPTH-1);
       out_valid <= valid_pipe(DEPTH-1);
   end architecture;
pipe_partial_tb.vhd — self-checking; mid-flight stall exposes the overwrite
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity pipe_partial_tb is
   end entity;
 
   architecture sim of pipe_partial_tb is
       constant WIDTH : positive := 32; constant DEPTH : positive := 3; constant QMAX : positive := 64;
       signal clk : std_logic := '0';
       signal rst, stall, in_valid, out_valid : std_logic := '0';
       signal in_data, out_data : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
   begin
       -- Bind to pipe_stall_good to PASS; pipe_stall_bad drops/duplicates across the stall.
       dut : entity work.pipe_stall_good
           generic map (WIDTH => WIDTH, DEPTH => DEPTH)
           port map (clk => clk, rst => rst, stall => stall,
                     in_valid => in_valid, in_data => in_data,
                     out_valid => out_valid, out_data => out_data);
 
       clkgen : process begin clk <= '0'; wait for 5 ns; clk <= '1'; wait for 5 ns; end process;
 
       stim : process
           type q_arr is array (0 to QMAX-1) of std_logic_vector(WIDTH-1 downto 0);
           variable exp_q : q_arr;
           variable head  : integer := 0;
           variable tail  : integer := 0;
           variable errs  : integer := 0;
 
           procedure step(constant v : std_logic; constant tag : std_logic_vector(WIDTH-1 downto 0);
                          constant s : std_logic) is
           begin
               wait until falling_edge(clk);
               in_valid <= v; in_data <= tag; stall <= s;
               if v = '1' and s = '0' then exp_q(tail) := tag; tail := (tail + 1) mod QMAX; end if;
               wait until rising_edge(clk); wait for 1 ns;
               if out_valid = '1' then
                   if head = tail then
                       report "out with none expected (dup)" severity error; errs := errs + 1;
                   else
                       assert out_data = exp_q(head) report "wrong word / out of order" severity error;
                       if out_data /= exp_q(head) then errs := errs + 1; end if;
                       head := (head + 1) mod QMAX;
                   end if;
               end if;
           end procedure;
 
           variable k : integer;
       begin
           rst <= '1'; stall <= '0'; in_valid <= '0'; in_data <= (others => '0');
           wait until rising_edge(clk); wait for 1 ns; rst <= '0';
           step('1', x"00000011", '0');
           step('1', x"00000022", '0');
           step('1', x"00000033", '1');              -- STALL while words in flight
           step('1', x"00000044", '1');              -- STALL
           step('1', x"00000055", '0');              -- resume
           step('1', x"00000066", '0');
           for k in 0 to DEPTH + 2 loop step('0', x"00000000", '0'); end loop;
           assert head = tail report "words lost across stall" severity error;
           if head /= tail then errs := errs + 1; end if;
           if errs = 0 then
               report "pipe_partial self-check complete: every word out once, in order" severity note;
           else
               report "pipe_partial self-check FAILED (partial-stall overwrite)" severity error;
           end if;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: a stall must freeze the stalled stage and its entire upstream region with one shared clock-enable and open a bubble beneath it — freeze only the stalled stage and an upstream word overwrites the held one; and a flush must clear every stage's valid, not just the front.

5. Verification Strategy

The stall/flush discipline is a conservation property — words are neither created nor destroyed by a stall — plus an abort property — a flush destroys exactly the in-flight words and nothing else. So verification lives at those two invariants, driven by a scoreboard, not by a single-cycle spot check.

A stall (and any number of bubbles) never loses or duplicates a word: every input word marked valid appears at the output exactly once, in order. A flush drops exactly the words in flight at the flush cycle — they never appear valid at the output — while every word pushed after the flush passes through normally.

Everything below makes those invariants checkable and maps onto the testbenches in §4.

  • Scoreboard self-check (order and count). Model the expected output as an in-order queue: every cycle an input word is genuinely accepted (in_valid high, not stalled, not flushed), push its tag; every cycle out_valid is high, pop the head and assert out_data equals it. This is exactly what the §4 testbenches do — SV uses a queue, Verilog and VHDL a circular array with head/tail — and it proves conservation: an empty-queue pop is a duplicate/leak, a leftover queue at the end is a lost word, a head mismatch is reordering. The scoreboard is trivially correct because a stalled pipeline is still a FIFO of depth DEPTH.
  • Stall-under-flight check. Assert stall specifically while words are in flight (the pipe partly full), not just when it is empty — because the partial-stall overwrite can only happen at the boundary between a held stage and a free upstream stage. A stall applied to an empty or a saturated-then-drained pipe can pass even the buggy DUT; the directed test in §4b stalls mid-flight, which is the only stimulus that exposes the collision.
  • Bubble check. Interleave in_valid = 0 cycles and confirm each stays invalid end-to-end — it must never appear as an out_valid cycle and must never cause a real word to be re-emitted. A bubble injected below a stall must keep the downstream draining without re-consuming the held word.
  • Flush check. Fill the pipe with up to DEPTH valid words, assert flush for one cycle, and confirm every in-flight word is invalidated — out_valid stays low for the following DEPTH cycles even though out_data still drives the (now meaningless) flushed values — and that a word pushed after the flush appears normally. A flush that clears only some stages leaks a stale word; the model un-expects exactly the words that were in flight at the flush.
  • Priority check. Drive rst, flush, and stall in overlapping combinations and confirm the fixed priority holds: reset clears all valids regardless; a flush in the same cycle as a stall still clears (flush beats hold); an enabled advance only happens when neither reset nor flush is asserted. The if (rst) … else if (flush) … else if (en) … ladder is what this proves.
  • Parameter-generic verification. Re-run the stall / bubble / flush / conservation suite for DEPTH = 1, 2, 3, 5 and WIDTH = 1, 8, 32. A DEPTH = 1 pipe is a good corner (stall and flush degenerate to a single register hold/clear); a deeper pipe stresses the multi-stage collision. The valid chain and data chain must stay equal-length at every DEPTH, and the sweep proves it.
  • Expected waveform. On a correct run, while stall is high the output holds — out_valid and out_data do not change and no new word appears — and the instant stall releases the held stream resumes with no gap and no repeat. On a flush, out_valid drops to low across the next DEPTH cycles even as out_data keeps driving the flushed values, then real results resume. The visual signature of the partial-stall bug is a word that changes on the output during a stall, or a duplicated/missing word right after the stall releases; the signature of a shallow flush is a valid word appearing one or two cycles after the flush that should have been killed.

6. Common Mistakes

Partial stall — holding only the stalled stage. The signature failure. Gating only stage k's clock-enable on stall while stages k-1 … 0 run free means that on a stalled cycle the upstream word advances into a stage that is full and frozen, so the held word (or the advancing word) is overwritten and lost, and the collision surfaces as a dropped or duplicated result when the stall releases. A stall must freeze the whole upstream region as one unit — one shared enable across the stalled stage and every stage behind it. (Post-mortem in §7.)

Forgetting to inject a bubble below the stall. The stages below the stall keep advancing and draining, but if the boundary keeps re-presenting the held stage's word instead of an invalid slot, the downstream re-processes the same stale word every frozen cycle — duplication. A stall must open a bubble beneath it: force the first free stage's input valid to zero while the region above is held. De-assert-enable-upstream and insert-bubble-below are two halves of one action; do one without the other and you either collide or duplicate.

Flushing only stage 0. Clearing the front stage's valid on a flush but leaving the deeper stages marked valid lets the words already past stage 0 leak out as if the aborted operation had completed. A flush is a broadcast clear of every stage's valid in one cycle; getting only some stages is the same bug as a partial stall, displaced from stall to flush.

Stalling the data but not the valid (or vice-versa). A stall must hold the (data, valid) pair as a unit. Hold the data but let the valid advance and the held word arrives at the output as an invalid slot (a lost result) or a downstream valid qualifies the wrong word; hold the valid but let the data advance and the valid ends up over the wrong data (a duplicate). Gate both registers with the same enable, or neither — any asymmetry tears the pair apart. (This is the 8.3 hold-the-pair rule, now under real backpressure.)

Missing the bubble when a stall meets a real input. If the input keeps presenting in_valid = 1 while the pipe is stalled, that word must not be silently dropped — either it is held at the input (its producer must also see the backpressure) or it is lost. The stall must ripple all the way back to the source: an input word offered during a stall is only accepted once the enable returns. Forgetting this is the seam where 8.4 becomes the valid/ready handshake of 9.1.

Treating flush and reset as interchangeable in priority. Reset and flush both clear the valid chain, but their priority relative to a stall matters: reset must win unconditionally, and a flush must win over a stall (you abort even while backpressured). Writing if (en) advance; else hold; before checking flush means a stall would swallow the flush and leave aborted words in flight. Order it rst > flush > (en ? advance : hold).

7. DebugLab

The pipeline that ate a result under backpressure — a stall that froze only one stage

The engineering lesson: a stall is never local — freezing a stage without freezing everything upstream of it on the same cycle lets an upstream word march into the frozen, occupied stage and destroy a result. The tell is data loss (or duplication) that appears only under real backpressure with a full pipe and vanishes the moment the pipe is idle or the stall lands on an empty pipeline — a bug the block-level tests miss because they never stall a full pipe. Three habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: gate the stalled stage and its entire upstream region with one shared clock-enable so the whole held region freezes as a unit; inject a bubble below the stall so the free downstream drains without re-consuming the held word; and verify with an order-and-count scoreboard, stalling while the pipe is full, so a lost or duplicated word is caught. This is the exact mechanism the valid/ready handshake (9.1) and backpressure (9.2) are built on: ready going low is a stall that must ripple upstream and open a bubble below, and the skid buffer (9.3) is the register that catches the one word already in flight when it does.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the stall-ripple / bubble / flush reasoning behind the controller.

Exercise 1 — Trace a stall through a full pipe

For a DEPTH = 4 pipeline holding four valid words W1..W4 (W4 nearest the output), the consumer asserts stall for two cycles, then releases. Hand-trace data_pipe[0..3] and valid_pipe[0..3] for each of the two stalled cycles and the first two cycles after release, using the correct whole-region stall. Confirm every word leaves exactly once, in order. Then re-trace the buggy partial stall (only stage 3 held) and mark the exact cycle and stage where a word is overwritten.

Exercise 2 — Place the bubble

Take a DEPTH = 4 pipeline where a hazard forces stages 0–2 to freeze for one cycle while stage 3 must keep draining to the consumer. State precisely which stage receives the injected bubble, what its input valid must be that cycle, and why the stage below the stall would otherwise duplicate a word. Then answer: if instead the stall is at stage 1 (stages 0–1 freeze, 2–3 drain), where does the bubble go, and what must happen to a real input word offered during the freeze?

Exercise 3 — Order the priority

Write the control ladder for a stage that must handle reset, flush, stall, and normal advance. State the exact priority order and justify each edge: why reset beats flush, why flush beats a stall, and why a stall is the fall-through that assigns nothing. Then describe what breaks if a designer writes the enable check before the flush check (if (en) advance; else if (flush) clear; …) and a flush arrives during a stall.

Exercise 4 — Verify the abort

Write the test plan (not the RTL) that would catch a shallow flush — one that clears only stage 0 — before tape-out. Specify: how you fill the pipe so there are words in stages 1 and 2 at the flush cycle; which words your scoreboard must un-expect when the flush fires; the post-flush assertion on out_valid and for how many cycles; and the check that a word pushed after the flush still appears. State which single step in your plan directly distinguishes a correct broadcast flush from one that clears only the front stage.

Continue in RTL Design Patterns (sibling and forward links unlock as they ship):

  • The Valid-Bit Discipline — Chapter 8.3; the prerequisite that carries a valid bit alongside the data through every stage and introduces the stall (clock-enable hold) and flush (clear all valids) this page turns into a full control discipline.
  • Pipeline Hazards — Chapter 8.5; the data and control hazards that cause stalls and flushes — why you must freeze a region or abort the pipe in the first place, and how detection drives the stall/flush signals built here.
  • Pipelined Datapath — Chapter 8.6; a full multi-stage datapath where this page's stall ripple and flush control keep every result correct across the whole compute path.
  • valid/ready Handshake — Chapter 9.1; the flow-control protocol where the stall is imposed by a backward ready line — de-assert-enable-upstream and insert-bubble-below is exactly this page's mechanism, driven by ready instead of a control input.
  • Backpressure & Flow Control — Chapter 9.2; the propagation of a consumer's not-ready upstream through the pipe — the stall ripple of this page generalized into a full backpressure chain.
  • Skid Buffer — Chapter 9.3; the register that catches the one word already in flight when backpressure deasserts — the missing piece when a stall cannot ripple upstream in a single cycle.
  • Case Study — Streaming FIFO Block — Chapter 13.3; a full streaming block where stalls, bubbles, flushes, and FIFO backpressure compose into one verified design.

Backward / in-track dependencies:

  • Register with Enable / Load — Chapter 2.2; the clock-enable that becomes the pipeline stall — holding a register's contents when its enable is low is exactly the hold-the-pair mechanism, replicated across a whole stalled region.
  • Why Pipeline? — Chapter 8.1; the pipeline this page controls, and the alignment lesson (delay everything that must stay together with the data) that the stall must preserve under a freeze.
  • Latency vs Throughput — Chapter 8.2; the throughput a stall interrupts and the bubbles that reduce it — the quantitative cost of the control this page adds.
  • Synchronous FIFO Architecture — Chapter 7.1; the elastic buffer that absorbs stalls instead of rippling them, and the standard way a producer and consumer decouple their stall behaviour.
  • FIFO Full/Empty Logic — Chapter 7.2; the full flag that is a stall source — a FIFO asserting full backpressures its producer exactly as a stalled stage backpressures its upstream.
  • Pulse & Strobe Generators — Chapter 5.4; the single-cycle strobe used to fire a one-shot flush cleanly, and the qualifier discipline that keeps a control pulse aligned to the cycle it acts on.
  • The RTL Design Mindset — Chapter 0.2; the Control lens this page applies — the stall/flush logic is the pipeline's control, distinct from its datapath, and proving no word is lost is the Verification lens.

Verilog v1 prerequisites (the grammar this pattern transcribes into):

  • Blocking and Non-Blocking Assignments — why every stage's data and valid registers update with non-blocking (<=) so the whole held region freezes or advances atomically on each edge.
  • If-Else Statements — the reset / flush / enable priority ladder (if (rst) … else if (flush) … else if (en) …) that selects clear, abort, advance, or hold.
  • Initial & Always Blocks — the clocked always block that advances or holds both chains, and the reset that clears the valid chain.
  • Physical Data Types — the vector widths (WIDTH data, one-bit valid) and register-array declarations whose deliberate sizing keeps the two chains the same length across DEPTH.

11. Summary

  • A pipeline cannot always advance, and three control actions manage the flow. Once the valid-bit discipline (8.3) makes bubbles and aborts harmless in principle, you still need to manage the advance itself: stall (freeze), bubble (inject an empty slot), and flush (abort in flight). These are the difference between a pipeline that survives real backpressure and one that silently drops results.
  • A stall freezes the stalled stage and its entire upstream region as one unit. Because a pipeline holds exactly one word per stage, freezing stage k forces you to freeze k-1 … 0 on the same cycle, with one shared clock-enable — hold only the stalled stage and an upstream word marches into the frozen, occupied slot and is overwritten. Gate the whole held region together, holding each (data, valid) pair as a unit.
  • A bubble is injected below the stall so the downstream keeps draining. The stages below the stall are not frozen; they advance and empty toward the output, and the boundary feeds them an invalid slot (valid = 0) so they never re-consume the stale word held above. De-assert-enable-upstream and insert-bubble-below are two halves of one action.
  • A flush is a broadcast clear of every valid; the data can stay. To abort a mispredict, an exception, or a cancelled stream, clear all the valid registers to zero in one cycle — the data is left untouched because validity is what the consumer qualifies on. Clearing only some stages leaks a stale word. The control skeleton is the fixed priority rst > flush > (en ? advance : hold).
  • The signature bug is the partial-stall overwrite. Gating only the stalled stage's enable while the upstream runs free destroys a result under real backpressure — and passes every test that never stalls a full pipe. The tell is data loss or duplication that appears only when the pipe is full and stalled. Built and self-check-verified here with an order-and-count scoreboard in SystemVerilog, Verilog, and VHDL — and the exact mechanism the valid/ready handshake (9.1), backpressure (9.2), and skid buffer (9.3) are built on.