RTL Design Patterns · Chapter 8 · Pipelining
Hazards & Forwarding (intro)
A pipeline is fast because operations overlap, and that overlap becomes the problem the moment those operations share a register file. An add computes a result bound for a register, and the very next subtract wants that register as a source, but the add has not written it back yet. When the subtract reads its operands, the register still holds the old value and it runs on stale data. Nothing crashed; the read simply returned the wrong number. This is a data hazard, a read-after-write across the pipeline, with two correct remedies. You can stall the consumer until the producer writes back, which costs cycles, or you can forward the in-flight result directly to the operand input, which is correct and fast. You build the read-after-write window, the bypass mux, nearest-producer-wins, and the load-use stall, in SystemVerilog, Verilog, and VHDL.
Intermediate16 min readRTL Design PatternsPipelineData HazardForwardingBypassRAWLoad-Use Stall
Chapter 8 · Section 8.5 · Pipelining
1. The Engineering Problem
You have a small pipelined datapath built on the register file from 6.3. Each operation flows through three phases: read operands from the register file, execute in the ALU, then write back the result. Because the pipeline overlaps operations, on any given cycle one operation is reading its operands while the operation ahead of it is executing and the one ahead of that is writing back. That overlap is exactly why the pipeline is fast — and it is exactly why a dependent operation can be handed the wrong data.
Run two back-to-back dependent operations. Operation A is add x5, x1, x2: it reads x1, x2, computes their sum, and will write x5 at writeback. Operation B, issued the very next cycle, is sub x9, x5, x3: it needs x5 as a source. But when B reaches the read-operands stage, A has not written x5 back yet — A is one stage ahead, its sum already computed and sitting on the ALU-output wire, but not yet stored in the register file. So B's read of x5 from the file returns the old value of x5, and B subtracts using stale data. The read did not fail; it returned a value; the value was simply the pre-add one. And the corruption only appears because A and B are adjacent and dependent — space them a few cycles apart and B reads a current file, passing every isolated test.
This is a data hazard — specifically a read-after-write (RAW) hazard — and it is intrinsic to any pipeline whose operations share state. It is the same read-during-write collision you met in 6.3, but stretched across pipeline stages: the write A owes is not one cycle away, it is several stages down the pipe, so the window during which the file is stale is a window, not an instant. Two other hazard families exist and are worth naming — a structural hazard is two operations wanting one physical resource in the same cycle (two memory accesses, one memory port), and a control hazard is a branch changing which operation is fetched next before the branch has resolved — but this page focuses on the data hazard and its remedy, because forwarding is the pattern you will reach for on nearly every pipeline you build.
// A 3-phase pipeline over a register file: read-operands -> execute -> writeback.
// Op A (cycle 0): add x5, x1, x2 // result for x5 computed at EXECUTE...
// // ...but NOT written to the file until WRITEBACK.
// Op B (cycle 1): sub x9, x5, x3 // reads x5 at READ-OPERANDS -- one cycle after A.
// WRONG -- B reads x5 straight from the register file:
// operand_a = regfile[rs_B]; // rs_B == x5, but A has not written x5 yet
// // => operand_a is the OLD x5 -> STALE subtract.
// The value B needs is not lost -- it is on A's ALU-output wire RIGHT NOW.
// Two fixes: STALL B until A writes back, or FORWARD A's result to B's operand.2. Mental Model
3. Pattern Anatomy
The pattern is built from three parts: a stage view that defines the hazard window, a forward compare that decides when to bypass, and an operand mux that carries out the bypass.
The stage view and the RAW window. Model each operation as three phases: read-operands (RO) fetches sources from the register file, execute (EX) runs the ALU and produces the result on a wire, and write-back (WB) stores that result into the file. In an overlapped pipeline, when consumer B is in RO, producer A is one stage ahead in EX, and the operation before A is in WB. A's result exists on the EX-output wire but has not reached WB yet — so the register file B reads is one write behind. The RAW window is the span from A's EX (result computed) to A's WB (result filed): for this 3-phase view it is a one-to-two-cycle window, and any consumer whose RO lands inside it reads a stale operand from the file.
The forward compare. To bypass, the datapath compares, for each consumer operand: does the producer's destination register equal this operand's source register (dest == src), and is the producer a valid operation (wb_valid, not a bubble — the valid-bit discipline from 8.3)? Only when both hold is the in-flight result the correct value for that operand. This compare runs per operand, per producing stage, every cycle.
The operand mux (the bypass). The operand feeding the ALU is not the register-file read alone — it is a mux selecting {register-file value, forwarded result from stage X}. When the forward compare fires, the mux picks the forwarded result; otherwise it picks the register-file read. When two producers ahead both match (both target x5), the mux must take the nearest (most recent) producer — the EX-stage forward outranks the WB-stage forward, because the nearer one will write last and is therefore the current value. This is a priority mux, and getting the priority backwards is a real bug.
The RAW window and the forwarding path — one consumer operand
data flowWhen forwarding cannot cover it — the load-use stall. Forwarding works because the value already exists on a wire by the time the consumer needs it. But some producers do not have their result ready in the EX stage — a load reads memory, and its data may not arrive until a stage later than the consumer's RO. There is no wire to forward from yet, so bypass is impossible; the only correct move is to stall the consumer for exactly one cycle (insert a bubble — the 8.4 mechanism), letting the load's data catch up, then forward it. This is the one case where the fast remedy fails and you fall back to the slow-but-correct one: a single load-use stall.
Where the register file's read/write policy interacts. The 6.3 same-register read-during-write policy sets the edge of the hazard window. A write-before-read (read-new) register file — where a write and a read to the same address in the same cycle returns the new data — closes the hazard one cycle earlier: the WB-stage write is visible to a same-cycle read, so you need no separate WB-to-RO forward path. A read-before-write (read-old) file leaves that last cycle stale, so you must forward from WB too. Choosing the read-new file is a cheap way to shrink the forwarding logic — a direct payoff from the 6.3 policy choice.
4. Real RTL Implementation
This is the core of the page. We build three families — (a) a small pipeline with a register file and a forwarding/bypass path, (b) the missing-forward stale-read bug vs the forwarding fix, and (c) a load-use one-cycle stall — and each is shown in SystemVerilog, Verilog, and VHDL, with an RTL block and a self-checking clocked testbench. The idea is identical in all three: a forward compare (dest == src AND producer valid) drives a priority mux on the operand input, nearest producer wins, and the load-use case stalls one cycle. Seeing them side by side is what makes the pattern language-independent in your head.
One discipline runs through every RTL block: an operand is never the register-file read alone — it is the register-file read plus a bypass mux, and the mux only selects an in-flight result when the destination matches the source and the producer is valid. Omit the valid check and a bubble's garbage forwards; omit the priority and the wrong producer wins; omit the whole compare and you are back to the stale read of §1.
4a. Pipeline with a register file and a forwarding path
We model a two-deep producer chain feeding a consumer: the EX-stage producer (nearest) and the WB-stage producer (older), each with a destination register and a valid bit, plus the register-file read. The operand is the priority mux: EX-forward beats WB-forward beats the register-file value. This is the whole forwarding pattern in one combinational block.
module fwd_unit #(
parameter int WIDTH = 32,
parameter int RBITS = 5 // register index width (32 regs)
)(
input logic [RBITS-1:0] rs, // this operand's SOURCE register
input logic [WIDTH-1:0] rf_data, // value read from the register file
// nearest producer, in EX (its result is on the ALU-output wire this cycle):
input logic [RBITS-1:0] ex_rd, // EX producer DESTINATION
input logic ex_valid, // EX producer is a real op (8.3 valid bit)
input logic [WIDTH-1:0] ex_data, // EX producer's forwarded result
// older producer, in WB (one stage further along):
input logic [RBITS-1:0] wb_rd, // WB producer DESTINATION
input logic wb_valid, // WB producer is a real op
input logic [WIDTH-1:0] wb_data, // WB producer's forwarded result
output logic [WIDTH-1:0] operand // the value that actually feeds the ALU
);
// Forward compare: a producer supplies this operand only when it WRITES the
// register this operand READS (rd == rs) AND it is a VALID op (not a bubble).
// Priority: the NEAREST producer (EX) outranks the older one (WB), because it
// will write last and is therefore the current value. Fall back to the file.
always_comb begin
if (ex_valid && ex_rd == rs) operand = ex_data; // nearest wins
else if (wb_valid && wb_rd == rs) operand = wb_data; // older producer
else operand = rf_data; // no hazard: use file
end
endmoduleThe if / else if / else chain is the nearest-producer priority: the EX arm is tested first, so when both EX and WB target rs the EX value is taken. The clocked testbench builds a tiny pipeline, issues back-to-back dependent ops, and checks the consumer's operand against a reference model that computes the true data dependency — the value the most recent writer of rs produced.
module fwd_unit_tb;
localparam int WIDTH = 32, RBITS = 5;
logic [RBITS-1:0] rs, ex_rd, wb_rd;
logic ex_valid, wb_valid;
logic [WIDTH-1:0] rf_data, ex_data, wb_data, operand, exp;
int errors = 0;
fwd_unit #(.WIDTH(WIDTH), .RBITS(RBITS)) dut (.*);
// Reference: the operand MUST be the value of the newest valid writer of rs,
// or the register-file value if no in-flight op writes rs.
function automatic logic [WIDTH-1:0] ref_operand;
if (ex_valid && ex_rd == rs) return ex_data;
else if (wb_valid && wb_rd == rs) return wb_data;
else return rf_data;
endfunction
initial begin
// Case 1: no hazard -> read the register file.
rs=5; rf_data=32'h1111; ex_rd=9; ex_valid=1; ex_data=32'hAAAA;
wb_rd=7; wb_valid=1; wb_data=32'hBBBB; #1;
exp = ref_operand;
assert (operand === exp) else begin $error("no-hazard: %h != %h", operand, exp); errors++; end
// Case 2: EX producer writes rs -> forward from EX (nearest).
rs=5; ex_rd=5; ex_valid=1; ex_data=32'hCAFE; wb_rd=5; wb_valid=1; wb_data=32'hDEAD; #1;
exp = ref_operand;
assert (operand === exp && operand === 32'hCAFE)
else begin $error("EX-forward should win: %h", operand); errors++; end
// Case 3: only WB writes rs (EX targets a different reg) -> forward from WB.
rs=5; ex_rd=8; ex_valid=1; wb_rd=5; wb_valid=1; wb_data=32'h1234; #1;
exp = ref_operand;
assert (operand === exp && operand === 32'h1234)
else begin $error("WB-forward expected: %h", operand); errors++; end
// Case 4: producer matches rs but is a BUBBLE (valid=0) -> do NOT forward.
rs=5; rf_data=32'h5A5A; ex_rd=5; ex_valid=0; wb_rd=5; wb_valid=0; #1;
exp = ref_operand;
assert (operand === exp && operand === 32'h5A5A)
else begin $error("bubble must not forward: %h", operand); errors++; end
if (errors==0) $display("PASS: forwarding picks the newest valid producer, else the file");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog form is identical in intent; the only differences are wire/reg typing and an explicit port map instead of .*. The if / else if / else priority is the same nearest-producer rule.
module fwd_unit #(
parameter WIDTH = 32,
parameter RBITS = 5
)(
input wire [RBITS-1:0] rs,
input wire [WIDTH-1:0] rf_data,
input wire [RBITS-1:0] ex_rd,
input wire ex_valid,
input wire [WIDTH-1:0] ex_data,
input wire [RBITS-1:0] wb_rd,
input wire wb_valid,
input wire [WIDTH-1:0] wb_data,
output reg [WIDTH-1:0] operand
);
// Forward only when a VALID producer's destination equals this source.
// EX tested first => nearest producer wins; else fall back to the file.
always @(*) begin
if (ex_valid && ex_rd == rs) operand = ex_data;
else if (wb_valid && wb_rd == rs) operand = wb_data;
else operand = rf_data;
end
endmodule module fwd_unit_tb;
parameter WIDTH = 32, RBITS = 5;
reg [RBITS-1:0] rs, ex_rd, wb_rd;
reg ex_valid, wb_valid;
reg [WIDTH-1:0] rf_data, ex_data, wb_data;
wire [WIDTH-1:0] operand;
reg [WIDTH-1:0] exp;
integer errors;
fwd_unit #(.WIDTH(WIDTH), .RBITS(RBITS)) dut
(.rs(rs), .rf_data(rf_data), .ex_rd(ex_rd), .ex_valid(ex_valid), .ex_data(ex_data),
.wb_rd(wb_rd), .wb_valid(wb_valid), .wb_data(wb_data), .operand(operand));
// Reference model: newest valid writer of rs, else the register-file value.
task ref_operand(output reg [WIDTH-1:0] r);
begin
if (ex_valid && ex_rd == rs) r = ex_data;
else if (wb_valid && wb_rd == rs) r = wb_data;
else r = rf_data;
end
endtask
initial begin
errors = 0;
// no hazard -> file
rs=5; rf_data=32'h1111; ex_rd=9; ex_valid=1; ex_data=32'hAAAA;
wb_rd=7; wb_valid=1; wb_data=32'hBBBB; #1;
ref_operand(exp);
if (operand !== exp) begin $display("FAIL no-hazard: %h!=%h", operand, exp); errors=errors+1; end
// EX writes rs -> forward EX (nearest) even though WB also matches
rs=5; ex_rd=5; ex_valid=1; ex_data=32'hCAFE; wb_rd=5; wb_valid=1; wb_data=32'hDEAD; #1;
ref_operand(exp);
if (operand !== exp || operand !== 32'hCAFE) begin $display("FAIL EX-fwd: %h", operand); errors=errors+1; end
// only WB matches -> forward WB
rs=5; ex_rd=8; ex_valid=1; wb_rd=5; wb_valid=1; wb_data=32'h1234; #1;
ref_operand(exp);
if (operand !== exp || operand !== 32'h1234) begin $display("FAIL WB-fwd: %h", operand); errors=errors+1; end
// matching producer is a bubble -> do NOT forward
rs=5; rf_data=32'h5A5A; ex_rd=5; ex_valid=0; wb_rd=5; wb_valid=0; #1;
ref_operand(exp);
if (operand !== exp || operand !== 32'h5A5A) begin $display("FAIL bubble: %h", operand); errors=errors+1; end
if (errors==0) $display("PASS: forwarding picks newest valid producer, else file");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the same priority is an if / elsif / else in a combinational process. numeric_std gives the register indices as std_logic_vector compared directly; the valid bits are std_logic. The structure — compare, then a priority mux on the operand — is identical.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity fwd_unit is
generic ( WIDTH : positive := 32; RBITS : positive := 5 );
port (
rs : in std_logic_vector(RBITS-1 downto 0); -- operand SOURCE reg
rf_data : in std_logic_vector(WIDTH-1 downto 0); -- register-file read
ex_rd : in std_logic_vector(RBITS-1 downto 0); -- EX producer dest
ex_valid : in std_logic; -- EX producer valid
ex_data : in std_logic_vector(WIDTH-1 downto 0); -- EX forwarded result
wb_rd : in std_logic_vector(RBITS-1 downto 0); -- WB producer dest
wb_valid : in std_logic; -- WB producer valid
wb_data : in std_logic_vector(WIDTH-1 downto 0); -- WB forwarded result
operand : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of fwd_unit is
begin
-- EX arm first => nearest producer wins; only forward a VALID producer whose
-- destination equals this source; else use the register-file value.
process (all)
begin
if ex_valid = '1' and ex_rd = rs then operand <= ex_data;
elsif wb_valid = '1' and wb_rd = rs then operand <= wb_data;
else operand <= rf_data;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity fwd_unit_tb is
end entity;
architecture sim of fwd_unit_tb is
constant WIDTH : positive := 32;
constant RBITS : positive := 5;
signal rs, ex_rd, wb_rd : std_logic_vector(RBITS-1 downto 0);
signal ex_valid, wb_valid : std_logic;
signal rf_data, ex_data, wb_data, operand : std_logic_vector(WIDTH-1 downto 0);
function ref_op(rs, ex_rd, wb_rd : std_logic_vector;
ex_v, wb_v : std_logic;
rf, ex_d, wb_d : std_logic_vector) return std_logic_vector is
begin
if ex_v = '1' and ex_rd = rs then return ex_d;
elsif wb_v = '1' and wb_rd = rs then return wb_d;
else return rf; end if;
end function;
begin
dut : entity work.fwd_unit
generic map (WIDTH => WIDTH, RBITS => RBITS)
port map (rs => rs, rf_data => rf_data, ex_rd => ex_rd, ex_valid => ex_valid,
ex_data => ex_data, wb_rd => wb_rd, wb_valid => wb_valid,
wb_data => wb_data, operand => operand);
stim : process
begin
-- no hazard -> file value
rs <= "00101"; rf_data <= x"00001111"; ex_rd <= "01001"; ex_valid <= '1';
ex_data <= x"0000AAAA"; wb_rd <= "00111"; wb_valid <= '1'; wb_data <= x"0000BBBB";
wait for 1 ns;
assert operand = ref_op(rs,ex_rd,wb_rd,ex_valid,wb_valid,rf_data,ex_data,wb_data)
report "no-hazard: operand /= file value" severity error;
-- EX and WB both match rs -> nearest (EX) wins
ex_rd <= "00101"; ex_data <= x"0000CAFE"; wb_rd <= "00101"; wb_data <= x"0000DEAD";
wait for 1 ns;
assert operand = x"0000CAFE" report "EX-forward should win" severity error;
-- only WB matches -> forward WB
ex_rd <= "01000"; wb_rd <= "00101"; wb_data <= x"00001234"; wait for 1 ns;
assert operand = x"00001234" report "WB-forward expected" severity error;
-- matching producer is a bubble -> use file
rf_data <= x"00005A5A"; ex_rd <= "00101"; ex_valid <= '0'; wb_valid <= '0'; wait for 1 ns;
assert operand = x"00005A5A" report "bubble must not forward" severity error;
report "fwd_unit self-check complete" severity note;
wait;
end process;
end architecture;4b. The missing-forward stale-read bug — buggy vs fixed, in all three HDLs
This is the signature failure, shown as buggy vs fixed RTL in each language and dramatized in §7. The bug is the same everywhere: the operand is taken straight from the register-file read, with no bypass, so inside the RAW window a dependent consumer reads the stale filed value while the producer's result is still in flight. The fix inserts the forward compare and mux from 4a.
// BUGGY: the operand is the register-file read ALONE. Inside the RAW window the
// producer has computed its result but not written it back, so a
// back-to-back dependent consumer reads the STALE filed value.
module operand_bad #(parameter WIDTH=32, RBITS=5)(
input logic [RBITS-1:0] rs,
input logic [WIDTH-1:0] rf_data,
input logic [RBITS-1:0] ex_rd, input logic ex_valid, input logic [WIDTH-1:0] ex_data,
output logic [WIDTH-1:0] operand
);
assign operand = rf_data; // NO bypass -> stale on a RAW hazard
endmodule
// FIXED: forward the producer's in-flight result when it writes this source AND
// is valid; else use the file. (One producer shown; extend per 4a.)
module operand_good #(parameter WIDTH=32, RBITS=5)(
input logic [RBITS-1:0] rs,
input logic [WIDTH-1:0] rf_data,
input logic [RBITS-1:0] ex_rd, input logic ex_valid, input logic [WIDTH-1:0] ex_data,
output logic [WIDTH-1:0] operand
);
// dest == src AND producer valid -> bypass the in-flight result.
assign operand = (ex_valid && ex_rd == rs) ? ex_data : rf_data;
endmodule module stale_op_tb;
localparam int WIDTH=32, RBITS=5;
logic [RBITS-1:0] rs, ex_rd;
logic ex_valid;
logic [WIDTH-1:0] rf_data, ex_data, operand, exp;
int errors = 0;
// Point at operand_good to PASS; at operand_bad to observe the stale read.
operand_good #(.WIDTH(WIDTH), .RBITS(RBITS)) dut
(.rs(rs), .rf_data(rf_data), .ex_rd(ex_rd), .ex_valid(ex_valid),
.ex_data(ex_data), .operand(operand));
initial begin
// Isolated op (no producer in flight): reading the file is correct.
rs=5; rf_data=32'h0000_0007; ex_rd=9; ex_valid=1; ex_data=32'hFF; #1;
exp = 32'h0000_0007; // no hazard -> file value
assert (operand === exp) else begin $error("isolated: %h!=%h", operand, exp); errors++; end
// Back-to-back dependent: EX producer writes x5, consumer reads x5.
// The TRUE operand is the producer's fresh result, not the stale file.
rs=5; rf_data=32'h0000_0007; ex_rd=5; ex_valid=1; ex_data=32'h0000_00AB; #1;
exp = 32'h0000_00AB; // forwarded value is correct
assert (operand === exp)
else begin $error("RAW hazard read STALE: got %h want %h", operand, exp); errors++; end
if (errors==0) $display("PASS: dependent op sees the forwarded result, not the stale file");
else $display("FAIL: %0d mismatches (stale read on the RAW window)", errors);
$finish;
end
endmoduleThe Verilog buggy/fixed pair is the same story with wire/reg and a ternary bypass. Issuing the back-to-back dependent pair is what exposes the stale read — the isolated op passes either way.
// BUGGY: operand is the register-file read alone -> stale inside the RAW window.
module operand_bad #(parameter WIDTH=32, RBITS=5)(
input wire [RBITS-1:0] rs,
input wire [WIDTH-1:0] rf_data,
input wire [RBITS-1:0] ex_rd,
input wire ex_valid,
input wire [WIDTH-1:0] ex_data,
output wire [WIDTH-1:0] operand
);
assign operand = rf_data; // NO bypass -> stale on a RAW hazard
endmodule
// FIXED: bypass the in-flight result when it writes this source and is valid.
module operand_good #(parameter WIDTH=32, RBITS=5)(
input wire [RBITS-1:0] rs,
input wire [WIDTH-1:0] rf_data,
input wire [RBITS-1:0] ex_rd,
input wire ex_valid,
input wire [WIDTH-1:0] ex_data,
output wire [WIDTH-1:0] operand
);
assign operand = (ex_valid && ex_rd == rs) ? ex_data : rf_data;
endmodule module stale_op_tb;
parameter WIDTH=32, RBITS=5;
reg [RBITS-1:0] rs, ex_rd;
reg ex_valid;
reg [WIDTH-1:0] rf_data, ex_data;
wire [WIDTH-1:0] operand;
reg [WIDTH-1:0] exp;
integer errors;
// Bind to operand_good to PASS; to operand_bad to observe the stale read.
operand_good #(.WIDTH(WIDTH), .RBITS(RBITS)) dut
(.rs(rs), .rf_data(rf_data), .ex_rd(ex_rd), .ex_valid(ex_valid),
.ex_data(ex_data), .operand(operand));
initial begin
errors = 0;
// isolated op: reading the file is correct
rs=5; rf_data=32'h00000007; ex_rd=9; ex_valid=1; ex_data=32'hFF; #1;
exp = 32'h00000007;
if (operand !== exp) begin $display("FAIL isolated: %h!=%h", operand, exp); errors=errors+1; end
// back-to-back dependent: producer writes x5, consumer reads x5
rs=5; rf_data=32'h00000007; ex_rd=5; ex_valid=1; ex_data=32'h000000AB; #1;
exp = 32'h000000AB; // forwarded value, not the stale file
if (operand !== exp) begin
$display("FAIL RAW: read STALE got %h want %h", operand, exp);
errors = errors + 1;
end
if (errors==0) $display("PASS: dependent op sees forwarded result, not stale file");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the bug is the concurrent assignment operand <= rf_data; with no bypass; the fix is a when/else that selects the forwarded value under the dest == src AND valid condition. Same window, same remedy.
library ieee;
use ieee.std_logic_1164.all;
-- BUGGY: operand is the register-file read alone -> stale on a RAW hazard.
entity operand_bad is
generic ( WIDTH : positive := 32; RBITS : positive := 5 );
port (
rs : in std_logic_vector(RBITS-1 downto 0);
rf_data : in std_logic_vector(WIDTH-1 downto 0);
ex_rd : in std_logic_vector(RBITS-1 downto 0);
ex_valid : in std_logic;
ex_data : in std_logic_vector(WIDTH-1 downto 0);
operand : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of operand_bad is
begin
operand <= rf_data; -- NO bypass -> stale inside the window
end architecture;
-- FIXED: forward the in-flight result when it writes this source and is valid.
library ieee;
use ieee.std_logic_1164.all;
entity operand_good is
generic ( WIDTH : positive := 32; RBITS : positive := 5 );
port (
rs : in std_logic_vector(RBITS-1 downto 0);
rf_data : in std_logic_vector(WIDTH-1 downto 0);
ex_rd : in std_logic_vector(RBITS-1 downto 0);
ex_valid : in std_logic;
ex_data : in std_logic_vector(WIDTH-1 downto 0);
operand : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of operand_good is
begin
-- dest = src AND producer valid -> bypass; else the register-file value.
operand <= ex_data when (ex_valid = '1' and ex_rd = rs) else rf_data;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity stale_op_tb is
end entity;
architecture sim of stale_op_tb is
constant WIDTH : positive := 32;
constant RBITS : positive := 5;
signal rs, ex_rd : std_logic_vector(RBITS-1 downto 0);
signal ex_valid : std_logic;
signal rf_data, ex_data, operand : std_logic_vector(WIDTH-1 downto 0);
begin
-- Bind to operand_good to PASS; to operand_bad to observe the stale read.
dut : entity work.operand_good
generic map (WIDTH => WIDTH, RBITS => RBITS)
port map (rs => rs, rf_data => rf_data, ex_rd => ex_rd,
ex_valid => ex_valid, ex_data => ex_data, operand => operand);
stim : process
begin
-- isolated op: file read is correct
rs <= "00101"; rf_data <= x"00000007"; ex_rd <= "01001";
ex_valid <= '1'; ex_data <= x"000000FF"; wait for 1 ns;
assert operand = x"00000007" report "isolated: operand /= file" severity error;
-- back-to-back dependent: EX producer writes x5, consumer reads x5
ex_rd <= "00101"; ex_data <= x"000000AB"; wait for 1 ns;
assert operand = x"000000AB"
report "RAW hazard read STALE instead of forwarded value" severity error;
report "stale_op self-check complete" severity note;
wait;
end process;
end architecture;4c. The load-use case — one unavoidable stall
Forwarding covers a producer whose result is ready by the time the consumer needs it. A load is the exception: its data is not available in the producer's EX stage, so there is no wire to bypass yet. The hazard-detection logic spots a load in EX whose destination matches the next op's source and asserts stall for exactly one cycle — inserting a bubble (8.4) — after which the load's data can be forwarded. Here is the detection, shown in all three HDLs.
module load_use #(parameter RBITS=5)(
input logic [RBITS-1:0] use_rs1, use_rs2, // the consumer's two sources
input logic [RBITS-1:0] ex_rd, // dest of the op in EX
input logic ex_is_load, // is that op a load? (data late)
input logic ex_valid, // is it a real op (not a bubble)?
output logic stall // hold consumer 1 cycle, insert bubble
);
// A load's result is not on a forwardable wire in EX. If the next op uses the
// load's destination as a source, we CANNOT forward this cycle -> stall once.
assign stall = ex_valid & ex_is_load &
((ex_rd == use_rs1) | (ex_rd == use_rs2));
endmodule module load_use #(parameter RBITS=5)(
input wire [RBITS-1:0] use_rs1, use_rs2,
input wire [RBITS-1:0] ex_rd,
input wire ex_is_load,
input wire ex_valid,
output wire stall
);
// load in EX whose dest feeds the next op's source -> not forwardable now.
assign stall = ex_valid & ex_is_load &
((ex_rd == use_rs1) | (ex_rd == use_rs2));
endmodule library ieee;
use ieee.std_logic_1164.all;
entity load_use is
generic ( RBITS : positive := 5 );
port (
use_rs1, use_rs2 : in std_logic_vector(RBITS-1 downto 0);
ex_rd : in std_logic_vector(RBITS-1 downto 0);
ex_is_load : in std_logic;
ex_valid : in std_logic;
stall : out std_logic
);
end entity;
architecture rtl of load_use is
begin
-- load in EX whose destination is a source of the next op -> stall one cycle.
stall <= '1' when (ex_valid = '1' and ex_is_load = '1'
and (ex_rd = use_rs1 or ex_rd = use_rs2)) else '0';
end architecture;Across all three languages the load-use rule is one sentence: a load's data is not ready to forward in EX, so if the next op needs it, stall exactly one cycle and forward on the next. Combine 4a, 4b, and 4c and you have the complete data-hazard policy: forward whenever the value exists on a wire, stall the single case where it does not, and never read a register a still-in-flight op will change.
5. Verification Strategy
A forwarding datapath is correct when its result matches a dependency-aware reference model — a model that, for every operand, uses the value of the most recent in-flight writer of that register, or the register-file value if none. The single invariant that captures a correct forwarding unit is:
Every operand equals the value the newest valid producer of its source register produced; if no in-flight op writes that source, it equals the register-file value — never the stale filed value inside the RAW window.
Everything below is that one invariant, made checkable, and it maps directly onto the testbenches in §4.
- Back-to-back dependent ops (self-checking). Drive the classic pair: write
x5, then immediately readx5. A dependency-correct reference computes the true operand (the producer's fresh result); the testbench asserts the datapath's operand equals it. The threefwd_unit/stale_optestbenches in §4 do exactly this — SVassert (operand === exp), Verilogif (operand !== exp) $display("FAIL…"), VHDLassert operand = … report … severity error. - Prove the un-forwarded version reads stale. Point the same testbench at the no-forwarding DUT (
operand_bad) and confirm the back-to-back dependent case now fails — the operand equals the stale register-file value, not the producer's result. This is the falsification step: the test that catches the bug must be shown to actually catch it, or you have a test that passes on broken RTL. - Nearest-producer-wins. Set up two in-flight producers that both target the source register (
EXandWBboth writex5) with different data, and assert the operand equals the EX (nearest) value, not the WB one. Getting the priority backwards passes a naive single-producer test and fails only here — so this case is mandatory. - Valid/bubble qualification. Drive a producer whose destination matches the source but whose
validis low (a bubble). Assert the unit does not forward — it must fall back to the register-file value. Omitting the valid check forwards a bubble's garbage; this case is what exposes that. - Load-use single stall. Drive a load in
EXwhose destination feeds the next op's source and assertstallis high for exactly one cycle (not zero, not two). Then confirm that after the one bubble, the load's data forwards correctly. The corner is the count: a load-use that forwards with no stall reads garbage; one that stalls forever hangs. - Expected waveform. On a correct run, when a dependent op enters
ROinside the RAW window, its operand wire shows the forwarded value the cycle the producer'sEXresult is available — not the stale file value — and thestallline pulses high for exactly one cycle only on a load-use. The visual signature of the bug is an operand that tracks the old register-file value for the dependent op, corrupting only the back-to-back pair.
6. Common Mistakes
Hazard not detected — reading the register file while the producer is in flight. The signature bug. The operand is taken straight from the register-file read with no bypass, so inside the RAW window a dependent consumer reads the stale filed value. It passes every isolated-op test and corrupts only back-to-back dependents, so it survives casual testing and shows up as data-dependent, order-sensitive wrongness in system runs. The fix is the forward compare and mux (§4a, §4b); the verification that catches it is the back-to-back dependent pair with a dependency-aware reference (§5).
Forwarding from the wrong stage, or without the valid/destination compare. A bypass that forwards unconditionally — no dest == src check, or no valid check — is worse than no bypass. Skip the destination compare and you forward a producer's result to an operand that did not want it. Skip the valid check and you forward a bubble's garbage as if it were a real result (this is exactly why the valid bit of 8.3 is load-bearing here). Forward from a stage whose result is not actually ready and you bypass a value that has not been computed. Always: forward only a valid producer whose destination equals this source, from a stage whose result truly exists on a wire.
Wrong priority when two producers target the same register — not taking the most recent. When two ops ahead of the consumer both write the source register, the operand must come from the nearest (most recent) producer — the one that will write last. Take the older one and you forward a value the newer op has already superseded. The priority mux must test the nearest stage first (EX before WB); getting it backwards is a subtle bug that passes a single-producer test and fails only on a double-write.
Load-use not stalled — forwarding a value that is not ready yet. A load's data is not available in EX, so there is no wire to forward. Treating a load like any other producer bypasses a value that does not exist yet, reading garbage. The load-use case must be detected and stalled exactly one cycle (§4c); it is the one hazard forwarding cannot solve, and forgetting it is a classic pipeline bug.
Assuming the register file's read-during-write policy for you. Whether the last cycle of the RAW window is stale depends on the 6.3 read-old vs read-new policy. Assume a write-before-read (read-new) file when you actually have a read-old one and you will drop the WB-to-RO forward path that read-old requires — leaving a one-cycle-wide hazard hole. Match the forwarding logic to the file's actual same-cycle policy, or make the file read-new deliberately to shrink the logic.
7. DebugLab
The subtract that used yesterday's value — a missing forward reads stale
The engineering lesson: in a pipeline that shares state, a value can be computed but not yet written back — so reading the register file inside that window returns a stale operand. The tell is a bug that depends on distance: correctness that changes with how many cycles apart two operations sit means you have an unhandled read-after-write hazard, not a functional-logic error. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: make every operand a bypass mux — forward a valid producer's in-flight result when its destination equals this source, nearest producer first, and stall the one load-use case forwarding cannot cover — and verify with back-to-back dependent ops against a dependency-aware reference, then prove the un-forwarded version reads stale.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the "never read state a still-in-flight op will change" habit.
Exercise 1 — Size the hazard window
A pipeline has stages IF → RO → EX → MEM → WB, and a result is produced (available on a wire) at the end of EX, then written to the register file at WB. For an ALU op, list every downstream operation position (how many cycles behind) that could read a stale operand from the register file if there were no forwarding — i.e. the width of the RAW window in cycles — and state, for each, which stage you would forward from. Then say how the window changes if the register file is write-before-read (read-new) versus read-before-write (read-old).
Exercise 2 — Design the forward compare
Write, in words, the exact boolean condition that must be true to forward the EX-stage result to a consumer's first source operand rs1. Include every term (destination match, valid, and anything else you need) and explain why each term is necessary by describing the specific wrong behaviour that results if you drop that term. Then extend the condition to a two-source op (rs1 and rs2) with two forwardable stages (EX and MEM), and state the priority order and why.
Exercise 3 — Break the forwarding unit on paper
Two ops ahead of a consumer both write register x5: the nearer one (in EX) will compute x5 = 100, the older one (in MEM) already holds x5 = 42. The consumer reads x5. (i) Which value is correct, and why? (ii) What does a forwarding unit that tests the older stage first return, and why does that pass a test with only one producer? (iii) Describe the single directed test you would add to each of a SystemVerilog, a Verilog, and a VHDL testbench to catch a reversed priority, and why an isolated dependent pair does not catch it.
Exercise 4 — Decide stall vs forward
For each dependency, state whether forwarding alone suffices or a one-cycle stall is unavoidable, and why: (a) add x5,x1,x2 then sub x9,x5,x3; (b) lw x5,0(x1) then sub x9,x5,x3; (c) add x5,x1,x2 then (two cycles later) sub x9,x5,x3; (d) lw x5,0(x1) then (two cycles later) sub x9,x5,x3. Then explain why a load's result cannot be forwarded in the same cycle an ALU result can, in terms of which stage produces the value.
10. Related Tutorials
Continue in RTL Design Patterns (sibling and forward links unlock as they ship):
- The Valid-Bit Discipline — Chapter 8.3; the valid bit that qualifies a forward — you only bypass a valid producer, never a bubble's garbage. The direct prerequisite.
- Pipeline Stalls & Flush — Chapter 8.4; the stall/bubble mechanism this page uses as the load-use remedy and the fallback when forwarding cannot reach.
- Pipelined Datapath — Chapter 8; the full multi-stage datapath these hazards and forwards live inside, built end to end.
- valid/ready Handshake — Chapter 9.1; the stream-level cousin — backpressure between producer and consumer, built on the same valid substrate.
In-track dependencies (the structures this pattern builds on):
- Multi-Port Memories & Register Files — Chapter 6.3; the 2R1W register file whose same-register read-during-write becomes the cross-stage RAW window here; its read-old vs read-new policy sets the hazard-window edge.
- Dual-Port RAM — Chapter 6; the read/write-port concurrency and same-address collision that underlie the register file's hazard behaviour.
- Why Pipeline? — Chapter 8.1; the overlap that buys throughput and, as a consequence, creates the in-flight window a hazard exploits.
- Latency vs Throughput — Chapter 8.2; why a stall costs throughput and forwarding preserves it — the trade-off behind choosing bypass over bubble.
- ALU Construction — Chapter 5; the execute stage whose result is the value forwarded, and whose operand inputs the bypass mux feeds.
- FSM + Datapath — Chapter 4.6; the control/datapath split the hazard-detection and forwarding logic slots into.
Backward / method:
- The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses — a hazard is a control problem over shared state, solved with a datapath bypass.
- What Is an RTL Design Pattern? — Chapter 0.1; why forwarding earns the name "pattern" — a recurring problem, a reusable bypass form, a synthesis reality, and a signature failure.
Verilog v1 prerequisites (the grammar this pattern transcribes into):
- Case Statements — the construct behind the priority forward mux (nearest producer first).
- If-Else Statements — the
if / else if / elsechain that expresses the nearest-producer priority in the forwarding unit. - Blocking and Non-Blocking Assignments — why the combinational forward mux uses blocking (
=) and the pipeline registers use non-blocking (<=). - Physical Data Types — the
reg/wireand vector widths the operand and register-index buses are declared with.
11. Summary
- A data hazard is a read-after-write across pipeline stages. When operations that share a register file overlap, a consumer can read a register in an early stage while a producer ahead of it will write that register in a later stage — the consumer reads the stale filed value because the producer's result is still in flight. The bug appears only on back-to-back dependent ops and depends on how far apart they sit.
- The hazard is a window, sized by the stages between compute and writeback. From the cycle a result is produced (on the ALU-output wire) to the cycle it is written back, the register file is one write behind; any read inside that window is stale. The register file's read-old vs read-new policy (6.3) sets the window's trailing edge.
- Two remedies: stall or forward. Stall holds the consumer (bubbles) until the producer writes back — correct, costs cycles. Forward/bypass routes the producer's in-flight result directly to the consumer's operand input, skipping the register-file round trip — correct and fast. Real pipelines forward wherever possible and stall only where they must.
- Forwarding is a compare feeding a priority mux. The operand is a mux over
{register-file value, forwarded result from stage X}, selected when the producer's destination equals this source and the producer is valid (the 8.3 valid bit — never forward a bubble). When two producers ahead target the same register, the nearest (most recent) producer wins — a priority mux, EX before WB. - The load-use case is the one forwarding cannot cover. A load's data is not on a forwardable wire in EX, so a dependent consumer must stall exactly one cycle (insert a bubble), then forward. The whole policy is one sentence: detect the read-after-write window and either bypass the in-flight result or stall until it lands — never read state a still-in-flight op will change. Verify with back-to-back dependent ops against a dependency-aware reference, then prove the un-forwarded version reads stale — self-checking testbenches shown here in SystemVerilog, Verilog, and VHDL.