Run ADDI r1 = r0 + 5 and then ADD r2 = r1 + r1 on a three-stage pipe without forwarding, and r2 ends up 0, not 10. There is no stall, no X and no error: the second instruction read r1 one cycle before the first one wrote it. This is a read-after-write (RAW) data hazard, the third hazard kind named in the hub.
Follow one instruction from left to right: D reads its operands, X adds, and W writes the register file as the instruction leaves.
The black loop at the bottom is the only way into the register file, so a result still in X or W is invisible to a plain register read.
The X path carries the result of the instruction one ahead, straight from the adder.
The W path carries the result of the instruction two ahead. A load's data first appears here, because the memory answers one cycle after it gets the address.
The instruction offered at the input is in D. It names op, rd, rs1, rs2 and an 8-bit immediate. ADD adds two registers, ADDI adds the immediate, and LD adds the immediate to compute an address and loads the memory word there. There are four registers, r0 to r3, all reset to 0; r0 is an ordinary register here. Memory word i holds 0x20 + i. The full RTL is under Testbench below.
# | Instruction | Correct result | Reads a result from |
|---|---|---|---|
I0 | ADDI r1 = r0 + 5 | r1 = 05 | nothing in flight |
I1 | ADD r2 = r1 + r1 | r2 = 0a | I0, one ahead |
I2 | ADD r3 = r2 + r1 | r3 = 0f | I1, one ahead; I0, two ahead |
I3 | LD r1 = mem[r0 + 3] | r1 = 23 | nothing in flight |
I4 | ADD r2 = r1 + r3 | r2 = 32 | I3 (a load), one ahead; I2, two ahead |
Read op_a and op_b in each slot and compare them with the values the table says the instruction needs.
Slot 1, the first stale read: I1 reads r1 as 0 on both operands. I0 is in X, and its 5 reaches the register file only as slot 2 ends.
Slot 2: I2 reads r2 = 0 and r1 = 0, both stale.
Slot 4: I4 reads r1 = 5, the value before the load, and r3 = 0.
wb, slots 2 to 6: the pipe writes 5, 0, 0, 23 and 5 where the program needs 5, 0a, 0f, 23 and 32. Slots 3 and 4 both write 0, so the lane shows one long value. Every wrong result is an ordinary number.
The fix is a mux in front of each operand. If an older instruction still in X or W writes the register this one reads, take its result from the pipe instead of from the register file. Two older instructions can write the same register; the one in X is younger in program order, so its value is the current one and X is tested first.
// snippet
// Forwarding mux, one per operand: the youngest producer wins
always_comb begin
if (FWD && x_valid && x_rd == rs1) fwd_a = x_sum; // 1 ahead, in X: youngest first
else if (FWD && w_valid && w_rd == rs1) fwd_a = w_val; // 2 ahead, in W
else fwd_a = rf[rs1]; // 3 or more ahead: written
endThe X path runs adder, mux and register in one cycle, which is often the critical path of a short pipe. That timing cost buys a pipe that never waits for an ALU result.
A load in X has only its address in x_sum; the data leaves the memory as the load enters W. An instruction right behind a load cannot get the data in time, so D holds it for one cycle and sends a bubble into X. One cycle later the load is in W and the normal W path forwards its data.
uses_rs2 is 1 only for ADD: ADDI and LD take the immediate instead, so a match on their rs2 field is not a hazard. The RTL also skips NOPs. While stall is high, in_ready is low and the source holds the instruction, as in valid/ready flow control.
Compare each operand with the table again; stall marks the slot in which D holds its instruction.
Slot 1: op_a = op_b = 5, forwarded from X (I0 is one ahead).
Slot 2: op_a = 0a from X (I1) and op_b = 5 from W (I0).
Slot 4: stall = 1. op_a shows 3, the load's address in X, not its data. These operands are dropped and I4 is offered again.
Slot 5: op_a = 23 from W (the load) and op_b = 0f from the register file. The bubble writes nothing in slot 6, and slot 7 writes r2 = 32.
The bench offers 3000 random instructions over random registers, with random gaps, and holds any offer that is stalled. A reference model runs each accepted instruction to completion in program order, and every register write is compared, in order, with the model. Every stall is also checked: it must sit behind a load that the stalled instruction reads, so a pipe that stalls too often fails as well.
// Three stages: D reads operands (the instruction at the input), X adds, W writes the register file.
// Instruction: {op[1:0], rd[1:0], rs1[1:0], rs2[1:0], imm[7:0]}
// op 0 NOP, 1 ADD rd = rs1 + rs2, 2 ADDI rd = rs1 + imm, 3 LD rd = mem[rs1 + imm]
module fwd_pipe #(parameter bit FWD = 1) (
input logic clk, rst_n,
input logic in_valid,
input logic [15:0] in_instr,
output logic in_ready, // 0 during a load-use stall: hold the instruction
output logic wb_valid,
output logic [1:0] wb_rd,
output logic [7:0] wb_val
);
localparam logic [1:0] NOP = 2'd0, ADD = 2'd1, ADDI = 2'd2, LD = 2'd3;
logic [7:0] rf [4];
logic [7:0] mem [16];
initial for (int i = 0; i < 16; i++) mem[i] = 8'h20 + 8'(i);
// D: decode the offered instruction
logic [1:0] op, rd, rs1, rs2;
logic [7:0] imm;
assign {op, rd, rs1, rs2, imm} = in_instr;
logic uses_rs2;
assign uses_rs2 = (op == ADD);
// X and W stage registers
logic x_valid, w_valid;
logic [1:0] x_op, x_rd, w_rd;
logic [7:0] x_a, x_b, w_val;
logic [7:0] x_sum;
assign x_sum = x_a + x_b; // ADD, ADDI result; LD address
// Forwarding mux, one per operand: the youngest producer wins
logic [7:0] fwd_a, fwd_b;
always_comb begin
if (FWD && x_valid && x_rd == rs1) fwd_a = x_sum; // 1 ahead, in X: youngest first
else if (FWD && w_valid && w_rd == rs1) fwd_a = w_val; // 2 ahead, in W
else fwd_a = rf[rs1]; // 3 or more ahead: written
if (FWD && x_valid && x_rd == rs2) fwd_b = x_sum;
else if (FWD && w_valid && w_rd == rs2) fwd_b = w_val;
else fwd_b = rf[rs2];
end
// Load-use hazard: a load in X has only its address (x_sum), not its data,
// so a consumer waits one cycle in D and then takes the data from W
logic stall;
assign stall = FWD && in_valid && op != NOP && x_valid && x_op == LD &&
(x_rd == rs1 || (uses_rs2 && x_rd == rs2));
assign in_ready = !stall;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
x_valid <= 1'b0; w_valid <= 1'b0;
for (int i = 0; i < 4; i++) rf[i] <= 8'h00;
end else begin
x_valid <= in_valid && !stall && op != NOP; // a stall sends a bubble into X
w_valid <= x_valid;
if (w_valid) rf[w_rd] <= w_val; // W writes at the end of its cycle
end
always_ff @(posedge clk) begin
x_op <= op; x_rd <= rd;
x_a <= fwd_a;
x_b <= uses_rs2 ? fwd_b : imm;
w_rd <= x_rd;
w_val <= (x_op == LD) ? mem[x_sum[3:0]] : x_sum; // memory answers one cycle later
end
assign wb_valid = w_valid;
assign wb_rd = w_rd;
assign wb_val = w_val;
endmoduleI1 reads r2 from the load I0 one ahead: one stall, then the W path. I2 reads r3 from I1 one ahead: the X path. I3 reads r3 while I2 (in X) and I1 (in W) both write it: I2 must win, so the X path. I3's r2 comes from the register file, because I0 wrote it as I1 sat in D after the stall. Nothing in flight writes r1, so the r1 reads in I0 and I1 need nothing.
In X the load has only computed its address; the memory returns the data as the load enters W. Forwarding x_sum would hand the consumer the address, as slot 4 of the second trace shows. The one-cycle stall lets the W path carry the data instead.
Waveforms: Icarus Verilog 13.0, 2026-10-17, node scripts/tools/gen-waves.mjs sequential:stalls-bubbles on rich/sequential/wave-src/stalls-bubbles/no-forward.sv (FWD = 0) and forward.sv (FWD = 1). No MISMATCH after one expectation was corrected by hand: in the stall slot op_a is the load address 3, which the lesson now explains. A second run left waves-stalls-bubbles.mjs unchanged.
Bench: Icarus Verilog 13.0, 2026-10-17, iverilog -g2012 -s tb_fwd_pipe then vvp. Result: PASS, 3000 instructions, 2244 register writes match the model, 132 load-use stalls. Mutants run on the same bench, all FAIL: FWD = 0 (2135 errors, 0 stalls); W tested before X (902 errors); stall removed (381 errors); stall on every load (307 unjustified stalls).
The check-yourself sequence (LD, ADD, ADDI, ADD) was also simulated: one stall after the load, writes 20, 20, 21, 41. Scope: one small in-order pipe, default random seed, no timing analysis of the forwarding path.