HomeRISC-V from ScratchDay 19
DAY 19 · PHASE 3 — PIPELINE & OPTIMIZE

Control Hazards & Branch Handling

By EcrioniX · Updated 2026-06-11

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.

Why Branches Create Hazards

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)

flush_unit.v — Port Table

PortDirectionWidthDescription
branch_takenInput11 when a branch/jump is taken (from EX stage)
flush_if_idOutput1Assert 1 to flush the IF/ID pipeline register (replace with NOP)
flush_id_exOutput1Assert 1 to flush the ID/EX pipeline register (replace with NOP)
flush_unit.v
// 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

How the Flush Works in pipe_regs.v

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.

branch_flush_snippet.v
// 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
);

Static Not-Taken vs Taken Prediction

StrategyPenalty if wrongHardware costBest for
Always not-taken2 cycles when branch is takenMinimal (just flush logic)Simple cores, mostly sequential code
Always taken2 cycles when branch not takenMinimalTight loops (most branches taken)
1-bit predictor2 cycles on first misprediction in loopSmall tableLoops with consistent behaviour
2-bit predictor2 cycles on second consecutive missModerate tableMost real workloads (used in production CPUs)

Day 19 Takeaways

FAQ

What is a control hazard?

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.

What is a pipeline flush?

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.

What is static not-taken branch prediction?

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.

Previous
← Day 18: Data Hazards & Forwarding

← Full roadmap