5.2.10 · D2Processor Datapath & Pipelining

Visual walkthrough — Hazard detection units

3,674 words17 min readBack to topic

Step 1 — Draw the pipeline as five rooms in a row

WHAT. A pipelined processor is not one big machine that does everything at once. It is a hallway with five rooms, and every instruction walks through them left to right, one room per tick of the clock.

The five rooms, in order:

  • IFInstruction Fetch: go grab the next instruction from memory.
  • IDInstruction Decode: read the instruction, look up the register numbers it needs.
  • EXExecute: do the arithmetic in the ALU (the calculator).
  • MEMMemory: talk to data memory (only loads/stores need this room).
  • WBWrite Back: write the final result into the register file (the CPU's scratchpad of named boxes like $1, $2).

WHY these five and not four or six? Each room does one kind of work, so the clock can tick fast: while room EX crunches instruction A, room ID can already decode instruction B. Five instructions can be "in flight" at once — that is the whole point of a pipeline.

PICTURE. Five labelled rooms, and a single instruction shown walking through them across five clock ticks (the diagonal).


Step 2 — Put two instructions in the hallway at once

WHAT. Now send two instructions in, one right behind the other:

lw   $2, 20($1)     ; I1: load - fetch a number from memory into box $2
add  $4, $2, $5     ; I2: needs $2 as an input

Because they enter one tick apart, at any moment they sit in adjacent rooms. When I2 (the add) is decoding in ID, I1 (the lw) is one room ahead in EX.

WHY this pairing matters. I2 wants the value in box $2. But box $2 is exactly what I1 is in the middle of producing (the load writes into $2, its Rd*). This is a read-after-write (RAW) dependency: I2 reads what I1 writes. If I2 reads box $2 before I1 has written it, I2 reads a stale (old, wrong) number.

PICTURE. The two instructions overlapped on the room-timeline; a red bracket highlights the cycle where add is in ID while lw is in EX.

Recall

Which two rooms hold lw and add in the dangerous cycle? ::: lw is in EX, add is in ID.


Step 3 — When is each number actually ready?

WHAT. The key to everything is a timeline of when the answer physically exists. Not when it's written to the register file — when the bits first appear on a wire.

First, a tiny piece of notation so the formula reads cleanly:

  • For an arithmetic instruction like add: the ALU finishes at the end of EX. So ready(ALU op) = end of EX.
  • For a load (lw): the number comes from memory, and memory is the MEM room. It does not exist until the end of MEM — one whole room later. So ready(load) = end of MEM.

WHY this single fact decides the whole page. Everything downstream is a race between "when does the data appear?" and "when does the consumer need it?" A load's data appears one room later than an ALU result. Hold that thought.

PICTURE. Two horizontal "data-ready" timelines: one for add (ready at end of EX), one for lw (ready at end of MEM, marked one column to the right).


Step 4 — Forwarding: the shortcut wire that usually saves us

WHAT. Normally we don't wait for WB. We string a forwarding wire (also called a bypass) that snatches a result the moment it exists and hands it straight to the ALU inputs of the waiting instruction. This is the job of the forwarding unit — and here is all you need to know about it in one sentence: it is a small logic block that, each cycle, checks whether a result already sitting in the EX/MEM or MEM/WB latch matches the register an executing instruction wants, and if so flips a mux to feed that result straight into the ALU instead of the stale register-file value.

Two shortcut wires exist, each tapping one of the latches from Step 1:

  • EX/MEM → EX: take a result that just finished the ALU (now sitting in the EX/MEM latch) and feed it to the next instruction's ALU.
  • MEM/WB → EX: take a result sitting in the MEM/WB latch (about to be written back) and feed it to the ALU.

WHY it works for add-then-add. If I1 is an add, its answer exists at the end of EX and lands in the EX/MEM latch. In the very next cycle I2 needs it at the start of its EX. The EX/MEM → EX wire delivers it just in time. No stall.

PICTURE. An add (I1) in EX, its result curving along the orange EX/MEM forwarding wire back into the ALU inputs of the add (I2) that is entering EX. Timing lines up.


Step 5 — Watch the shortcut FAIL for a load

WHAT. Now replace I1's add with our lw. Try to use the same EX/MEM → EX shortcut. Line up the timing:

  • I2 (add) needs $2 at the start of its EX — which is the same cycle lw is in MEM.
  • But lw's data does not exist until the end of MEM.

So at the instant the add needs the number, the load has not fetched it yet. The shortcut wire would carry garbage — an empty value, because memory hasn't answered.

WHY it can't be patched by a faster wire. You cannot forward data backwards in time. The consumer needs it at the start of a cycle; the producer finishes it only at the end of that same cycle. There is no wire that delivers the future. This is a timing impossibility, not a wiring oversight.

PICTURE. The same forwarding attempt as Step 4, but now the source is lw in MEM; a red X marks the wire because the data appears at end-of-MEM while the add needs it at start-of-EX — an arrow pointing left (backwards in time) crossed out.


Step 6 — The cure: insert exactly ONE bubble

WHAT. If we push the add back by one cycle, look what happens: now when the add finally reaches EX, the lw has moved on to MEM/WB. Its data now exists (end of MEM already happened) and the MEM/WB → EX wire delivers it perfectly.

The one-cycle delay is created by inserting a bubble — a fake do-nothing instruction (a NOP) that occupies EX for one tick so the real add waits in ID.

Cycle-by-cycle:

Cycle IF ID EX MEM WB
1 lw
2 add lw
3 add NOP lw
4 add NOP lw
5 add NOP lw

Notice add appears in the IF column for both cycle 2 and cycle 3. That is not a typo: because the stall drives PCWrite = 0 (Step 7), the program counter is frozen, so in cycle 3 the IF room simply re-fetches the very same add it fetched in cycle 2 — it has nowhere new to go until the freeze lifts. The add is effectively held in place across IF and ID.

In cycle 5 the add is in EX and lw is in MEM/WB — forwarding now works, thanks to the forwarding unit.

WHY exactly one bubble and not two? The gap between ready-times was exactly one room (Step 3). One bubble closes exactly one room of gap. More would waste a cycle; fewer wouldn't be enough.

PICTURE. The corrected timeline: add frozen in ID for an extra cycle, a gray NOP bubble sliding through EX, and the green MEM/WB → EX forwarding arrow succeeding in cycle 5.


Step 7 — Which knobs create the bubble?

WHAT. To freeze the pipeline for one cycle the hazard unit flips three control signals to 0. These are the pipeline control signals:

  • PCWrite = 0 — freeze the program counter, so IF does not advance to a new instruction (it re-fetches the same one, as we saw in Step 6).
  • IF/ID_Write = 0 — freeze the IF/ID register, so the add stays in ID and retries next cycle.
  • Control_Mux = 0 — use the mux (Step 4's selector switch) to zero out all control signals heading into ID/EX, turning the outgoing instruction into a NOP.

WHY all three together?

  • Without PCWrite=0, IF would advance to a new instruction we'd have nowhere to put.
  • Without IF/ID_Write=0, the add would slip forward and read stale $2.
  • Without Control_Mux=0, the instruction that was about to enter EX would still act — corrupting state instead of being an innocent bubble.

PICTURE. The datapath with the three freeze-lines highlighted (PCWrite on the PC, IF/ID_Write on the IF/ID latch, Control_Mux forcing zeros into ID/EX), each labelled with its effect.

Recall

What does Control_Mux = 0 accomplish? ::: It uses the mux to zero all control signals into ID/EX, so the instruction leaving ID becomes a harmless NOP (a bubble) instead of executing.


Step 8 — Write the detection condition, term by term

WHAT. The hazard unit sits in ID and fires the stall only when the load-use pattern is present. Before the formula, two symbols must be earned:

Now the exact test, using the pipeline-register fields from Step 1 and the Rd* convention:

Term by term:

  • (a) ID/EX.MemRead — the instruction currently in EX is a load (its MemRead flag, carried in the ID/EX latch, is 1). This is the only case where data appears too late to forward (Step 5).
  • ID/EX.Rd* — the destination register the load is filling (e.g. $2). Remember from Step 1 that for a load this physically comes from its Rt field, but we call it Rd* uniformly.
  • IF/ID.Rs / IF/ID.Rt — the two source register numbers of the instruction currently in ID (the potential victim), carried in the IF/ID latch.
  • (b) (c) — if the load's target equals either input, this instruction depends on the load. One match is enough.
  • (AND) — both sides must hold: it must be a load and the register must match. An ordinary add producer (MemRead = 0) fails (a) and so never stalls.

WHY test in ID? ID is the earliest room where we know the register numbers being read. Catching it here lets us stall before the wrong data is ever used.

PICTURE. The detection logic drawn as gates: MemRead into an AND, the two equality comparators into an OR, feeding the AND, output = Stall — with example values $2 flowing through to make Stall = 1.


Step 9 — The edge cases you must never skip

Every condition hides corner cases. Here they are, each with its own reasoning:

  • Register $0. In MIPS $0 is hardwired to zero and can never be written. If a load "targets" $0 (weird but legal), no stall is needed — nothing real changes. Real hardware adds the extra guard ID/EX.Rd* ≠ 0 to the AND, so a $0 destination can never trigger a stall. Without this guard you'd stall for nothing.
  • Only one register matches, or neither. The (OR) in Step 8 handles "match on Rs, on Rt, or on both." If neither matches, the AND is false → no stall, even though a load is in EX. Independence is respected.
  • Producer is an add, not a load (MemRead = 0). Term (a) is false → no stall. The forwarding unit silently handles it. The hazard unit stays out of the way.
  • A store after the load (lw then sw). A store like sw $2, 0($6) reads $2 (via its Rt) to send it to memory. But a store consumes its data in the MEM room, not EX — one room later than an add would. By the time the store reaches MEM, the load's value is already in MEM/WB, so a MEM/WB → MEM store-data forward covers it: no stall. The address register ($6, its Rs), read in EX, would still stall if it depended on the load — so the Rs check in Step 8 still applies to stores.
  • Two-instruction gap (lw, then something, then the user). If a different instruction sits between the load and its consumer, by the time the consumer reaches ID the load is already in MEM/WB — forwarding covers it, no stall. The condition in Step 8 naturally checks only the immediately following instruction (ID vs ID/EX), so it correctly stays silent here.
  • Branch target after a load (lw then beq). A branch that resolves early compares its two source registers while still in ID — the same room the load-use test lives in. So a beq reading a register the previous lw fills triggers this exact stall (its Rs/Rt are checked like any other consumer), and because branches need their inputs even earlier than add, some designs stall two cycles here. Don't mistake the load-use test as applying only to arithmetic; it applies to anything that reads Rs or Rt in ID. The separate "which way does the branch go?" problem is control-flow, deferred to branch prediction and the control unit.
  • A control (branch) hazard is a different animal. When the "which instruction next?" is unknown, we flush rather than stall. That story lives in branch prediction.

PICTURE. A small decision fan: input pattern on the left, several branches on the right, each ending in STALL or NO-STALL with a one-line reason.


The one-picture summary

Everything collapses to one race: the load's data is ready one room too late for the very next instruction, so we delay that instruction by one bubble, after which forwarding works.

Recall Feynman retelling — say it like a story

Instructions march through five rooms: fetch, decode, execute, memory, write-back. Between each pair of rooms is a little latch — a pipeline register (IF/ID, ID/EX, EX/MEM, MEM/WB) — that carries the instruction's register numbers and control flags forward. An add finishes its answer as it leaves the execute room and drops it in the EX/MEM latch, so a shortcut wire (chosen by a mux) can hand that answer to the next instruction just in time. But a load gets its answer from the memory room — one room later. So when the very next instruction reaches execute, the load hasn't fetched the number yet; the shortcut would carry garbage, and you can't send data backwards in time. The cure is a single stall: freeze the program counter (so fetch just re-grabs the same instruction) and the fetch/decode latch, and slip one empty "do-nothing" instruction into execute. That one-cycle delay is exactly enough for the load to reach the write-back latch, where the forwarding wire can now reach it. A tiny piece of logic in the decode room watches for the tell-tale pattern — "the instruction in execute is a load (MemRead = 1), and the register it fills (its Rd*) is one I'm about to read (Rs or Rt)" — and only then pulls the stall lever, provided that register isn't the hardwired $0. It fires for anything reading Rs or Rt in ID, branches included; a store's data-only dependence and any two-instruction gap it leaves alone, because forwarding already has those covered.