Data hazards (Day 18) affect what operands are available. Control hazards affect which instructions should even be in the pipeline at all. When a branch is taken, the two instructions already fetched after the branch are wrong and must be discarded. Today we build flush_unit.v to handle this cleanly.
The pipeline evaluates branch conditions in the EX stage. By then, two more instructions have entered IF and ID. Under static not-taken prediction the CPU assumes every branch is not taken and keeps fetching sequentially. If the branch is taken, those two instructions must be flushed (replaced with NOPs) and the PC redirected to the branch target.
Cycle: 1 2 3 4 5 6 BEQ: [IF] [ID] [EX] ... I+1: [IF] [ID] [flush→NOP] I+2: [IF] [flush→NOP] target: [IF] [ID] [EX] ... Branch taken → 2-cycle penalty (bubbles injected at I+1 and I+2)
| Port | Direction | Width | Description |
|---|---|---|---|
| branch_taken | Input | 1 | 1 when a branch/jump is taken (from EX stage) |
| flush_if_id | Output | 1 | Assert 1 to flush the IF/ID pipeline register (replace with NOP) |
| flush_id_ex | Output | 1 | Assert 1 to flush the ID/EX pipeline register (replace with NOP) |
// flush_unit.v — Inserts NOP bubbles on branch taken
// When branch_taken is asserted, the two instructions that entered
// IF and ID after the branch must be flushed.
module flush_unit (
input branch_taken,
output flush_if_id, // flush IF/ID register (the instr after branch)
output flush_id_ex // flush ID/EX register (two after branch)
);
// Both stages get a bubble simultaneously when branch is taken
assign flush_if_id = branch_taken;
assign flush_id_ex = branch_taken;
endmodule
The flush input to the ID/EX register (from Day 17's pipe_regs.v) forces all control signals to 0 — effectively inserting a NOP. The IF/ID register is cleared similarly. The PC is simultaneously updated to the branch target.
// In the pipelined CPU top module:
wire branch_taken; // comes from EX stage branch_unit
wire flush;
flush_unit fu (.branch_taken(branch_taken), .flush_if_id(flush), .flush_id_ex(flush));
// IF/ID register: on flush, load NOP instead of actual instruction
if_id_reg ifid (
.clk(clk), .rst(rst || flush), // rst acts like a flush
.stall(stall),
.if_pc(pc), .if_inst(inst),
.id_pc(id_pc), .id_inst(id_inst)
);
// ID/EX register: on flush, all control signals become 0
id_ex_reg idex (
.clk(clk), .rst(rst), .flush(flush),
// ... all the other ports
);
| Strategy | Penalty if wrong | Hardware cost | Best for |
|---|---|---|---|
| Always not-taken | 2 cycles when branch is taken | Minimal (just flush logic) | Simple cores, mostly sequential code |
| Always taken | 2 cycles when branch not taken | Minimal | Tight loops (most branches taken) |
| 1-bit predictor | 2 cycles on first misprediction in loop | Small table | Loops with consistent behaviour |
| 2-bit predictor | 2 cycles on second consecutive miss | Moderate table | Most real workloads (used in production CPUs) |
A control hazard occurs when a branch changes the PC, but the pipeline has already fetched instructions that should not execute. The two instructions after the branch in the pipeline must be flushed when the branch is taken.
Replacing in-flight instructions with NOP bubbles (all-zero control signals, RegWrite=0). A flush is triggered by asserting the flush input of the relevant pipeline registers, converting them to NOPs before they reach the WB stage.
Always assume the branch is not taken. Continue fetching PC+4, PC+8 after the branch. If the branch is actually taken, flush the two wrongly-fetched instructions (2-cycle penalty). Requires no prediction hardware.