RTL Design Patterns · Chapter 8 · Pipelining
The Valid-Bit Discipline
A pipeline register is full every cycle, but a value in a register is not the same as a real result. After reset the stages hold undefined state, a gap in the input flows down as a bubble, and an aborted operation leaves garbage behind. In all of these the data lines drive something the consumer must not use, and treating that something as an answer is the signature pipeline bug. The fix is the valid-bit discipline: carry one valid bit alongside the data through every stage, reset it to zero, advance it with the data, and use a result only when its valid is high. Data tells you what, valid tells you when it is real. This lesson builds the N-stage discipline with stall and flush, exposes the valid-data misalignment bug, and self-checks all of it in SystemVerilog, Verilog, and VHDL.
Intermediate16 min readRTL Design PatternsPipelineValid BitBubblesStall FlushData Alignment
Chapter 8 · Section 8.3 · Pipelining
1. The Engineering Problem
In 8.1 you split a slow combinational computation into stages, dropping a register between each so the clock could run fast, and you learned the alignment lesson the hard way: everything that must stay together with the data has to be delayed the same number of stages as the data. A control field that skips a register arrives a cycle early; an operand that takes an extra stage arrives a cycle late; either way the pieces no longer line up. That page ended on the instruction to delay everything that must stay aligned with the data. This page is the single most important thing that must stay aligned: whether the data is real at all.
Here is the concrete need. You have a 3-stage arithmetic pipeline — call it multiply, then round, then saturate — and downstream of it sits a consumer that writes each result into a memory. The pipeline is a chain of registers, so on every clock edge each stage latches whatever its predecessor was holding and drives it onward. That means the output port is driving a value on every single cycle, forever. But the producer feeding the pipeline does not supply a fresh operand every cycle. In the first two cycles after reset the pipeline is still filling — stage 3 is holding power-up X, not a computed result. Whenever the producer has a gap, a bubble flows down the pipe — a cycle with no real operand, whose stage contents are stale. And if an operation is aborted partway down, its in-flight contents are garbage that must never be written.
So the consumer faces a question the data lines cannot answer: is the value on the output bus this cycle a real result, or is it startup noise, a bubble, or an aborted operation? The bus looks identical in all four cases — a full-width word, driven cleanly. If the consumer writes on every cycle it will write undefined power-up state as the first two entries, write duplicate or stale words for every bubble, and write aborted operations it was told to discard. The data carries what; nothing in it carries when it is real. That missing bit of information — present exactly on the cycles that carry a genuine result and absent on all the others — is what this page adds and keeps aligned.
// A 3-stage pipeline. Every clock edge, each stage latches its predecessor -> the
// OUTPUT drives a value on EVERY cycle, whether or not a real operand went in.
wire [31:0] out_data; // driven every cycle, always -- a full word, cleanly
// But the producer does NOT feed a real operand every cycle:
// - fill: after reset, stages still hold power-up X (not a result)
// - bubble: a gap cycle with no operand -> stale contents (not a result)
// - flush: an aborted op in flight -> garbage (must be discarded)
// WRONG -- the consumer writes on every cycle, so it stores X, bubbles, aborts:
// always @(posedge clk) mem[waddr] <= out_data; // no idea WHICH cycles are real
// The need: a bit that is HIGH exactly on the cycles out_data is a real result,
// aligned to the data to the exact cycle. That bit is the valid-bit discipline.2. Mental Model
3. Pattern Anatomy
The structure is the 8.1 pipeline — a chain of data registers, one per stage — with a parallel chain of one-bit valid registers running alongside it, plus the enable and clear logic that handles stalls and flushes. Everything hard is in keeping the two chains in exact lockstep.
The paired per-stage registers. For a DEPTH-stage pipeline of WIDTH-bit data, there are DEPTH data registers data_pipe[0..DEPTH-1] and DEPTH valid registers valid_pipe[0..DEPTH-1]. Stage 0 latches the input pair (in_data, in_valid); every later stage latches its predecessor's pair, data_pipe[i] <= data_pipe[i-1] and valid_pipe[i] <= valid_pipe[i-1]. The two chains are the same length and clocked by the same edge, so a (data, valid) pair that enters at stage 0 stays paired all the way to data_pipe[DEPTH-1] / valid_pipe[DEPTH-1]. That structural pairing — not any comparison or computation — is what guarantees the valid bit arrives with its data.
The reset asymmetry. The valid registers are reset to 0; the data registers need no reset. This is deliberate and it is the whole reason startup is safe: after reset, valid_pipe is all zeros, so for the first DEPTH cycles — while the pipe fills with whatever X the data flops powered up holding — every output cycle is marked invalid. Only once a real in_valid has walked all DEPTH stages does valid_pipe[DEPTH-1] go high. Resetting valid but not data is a real area saving (you flop the datapath's reset away) and it is correct, because the datapath's undefined contents are never consumed while their valid is low.
The qualified output. The pipeline exposes two outputs that must be read together: out_data = data_pipe[DEPTH-1] and out_valid = valid_pipe[DEPTH-1]. out_valid is the qualifier — the consumer acts on out_data if and only if out_valid is high. This is the same shape you will meet again as the AXI-style valid line in 9.1: a data bus plus a one-bit qualifier, where the data is meaningful only in the cycles the qualifier asserts.
Bubbles. A bubble is simply a cycle presented at the input with in_valid = 0. It flows through the pipeline exactly like a real operand — it occupies a stage, advances every cycle — but its valid bit is 0, so it is an invalid slot. When it reaches the output, out_valid is low and the consumer ignores it. Bubbles are not a special case in the RTL; they fall out for free the moment valid is pipelined alongside data. A gap in the input stream becomes a gap in the valid stream, automatically aligned.
Stall via clock-enable. To freeze the pipeline (or a stage) — because the consumer is not ready, or an upstream resource is busy — you gate the registers with a clock-enable: when en is low, hold both the data and the valid registers. Holding data alone would let the valid bit advance while its data stood still, tearing the pair apart; holding valid alone would do the reverse. The rule is absolute: a stall holds the (data, valid) pair as a unit, or not at all. A held stage keeps its exact contents and its exact valid, so no result is dropped and none is duplicated.
Flush. To invalidate the whole pipeline at once — on a branch mispredict, an abort, a mode change — you clear all the valid registers to 0 in a single cycle, valid_pipe[*] <= 0. You do not need to touch the data registers: clearing the valids alone turns every in-flight word into an invalid slot, so nothing already in the pipe is ever consumed as a result. Flush is the exact dual of reset applied at runtime — reset clears valids so startup X is harmless; flush clears valids so aborted operations are harmless.
Data and valid march together — a real word, then a bubble, aligned stage by stage
data flowThe reset and flush behaviour is worth stating precisely because it is where the pattern earns its safety. Take DEPTH = 3. After reset, valid_pipe = 000 and the data flops hold X. Push a real operand at cycle 1 (in_valid = 1): the valid bit walks valid_pipe[0] = 1 at cycle 1, valid_pipe[1] = 1 at cycle 2, valid_pipe[2] = 1 at cycle 3 — so out_valid first rises on cycle 3, exactly when the first operand's data reaches data_pipe[2], and stays low through the two fill cycles when the output was still driving X. If instead a flush fires at some later cycle, all three valid bits clear to 0 in that one cycle, so the up-to-three in-flight words are all marked invalid at the output over the following cycles and none is consumed — even though their data lines still carry their (now meaningless) values.
The second visual is the one that makes the decision tangible: the output data bus is the same wire in every case; the valid bit is what routes it to "use it" or "ignore it."
Qualify the output — same data bus, opposite verdict, decided by the valid bit
data flow4. Real RTL Implementation
This is the core of the page. We build three things — (a) an N-stage pipeline with the valid-bit discipline (per-stage data_pipe/valid_pipe registers, valid reset to 0, output qualified by the final valid, WIDTH and DEPTH generic, supports bubbles); (b) stall via clock-enable and flush (hold both data and valid on a stall; clear all valids on flush); and (c) the valid-misalignment bug beside its 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 the register pattern and 8.1: the registers update on the clock with non-blocking assignments, reset is explicit (synchronous, active-high rst here) and clears the valid chain, and the valid registers are pipelined the same number of stages as the data registers so the pair never tears apart.
4a. The N-stage valid-bit pipeline — three ways
The pipeline carries WIDTH-bit data across DEPTH stages. Two parallel register arrays, data_pipe and valid_pipe, advance every clock. Reset clears valid_pipe only; the output is (data_pipe[DEPTH-1], valid_pipe[DEPTH-1]). A bubble is just an input cycle with in_valid = 0.
module pipe_valid #(
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 in_valid, // 1 = real operand this cycle; 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
);
// Two PARALLEL register chains, same length, same clock. The valid chain is
// pipelined IDENTICALLY to the data chain, so a (data, valid) pair that enters
// at stage 0 stays paired all the way to stage DEPTH-1.
logic [WIDTH-1:0] data_pipe [DEPTH];
logic valid_pipe [DEPTH];
always_ff @(posedge clk) begin
if (rst) begin
// Reset clears the VALID chain only. The data flops may hold X -- that
// is harmless because every cycle is marked invalid until a real
// operand has walked all DEPTH stages.
for (int i = 0; i < DEPTH; i++)
valid_pipe[i] <= 1'b0;
end else begin
// Stage 0 latches the input pair; a bubble is simply in_valid = 0.
data_pipe[0] <= in_data;
valid_pipe[0] <= in_valid;
// Every later stage latches its predecessor's pair, in lockstep.
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
end
// The output is QUALIFIED: out_data is meaningful only when out_valid is high.
assign out_data = data_pipe[DEPTH-1];
assign out_valid = valid_pipe[DEPTH-1];
endmoduleThe clocked testbench does the thing the flagship stands or falls on: it drives a stream with bubbles interleaved, and checks that out_valid is high on exactly the cycles a real operand reaches the end — never during the DEPTH fill cycles, never on a bubble — and that out_data matches the operand whenever out_valid is high. It models the pipeline as a reference delay line of the same depth.
module pipe_valid_tb;
localparam int WIDTH = 32;
localparam int DEPTH = 3;
localparam int N = 12; // cycles of stimulus
logic clk = 1'b0, rst, in_valid, out_valid;
logic [WIDTH-1:0] in_data, out_data;
int errors = 0;
pipe_valid #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
.clk(clk), .rst(rst), .in_valid(in_valid), .in_data(in_data),
.out_valid(out_valid), .out_data(out_data));
always #5 clk = ~clk;
// Reference: a DEPTH-deep delay line of the (valid, data) pair, reset-cleared.
logic ref_v [DEPTH];
logic [WIDTH-1:0] ref_d [DEPTH];
// Stimulus pattern: 1 = real operand, 0 = bubble. Deliberately gappy.
localparam bit stim_v [N] = '{1,1,0,1,0,0,1,1,1,0,1,0};
initial begin
rst = 1'b1; in_valid = 1'b0; in_data = '0;
for (int i = 0; i < DEPTH; i++) ref_v[i] = 1'b0;
@(posedge clk); #1; rst = 1'b0;
for (int t = 0; t < N + DEPTH; t++) begin
// Drive input (bubbles after the pattern ends).
@(negedge clk);
in_valid = (t < N) ? stim_v[t] : 1'b0;
in_data = 32'hD000 + t[15:0]; // a unique tag per cycle
// Advance the reference delay line by one stage, in lockstep with the DUT.
for (int i = DEPTH-1; i > 0; i--) begin
ref_v[i] = ref_v[i-1]; ref_d[i] = ref_d[i-1];
end
ref_v[0] = in_valid; ref_d[0] = in_data;
@(posedge clk); #1;
// out_valid must match the reference EXACTLY -- high only on real cycles.
assert (out_valid === ref_v[DEPTH-1])
else begin $error("t=%0d out_valid=%b exp=%b", t, out_valid, ref_v[DEPTH-1]); errors++; end
// When valid, the data must be the operand that entered DEPTH cycles ago.
if (out_valid)
assert (out_data === ref_d[DEPTH-1])
else begin $error("t=%0d out_data=%h exp=%h", t, out_data, ref_d[DEPTH-1]); errors++; end
end
if (errors == 0) $display("PASS: out_valid high exactly on real cycles; data aligned");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleThe Verilog form is the same structure with reg/wire typing; because Verilog cannot pack a 2-D port array cleanly across all tools, the pipeline uses simple indexed register arrays and a genvar-free loop inside the clocked block.
module pipe_valid #(
parameter WIDTH = 32,
parameter DEPTH = 3
)(
input wire clk,
input wire rst, // synchronous, active-high
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;
always @(posedge clk) begin
if (rst) begin
for (i = 0; i < DEPTH; i = i + 1)
valid_pipe[i] <= 1'b0; // clear the VALID chain only
end else 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
end
assign out_data = data_pipe[DEPTH-1];
assign out_valid = valid_pipe[DEPTH-1]; // qualified output
endmodule module pipe_valid_tb;
parameter WIDTH = 32, DEPTH = 3, N = 12;
reg clk, rst, in_valid;
reg [WIDTH-1:0] in_data;
wire out_valid;
wire [WIDTH-1:0] out_data;
integer t, i, errors;
pipe_valid #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
.clk(clk), .rst(rst), .in_valid(in_valid), .in_data(in_data),
.out_valid(out_valid), .out_data(out_data));
always #5 clk = ~clk;
// Reference DEPTH-deep delay line of the (valid, data) pair.
reg ref_v [0:DEPTH-1];
reg [WIDTH-1:0] ref_d [0:DEPTH-1];
// Gappy stimulus: 1 = real, 0 = bubble.
reg stim_v [0:N-1];
initial begin
stim_v[0]=1; stim_v[1]=1; stim_v[2]=0; stim_v[3]=1;
stim_v[4]=0; stim_v[5]=0; stim_v[6]=1; stim_v[7]=1;
stim_v[8]=1; stim_v[9]=0; stim_v[10]=1; stim_v[11]=0;
errors = 0; clk = 1'b0; rst = 1'b1; in_valid = 1'b0; in_data = 0;
for (i = 0; i < DEPTH; i = i + 1) ref_v[i] = 1'b0;
@(posedge clk); #1; rst = 1'b0;
for (t = 0; t < N + DEPTH; t = t + 1) begin
@(negedge clk);
in_valid = (t < N) ? stim_v[t] : 1'b0;
in_data = 16'hD000 + t;
for (i = DEPTH-1; i > 0; i = i - 1) begin
ref_v[i] = ref_v[i-1]; ref_d[i] = ref_d[i-1];
end
ref_v[0] = in_valid; ref_d[0] = in_data;
@(posedge clk); #1;
if (out_valid !== ref_v[DEPTH-1]) begin
$display("FAIL: t=%0d out_valid=%b exp=%b", t, out_valid, ref_v[DEPTH-1]);
errors = errors + 1;
end
if (out_valid && (out_data !== ref_d[DEPTH-1])) begin
$display("FAIL: t=%0d out_data=%h exp=%h", t, out_data, ref_d[DEPTH-1]);
errors = errors + 1;
end
end
if (errors == 0) $display("PASS: out_valid high exactly on real cycles; data aligned");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleIn VHDL the two chains are arrays of vector / of std_logic, updated in a clocked process; reset clears the valid array with an aggregate, and the output pair is a concurrent assignment of the last stage.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity pipe_valid is
generic (
WIDTH : positive := 32;
DEPTH : positive := 3
);
port (
clk : in std_logic;
rst : in std_logic; -- synchronous, active-high
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_valid 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'); -- clear the VALID chain only
else
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;
end if;
end process;
out_data <= data_pipe(DEPTH-1);
out_valid <= valid_pipe(DEPTH-1); -- qualified output
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity pipe_valid_tb is
end entity;
architecture sim of pipe_valid_tb is
constant WIDTH : positive := 32;
constant DEPTH : positive := 3;
constant N : positive := 12;
signal clk : std_logic := '0';
signal rst, in_valid, out_valid : std_logic := '0';
signal in_data, out_data : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
-- Gappy stimulus: '1' = real operand, '0' = bubble.
type bit_arr is array (0 to N-1) of std_logic;
constant stim_v : bit_arr :=
('1','1','0','1','0','0','1','1','1','0','1','0');
begin
dut : entity work.pipe_valid
generic map (WIDTH => WIDTH, DEPTH => DEPTH)
port map (clk => clk, rst => rst, 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
-- Reference DEPTH-deep delay line of the (valid, data) pair.
type data_arr is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
variable ref_v : std_logic_vector(DEPTH-1 downto 0) := (others => '0');
variable ref_d : data_arr;
variable iv : std_logic;
variable id : std_logic_vector(WIDTH-1 downto 0);
begin
rst <= '1'; wait until rising_edge(clk); wait for 1 ns; rst <= '0';
for t in 0 to N + DEPTH - 1 loop
wait until falling_edge(clk);
if t < N then iv := stim_v(t); else iv := '0'; end if;
id := std_logic_vector(to_unsigned(16#D000# + t, WIDTH));
in_valid <= iv;
in_data <= id;
-- Advance the reference delay line by one stage.
for i in DEPTH-1 downto 1 loop
ref_v(i) := ref_v(i-1); ref_d(i) := ref_d(i-1);
end loop;
ref_v(0) := iv; ref_d(0) := id;
wait until rising_edge(clk); wait for 1 ns;
assert out_valid = ref_v(DEPTH-1)
report "out_valid mismatch: marked wrong cycle valid" severity error;
if out_valid = '1' then
assert out_data = ref_d(DEPTH-1)
report "out_data mismatch: valid word carries wrong data" severity error;
end if;
end loop;
report "pipe_valid self-check complete" severity note;
wait;
end process;
end architecture;4b. Stall (clock-enable) and flush
Two runtime controls sit on top of the discipline. A stall (en = 0) freezes the pipeline by holding both chains — data and valid — so a paused stage keeps its exact contents and its exact valid, dropping and duplicating nothing. A flush clears all the valid bits in one cycle, turning every in-flight word into an invalid slot without touching the data. Both are shown here in all three HDLs.
module pipe_valid_ctrl #(
parameter int WIDTH = 32,
parameter int DEPTH = 3
)(
input logic clk,
input logic rst,
input logic en, // clock-enable: 0 = STALL (hold everything)
input logic flush, // 1 = clear ALL valids this cycle
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 if (flush) begin
// FLUSH: invalidate everything in flight. Data is left as-is -- clearing
// the valids alone makes every in-flight word an invalid slot.
for (int i = 0; i < DEPTH; i++) valid_pipe[i] <= 1'b0;
end else if (en) begin
// Normal advance -- only when enabled.
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];
end
end
// else (en == 0, no flush): STALL -- hold BOTH chains as a unit by not
// assigning them, so no (data, valid) pair is torn apart.
end
assign out_data = data_pipe[DEPTH-1];
assign out_valid = valid_pipe[DEPTH-1];
endmodule module pipe_valid_ctrl_tb;
localparam int WIDTH = 32, DEPTH = 3;
logic clk = 1'b0, rst, en, flush, in_valid, out_valid;
logic [WIDTH-1:0] in_data, out_data;
int errors = 0;
pipe_valid_ctrl #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
.clk(clk), .rst(rst), .en(en), .flush(flush),
.in_valid(in_valid), .in_data(in_data), .out_valid(out_valid), .out_data(out_data));
always #5 clk = ~clk;
task drive(input logic v, input logic [WIDTH-1:0] d, input logic e, input logic f);
@(negedge clk); in_valid = v; in_data = d; en = e; flush = f;
@(posedge clk); #1;
endtask
initial begin
rst = 1'b1; en = 1'b1; flush = 1'b0; in_valid = 1'b0; in_data = '0;
@(posedge clk); #1; rst = 1'b0;
// Push one real word, then STALL for two cycles: it must NOT reach the
// output early and must NOT be duplicated -- the pair is held.
drive(1'b1, 32'hCAFE0001, 1'b1, 1'b0); // enters stage 0
drive(1'b0, 32'h0, 1'b0, 1'b0); // STALL: hold everything
drive(1'b0, 32'h0, 1'b0, 1'b0); // STALL again
// Two cycles frozen: nothing advanced, so still no valid at the output.
assert (!out_valid) else begin $error("stall let a word advance"); errors++; end
// Release: the word now walks the remaining stages and appears once.
drive(1'b0, 32'h0, 1'b1, 1'b0);
drive(1'b0, 32'h0, 1'b1, 1'b0);
assert (out_valid && out_data === 32'hCAFE0001)
else begin $error("word lost/duplicated across stall"); errors++; end
drive(1'b0, 32'h0, 1'b1, 1'b0);
assert (!out_valid) else begin $error("word duplicated after stall"); errors++; end
// Now fill the pipe and FLUSH: every in-flight word must vanish (invalid).
drive(1'b1, 32'hBBBB0001, 1'b1, 1'b0);
drive(1'b1, 32'hBBBB0002, 1'b1, 1'b0);
drive(1'b0, 32'h0, 1'b1, 1'b1); // FLUSH -- clears all valids
drive(1'b0, 32'h0, 1'b1, 1'b0);
assert (!out_valid) else begin $error("flushed word leaked out as valid"); errors++; end
drive(1'b0, 32'h0, 1'b1, 1'b0);
assert (!out_valid) else begin $error("second flushed word leaked out"); errors++; end
if (errors == 0) $display("PASS: stall holds the pair; flush invalidates in-flight words");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmodule module pipe_valid_ctrl #(
parameter WIDTH = 32,
parameter DEPTH = 3
)(
input wire clk,
input wire rst,
input wire en, // 0 = STALL (hold both chains)
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;
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;
for (i = 1; i < DEPTH; i = i + 1) begin
data_pipe[i] <= data_pipe[i-1];
valid_pipe[i] <= valid_pipe[i-1];
end
end
// else: STALL -- no assignment holds BOTH chains as a unit.
end
assign out_data = data_pipe[DEPTH-1];
assign out_valid = valid_pipe[DEPTH-1];
endmodule module pipe_valid_ctrl_tb;
parameter WIDTH = 32, DEPTH = 3;
reg clk, rst, en, flush, in_valid;
reg [WIDTH-1:0] in_data;
wire out_valid;
wire [WIDTH-1:0] out_data;
integer errors;
pipe_valid_ctrl #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
.clk(clk), .rst(rst), .en(en), .flush(flush),
.in_valid(in_valid), .in_data(in_data), .out_valid(out_valid), .out_data(out_data));
always #5 clk = ~clk;
task drive(input v, input [WIDTH-1:0] d, input e, input f);
begin
@(negedge clk); in_valid = v; in_data = d; en = e; flush = f;
@(posedge clk); #1;
end
endtask
initial begin
errors = 0; clk = 1'b0; rst = 1'b1; en = 1'b1; flush = 1'b0;
in_valid = 1'b0; in_data = 0;
@(posedge clk); #1; rst = 1'b0;
drive(1'b1, 32'hCAFE0001, 1'b1, 1'b0); // real word enters
drive(1'b0, 0, 1'b0, 1'b0); // STALL
drive(1'b0, 0, 1'b0, 1'b0); // STALL
if (out_valid) begin $display("FAIL: stall let a word advance"); errors = errors + 1; end
drive(1'b0, 0, 1'b1, 1'b0);
drive(1'b0, 0, 1'b1, 1'b0);
if (!(out_valid && out_data === 32'hCAFE0001)) begin
$display("FAIL: word lost/duplicated across stall"); errors = errors + 1;
end
drive(1'b0, 0, 1'b1, 1'b0);
if (out_valid) begin $display("FAIL: word duplicated after stall"); errors = errors + 1; end
drive(1'b1, 32'hBBBB0001, 1'b1, 1'b0);
drive(1'b1, 32'hBBBB0002, 1'b1, 1'b0);
drive(1'b0, 0, 1'b1, 1'b1); // FLUSH
drive(1'b0, 0, 1'b1, 1'b0);
if (out_valid) begin $display("FAIL: flushed word leaked out"); errors = errors + 1; end
drive(1'b0, 0, 1'b1, 1'b0);
if (out_valid) begin $display("FAIL: second flushed word leaked out"); errors = errors + 1; end
if (errors == 0) $display("PASS: stall holds the pair; flush invalidates in-flight words");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmodule library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity pipe_valid_ctrl is
generic ( WIDTH : positive := 32; DEPTH : positive := 3 );
port (
clk, rst, en, flush : in std_logic; -- en=0 STALL; flush=1 clear valids
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_valid_ctrl 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');
elsif flush = '1' then
valid_pipe <= (others => '0'); -- data untouched
elsif en = '1' then
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: no assignment holds BOTH chains together.
end if;
end process;
out_data <= data_pipe(DEPTH-1);
out_valid <= valid_pipe(DEPTH-1);
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity pipe_valid_ctrl_tb is
end entity;
architecture sim of pipe_valid_ctrl_tb is
constant WIDTH : positive := 32; constant DEPTH : positive := 3;
signal clk : std_logic := '0';
signal rst, en, flush, in_valid, out_valid : std_logic := '0';
signal in_data, out_data : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
signal errors : integer := 0;
procedure drive(signal iv : out std_logic; signal idt : out std_logic_vector(WIDTH-1 downto 0);
signal ie : out std_logic; signal ifl : out std_logic;
constant v : std_logic; constant d : std_logic_vector(WIDTH-1 downto 0);
constant e : std_logic; constant f : std_logic;
signal clkin : in std_logic) is
begin
wait until falling_edge(clkin);
iv <= v; idt <= d; ie <= e; ifl <= f;
wait until rising_edge(clkin); wait for 1 ns;
end procedure;
begin
dut : entity work.pipe_valid_ctrl
generic map (WIDTH => WIDTH, DEPTH => DEPTH)
port map (clk => clk, rst => rst, en => en, 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
begin
rst <= '1'; en <= '1'; flush <= '0'; in_valid <= '0'; in_data <= (others => '0');
wait until rising_edge(clk); wait for 1 ns; rst <= '0';
drive(in_valid, in_data, en, flush, '1', x"CAFE0001", '1', '0', clk); -- real word
drive(in_valid, in_data, en, flush, '0', x"00000000", '0', '0', clk); -- STALL
drive(in_valid, in_data, en, flush, '0', x"00000000", '0', '0', clk); -- STALL
assert out_valid = '0' report "stall let a word advance" severity error;
drive(in_valid, in_data, en, flush, '0', x"00000000", '1', '0', clk);
drive(in_valid, in_data, en, flush, '0', x"00000000", '1', '0', clk);
assert out_valid = '1' and out_data = x"CAFE0001"
report "word lost/duplicated across stall" severity error;
drive(in_valid, in_data, en, flush, '1', x"BBBB0001", '1', '0', clk);
drive(in_valid, in_data, en, flush, '1', x"BBBB0002", '1', '0', clk);
drive(in_valid, in_data, en, flush, '0', x"00000000", '1', '1', clk); -- FLUSH
drive(in_valid, in_data, en, flush, '0', x"00000000", '1', '0', clk);
assert out_valid = '0' report "flushed word leaked out as valid" severity error;
report "pipe_valid_ctrl self-check complete" severity note;
wait;
end process;
end architecture;4c. The valid-misalignment bug — buggy vs fixed, in all three HDLs
Pattern (c) 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 pipelines the data through DEPTH stages but drives out_valid from something that is not delayed the same DEPTH stages — here, out_valid is combinational from in_valid, so it leads the data by DEPTH cycles and marks the fill/X cycles valid. The fix is a valid register in every stage, reset to 0, advancing with the data.
// BUGGY: data is delayed DEPTH stages, but out_valid is driven COMBINATIONALLY from
// in_valid -> valid leads the data by DEPTH cycles. During the fill cycles it
// asserts valid while data_pipe still holds reset-X -> garbage marked valid.
module pipe_bad #(parameter int WIDTH = 32, parameter int DEPTH = 3) (
input logic clk, rst, 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];
always_ff @(posedge clk) begin
data_pipe[0] <= in_data;
for (int i = 1; i < DEPTH; i++) data_pipe[i] <= data_pipe[i-1];
end
assign out_data = data_pipe[DEPTH-1];
assign out_valid = in_valid; // WRONG: not delayed, not reset-cleared
endmodule
// FIXED: a valid register in EVERY stage, reset to 0, advancing WITH the data.
module pipe_good #(parameter int WIDTH = 32, parameter int DEPTH = 3) (
input logic clk, rst, 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
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 delayed EXACTLY like data
end
end
end
assign out_data = data_pipe[DEPTH-1];
assign out_valid = valid_pipe[DEPTH-1]; // qualified by the final-stage valid
endmodule module pipe_bug_tb;
localparam int WIDTH = 32, DEPTH = 3, N = 8;
logic clk = 1'b0, rst, in_valid, out_valid;
logic [WIDTH-1:0] in_data, out_data;
int errors = 0;
// Bind to pipe_good to PASS; pipe_bad marks the fill-X cycles valid.
pipe_good #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
.clk(clk), .rst(rst), .in_valid(in_valid), .in_data(in_data),
.out_valid(out_valid), .out_data(out_data));
always #5 clk = ~clk;
logic ref_v [DEPTH];
logic [WIDTH-1:0] ref_d [DEPTH];
localparam bit stim_v [N] = '{1,0,1,1,0,1,0,1}; // includes a bubble at t=1
initial begin
rst = 1'b1; in_valid = 1'b0; in_data = '0;
for (int i = 0; i < DEPTH; i++) ref_v[i] = 1'b0;
@(posedge clk); #1; rst = 1'b0;
for (int t = 0; t < N + DEPTH; t++) begin
@(negedge clk);
in_valid = (t < N) ? stim_v[t] : 1'b0;
in_data = 32'hE000 + t[15:0];
for (int i = DEPTH-1; i > 0; i--) begin ref_v[i]=ref_v[i-1]; ref_d[i]=ref_d[i-1]; end
ref_v[0] = in_valid; ref_d[0] = in_data;
@(posedge clk); #1;
// pipe_bad fails HERE on the first DEPTH cycles: out_valid high while ref is 0.
assert (out_valid === ref_v[DEPTH-1])
else begin $error("t=%0d valid=%b exp=%b (misaligned)", t, out_valid, ref_v[DEPTH-1]); errors++; end
if (out_valid)
assert (out_data === ref_d[DEPTH-1])
else begin $error("t=%0d data=%h exp=%h", t, out_data, ref_d[DEPTH-1]); errors++; end
end
if (errors == 0) $display("PASS: valid aligned to data through fill and bubble");
else $display("FAIL: %0d errors (valid marks the wrong cycle)", errors);
$finish;
end
endmoduleThe Verilog buggy/fixed pair is the same story with reg/wire typing. Sweeping the fill cycles and a bubble in the testbench is what exposes the lead: pipe_bad asserts out_valid on the first DEPTH cycles, when out_data is still reset-X.
// BUGGY: out_valid combinational from in_valid -> leads the data by DEPTH cycles.
module pipe_bad #(parameter WIDTH = 32, DEPTH = 3) (
input wire clk, rst, 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];
integer i;
always @(posedge clk) begin
data_pipe[0] <= in_data;
for (i = 1; i < DEPTH; i = i + 1) data_pipe[i] <= data_pipe[i-1];
end
assign out_data = data_pipe[DEPTH-1];
assign out_valid = in_valid; // WRONG: not delayed, not reset-cleared
endmodule
// FIXED: valid register in every stage, reset to 0, advancing with the data.
module pipe_good #(parameter WIDTH = 32, DEPTH = 3) (
input wire clk, rst, 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; i = i + 1) begin
data_pipe[i] <= data_pipe[i-1];
valid_pipe[i] <= valid_pipe[i-1]; // valid delayed exactly like data
end
end
end
assign out_data = data_pipe[DEPTH-1];
assign out_valid = valid_pipe[DEPTH-1];
endmodule module pipe_bug_tb;
parameter WIDTH = 32, DEPTH = 3, N = 8;
reg clk, rst, in_valid;
reg [WIDTH-1:0] in_data;
wire out_valid;
wire [WIDTH-1:0] out_data;
integer t, i, errors;
// Bind to pipe_good to PASS; pipe_bad marks the fill-X cycles valid.
pipe_good #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
.clk(clk), .rst(rst), .in_valid(in_valid), .in_data(in_data),
.out_valid(out_valid), .out_data(out_data));
always #5 clk = ~clk;
reg ref_v [0:DEPTH-1];
reg [WIDTH-1:0] ref_d [0:DEPTH-1];
reg stim_v [0:N-1];
initial begin
stim_v[0]=1; stim_v[1]=0; stim_v[2]=1; stim_v[3]=1;
stim_v[4]=0; stim_v[5]=1; stim_v[6]=0; stim_v[7]=1;
errors = 0; clk = 1'b0; rst = 1'b1; in_valid = 1'b0; in_data = 0;
for (i = 0; i < DEPTH; i = i + 1) ref_v[i] = 1'b0;
@(posedge clk); #1; rst = 1'b0;
for (t = 0; t < N + DEPTH; t = t + 1) begin
@(negedge clk);
in_valid = (t < N) ? stim_v[t] : 1'b0;
in_data = 16'hE000 + t;
for (i = DEPTH-1; i > 0; i = i - 1) begin ref_v[i]=ref_v[i-1]; ref_d[i]=ref_d[i-1]; end
ref_v[0] = in_valid; ref_d[0] = in_data;
@(posedge clk); #1;
if (out_valid !== ref_v[DEPTH-1]) begin
$display("FAIL: t=%0d valid=%b exp=%b (misaligned)", t, out_valid, ref_v[DEPTH-1]);
errors = errors + 1;
end
if (out_valid && (out_data !== ref_d[DEPTH-1])) begin
$display("FAIL: t=%0d data=%h exp=%h", t, out_data, ref_d[DEPTH-1]);
errors = errors + 1;
end
end
if (errors == 0) $display("PASS: valid aligned to data through fill and bubble");
else $display("FAIL: %0d errors (valid marks the wrong cycle)", errors);
$finish;
end
endmoduleIn VHDL the same bug appears when out_valid is a concurrent copy of in_valid while the data is pipelined through the array; the fix is the valid_pipe vector reset-cleared and advanced alongside data_pipe.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- BUGGY: out_valid is a concurrent copy of in_valid -> leads the data by DEPTH cycles.
entity pipe_bad is
generic ( WIDTH : positive := 32; DEPTH : positive := 3 );
port ( clk, rst, 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_bad is
type data_arr is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
signal data_pipe : data_arr;
begin
process (clk) begin
if rising_edge(clk) then
data_pipe(0) <= in_data;
for i in 1 to DEPTH-1 loop data_pipe(i) <= data_pipe(i-1); end loop;
end if;
end process;
out_data <= data_pipe(DEPTH-1);
out_valid <= in_valid; -- WRONG: not delayed, not reset-cleared
end architecture;
-- FIXED: a valid register in every stage, reset to 0, advancing with the data.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity pipe_good is
generic ( WIDTH : positive := 32; DEPTH : positive := 3 );
port ( clk, rst, 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_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');
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-1 loop
data_pipe(i) <= data_pipe(i-1);
valid_pipe(i) <= valid_pipe(i-1); -- valid delayed exactly like data
end loop;
end if;
end if;
end process;
out_data <= data_pipe(DEPTH-1);
out_valid <= valid_pipe(DEPTH-1);
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity pipe_bug_tb is
end entity;
architecture sim of pipe_bug_tb is
constant WIDTH : positive := 32; constant DEPTH : positive := 3; constant N : positive := 8;
signal clk : std_logic := '0';
signal rst, in_valid, out_valid : std_logic := '0';
signal in_data, out_data : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
type bit_arr is array (0 to N-1) of std_logic;
constant stim_v : bit_arr := ('1','0','1','1','0','1','0','1'); -- bubble at t=1
begin
-- Bind to pipe_good to PASS; pipe_bad marks the fill-X cycles valid.
dut : entity work.pipe_good
generic map (WIDTH => WIDTH, DEPTH => DEPTH)
port map (clk => clk, rst => rst, 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 data_arr is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
variable ref_v : std_logic_vector(DEPTH-1 downto 0) := (others => '0');
variable ref_d : data_arr;
variable iv : std_logic;
variable id : std_logic_vector(WIDTH-1 downto 0);
begin
rst <= '1'; wait until rising_edge(clk); wait for 1 ns; rst <= '0';
for t in 0 to N + DEPTH - 1 loop
wait until falling_edge(clk);
if t < N then iv := stim_v(t); else iv := '0'; end if;
id := std_logic_vector(to_unsigned(16#E000# + t, WIDTH));
in_valid <= iv; in_data <= id;
for i in DEPTH-1 downto 1 loop ref_v(i) := ref_v(i-1); ref_d(i) := ref_d(i-1); end loop;
ref_v(0) := iv; ref_d(0) := id;
wait until rising_edge(clk); wait for 1 ns;
assert out_valid = ref_v(DEPTH-1)
report "valid marks the wrong cycle (misaligned)" severity error;
if out_valid = '1' then
assert out_data = ref_d(DEPTH-1)
report "valid word carries wrong data" severity error;
end if;
end loop;
report "pipe_bug self-check complete" severity note;
wait;
end process;
end architecture;Across all three languages the lesson is one sentence: pipeline the valid bit through the same number of stages as the data, reset it to zero, and qualify the output with the final-stage valid — then fill cycles, bubbles, and flushes can never leak out as a real result.
5. Verification Strategy
The valid-bit discipline is an alignment property, so verification lives at the alignment: drive a stream with bubbles and a flush, and check that out_valid is high on exactly the cycles carrying a real result — not one cycle early (the fill/misalignment bug) and not on a bubble — and that out_data is correct whenever out_valid is high.
out_validis high on exactly the cycles a real operand reaches the last stage — low during theDEPTHfill cycles, low on every bubble, low after a flush — and wheneverout_validis high,out_dataequals the operand that enteredDEPTHcycles earlier.
Everything below makes that invariant checkable and maps onto the testbenches in §4.
- Reference-delay-line self-check. Model the DUT as a
DEPTH-deep delay line of the(valid, data)pair, reset-cleared, and advance it in lockstep with the DUT each cycle. Every cycle, assertout_valid === ref_valid[DEPTH-1]and, when valid,out_data === ref_data[DEPTH-1]. This is exactly what the §4 testbenches do (SVassert (out_valid === ref_v[DEPTH-1]), Verilogif (out_valid !== ref_v[DEPTH-1]) $display("FAIL…"), VHDLassert out_valid = ref_v(DEPTH-1) … severity error). The reference is trivially correct because the discipline is a delay line, so any deviation is a real bug. - Fill / startup check. Reset, then drive a real operand and assert
out_validis low for the firstDEPTHcycles — while the pipe still holds power-upX— and rises only on the cycle the first operand reaches the last stage. This is the direct detector for the misalignment bug: a combinational or under-delayed valid asserts during the fill and fails here. - Bubble check. Interleave
in_valid = 0cycles into the stream and confirm each bubble stays invalid end-to-end: it must appear at the output as anout_valid = 0cycle exactlyDEPTHcycles after it was injected, and never mark a stale word valid. A gap in the input stream must become an aligned gap in the output valid stream. - Flush check. Fill the pipe with up to
DEPTHvalid words, assertflushfor one cycle, and confirm every in-flight word is invalidated —out_validstays low for the following cycles even thoughout_datastill drives the (now meaningless) flushed values. A flush that clears only some stages, or that clears data but not valid, leaks a stale word. - Stall check. Push a word, then hold
en = 0for several cycles, and confirm the word neither advances early nor is duplicated:out_validdoes not rise while stalled, and whenenreturns the word appears exactly once at the output. This catches a stall that holds data but not valid (duplicates a valid) or valid but not data (tears the pair apart). - Parameter-generic verification. Re-run the fill / bubble / stall / flush suite for
DEPTH = 1, 2, 3, 5andWIDTH = 1, 8, 32. A discipline that passes atDEPTH = 3can still be wrong atDEPTH = 5if the valid chain was hardcoded to a different length than the data chain — the two must always be equal, and a generic sweep is what proves it. - Expected waveform. On a correct run, after reset
out_validstays low for exactlyDEPTHcycles, then tracks the input valid stream delayed byDEPTH— high on real cycles, low on bubbles — without_datacarrying the matching operand on every high cycle. The visual signature of the misalignment bug isout_validrisingDEPTHcycles too early (during fill, over reset-Xdata) or leading each bubble by a cycle; the signature of a flush bug isout_validstaying high for a word that should have been killed.
6. Common Mistakes
Valid not aligned with the data. The signature failure. If out_valid is delayed a different number of stages than the data — combinational from the input, delayed DEPTH-1 instead of DEPTH, or not pipelined at all — then the valid bit marks the wrong cycle, and the consumer latches the wrong word: a fill-cycle X, a bubble, or the neighbouring operand. The rule is exact: the valid bit must traverse the same number of registers, on the same clock and enable, as the data it qualifies. Pipeline them together, in the same block, and they cannot drift apart. (Post-mortem in §7.)
Not clearing valid on reset. At power-up the data flops hold X and the valid flops hold X too if you did not reset them — so on the first cycles the output can be marked valid over undefined data, and that X is consumed as a real result. Reset must clear the valid chain to 0 (the data chain need not be reset). Valid-reset-to-zero is precisely what makes the fill cycles harmless; skip it and startup noise leaks out as the first few results.
Using data without checking valid. The output data bus is driven on every cycle, so a consumer that acts on out_data unconditionally — writes it, accumulates it, forwards it — consumes bubbles and fill cycles as if they were results. out_valid is not decoration; it is the gate. Every use of the result must be predicated on its valid being high.
Not clearing valids on a flush. When an operation is aborted (a branch mispredict, a cancelled request), the in-flight words must be discarded — but if the flush clears only the first stage's valid, or clears the data but not the valid, the words already deeper in the pipe still carry valid = 1 and leak out as if the aborted operation had completed. A flush must clear all the valid registers in one cycle; clearing the data is neither necessary nor sufficient.
Losing valid during a stall. When a stage is frozen with a clock-enable, holding the data but letting the valid advance drops the held word's validity (it arrives at the output as an invalid slot — a lost result) or, symmetrically, holding valid while data advances duplicates a valid over the wrong word. A stall must hold the (data, valid) pair as a unit — gate both registers with the same enable, or neither. Any asymmetry tears the pair apart.
Resetting the data path instead of the valid path. A tempting inversion: reset the data flops to zero and leave valid un-reset, reasoning that zero data is safe. It is not — the consumer still sees valid (whatever it powers up to) high over that zero data and treats the zero as a genuine result. The safety comes from the valid being low, not from the data being any particular value. Reset the valid chain; the data chain's contents are irrelevant while their valid is low.
7. DebugLab
The pipeline that fired early — a valid/data misalignment that leaks garbage at every fill and after every bubble
The engineering lesson: a pipeline register is always full, so the only thing that makes an output real is the valid bit — and the valid bit is trustworthy only if it is delayed by exactly the same number of stages as the data, reset to zero, and cleared on every flush. The tell is a bug that appears at the edges of a stream — the first few cycles of a burst, the cycles right after a gap, the cycles right after an abort — and vanishes the moment the pipe is driven permanently full. A pipeline that is correct under saturated back-to-back traffic and corrupts at fill, after a bubble, or after a flush is a valid-alignment bug, not a datapath bug. Three habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: pipeline the valid alongside the data through the same stages, reset the valid chain to zero (never mind the data), and clear every stage's valid on a flush — then qualify every use of the output with the final-stage valid. This is the exact substrate the valid/ready handshake (9.1) and the skid buffer (9.3) are built on: both are just a valid bit paired with data, plus a back-pressure signal that says when the pair may advance.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the valid-alignment / reset-clear / stall-flush reasoning behind the discipline.
Exercise 1 — Trace the fill and a bubble
For a DEPTH = 4 pipeline, hand-trace valid_pipe[0..3] from reset through this input in_valid stream: 1, 1, 0, 1, 1 (the 0 is a bubble), then all zeros. State the exact cycle out_valid first rises, confirm it stays low through the four fill cycles, and mark the one output cycle that is low because of the bubble. In one line each, say what out_data is driving on the fill cycles and why writing it would be a bug.
Exercise 2 — Place the stall and flush controls
Take the same DEPTH = 4 pipeline and add (a) a clock-enable stall and (b) a flush. For each, state precisely which registers it must affect and which it must leave alone, and write the one-line rule that keeps the (data, valid) pair intact under a stall. Then answer: if a stall and a flush are asserted in the same cycle, which should win, and why?
Exercise 3 — Find the misalignment on paper
An engineer pipelines the data through DEPTH = 3 stages but writes valid_pipe[2] <= valid_pipe[0] for the last stage (skipping stage 1). Describe exactly what goes wrong: is out_valid early, late, or the wrong shape? Walk the fill cycles to show it, and state whether a saturated back-to-back test would catch it. Then give the correct assignment and explain why the valid chain must have exactly as many flops as the data chain.
Exercise 4 — Verify without shipping the bug
Write the test plan (not the RTL) that would have caught the §7 misalignment before tape-out. Specify: the reference model you compare against and why it is trivially correct; the fill-cycle assertion that is the direct detector for an early valid; the bubble injection and what you assert DEPTH cycles later; the flush step and the post-flush check; and the one every-cycle invariant that ties out_data to out_valid. State which single check in your plan directly catches a valid that leads the data.
10. Related Tutorials
Continue in RTL Design Patterns (sibling and forward links unlock as they ship):
- Why Pipeline? — Chapter 8.1; the prerequisite that split a combinational path into registered stages and taught the alignment lesson — delay everything that must stay together with the data — which this page applies to validity.
- Latency vs Throughput — Chapter 8.2; the sibling that quantifies what pipelining buys and costs — the fill latency the valid bit gates and the throughput the bubbles interrupt.
- Pipeline Stalls & Flush — Chapter 8.4; the deep treatment of the clock-enable stall and the flush this page introduces — holding the pair and killing in-flight valids under real hazard control.
- Pipeline Hazards — Chapter 8.5; the data and control hazards that cause stalls and flushes — why you freeze a stage or abort the pipe in the first place.
- Pipelined Datapath — Chapter 8.6; a full multi-stage datapath where the valid-bit discipline qualifies every result across the whole compute path.
- valid/ready Handshake — Chapter 9.1; the flow-control protocol built directly on this page's valid bit — adding a backward
readyso the consumer can stall the producer, wherevalidmust hold untilreadyis seen. - Skid Buffer — Chapter 9.3; the register that catches the in-flight word when back-pressure deasserts — a valid-bit pipeline stage sized to absorb one cycle of latency in the handshake.
Backward / in-track dependencies:
- The Register Pattern (D-FF) — Chapter 2.1; the flop the data and valid registers are built from, and the reset-first clocked-update discipline every stage here follows.
- Register with Enable / Load — Chapter 2.2; the clock-enable that becomes the pipeline stall — holding a register's contents when not advancing is exactly the hold-the-pair mechanism.
- Shift Registers — Chapter 3.1; the data pipeline is a wide shift register, and the valid pipeline is a one-bit shift register running alongside it — the alignment is shift-register lockstep.
- Synchronous FIFO Architecture — Chapter 7.1; the FIFO that decouples a producer and consumer where this page's aligned valid feeds the write side, and the FIFO's occupancy is the elastic version of a bubble.
- Pulse & Strobe Generators — Chapter 5.4; the single-cycle valid strobe that must stay aligned to its data as it propagates — the same one-cycle-qualifier discipline in a narrower setting.
- Reset Strategy — Chapter 2.5; the reset discipline that clears the valid chain to zero — why resetting valid (and not the data) is what makes startup safe.
- The RTL Design Mindset — Chapter 0.2; the State and Verification lenses this page applies — the valid bits are the pipeline's control state, and qualifying the output is how you prove no garbage leaks out.
- What Is an RTL Design Pattern? — Chapter 0.1; why the valid-bit discipline earns the name — a recurring problem, a reusable form, a synthesis reality, and a signature failure.
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 pipeline shifts atomically each edge. - Initial & Always Blocks — the clocked
alwaysblock that advances both chains, and the initial/reset that clears the valid chain. - If-Else Statements — the reset / flush / enable priority (
if (rst) … else if (flush) … else if (en) …) that selects clear, invalidate, advance, or hold. - Physical Data Types — the vector widths (
WIDTHdata, one-bit valid) and register-array declarations whose deliberate sizing keeps the two chains the same length.
11. Summary
- A pipeline register is always full, but not every cycle is real. After reset the stages hold power-up
X; a gap in the producer sends a bubble down the pipe; an abort leaves garbage in flight. The output data bus is driven on every cycle, so from the data alone the consumer cannot tell a real result from startup noise, a bubble, or a flushed operation — and treating any of those as a result is the signature pipeline bug. - Carry a valid bit alongside the data through every stage. Add a one-bit valid register per data register, pipelined the same number of stages, on the same clock and enable, so a
(data, valid)pair entering at stage 0 stays paired todata_pipe[DEPTH-1]/valid_pipe[DEPTH-1].out_data = data_pipe[DEPTH-1],out_valid = valid_pipe[DEPTH-1], and the consumer usesout_dataonly whenout_validis high. Data tells you what; valid tells you when it is real. - Reset clears the valid chain — the data chain needs no reset. Because the valid flops reset to
0, every output cycle is invalid until a real operand has walked allDEPTHstages, so the undefined datapathXis never consumed. A bubble is simply an input cycle within_valid = 0; it flows through as an aligned invalid slot for free. - Stall holds the pair; flush clears all the valids. A clock-enable stall freezes a stage by holding both its data and valid registers as a unit, dropping and duplicating nothing. A flush invalidates everything in flight by clearing all the valid registers in one cycle, leaving the data untouched — the exact dual of reset applied at runtime, for aborts and mispredicts.
- The signature bug is valid/data misalignment. If the valid is delayed a different number of stages than the data — combinational from the input, under-delayed, or not reset-cleared — it marks the wrong cycle valid, and the consumer accepts a fill-
X, a bubble, or a neighbouring word. The tell is corruption at the edges of a stream (fill, after a bubble, after a flush) that vanishes when the pipe is driven permanently full. Built and self-check-verified here in SystemVerilog, Verilog, and VHDL — and the substrate the valid/ready handshake (9.1) and skid buffer (9.3) are built on.