Back to Blog

The Fetch-Decode-Execute Cycle: Tracing ADD, LOAD, STORE, and BEQ

August 18, 202619 min read
Computer Architecture CPU Design Systems Learning

Lesson 1, Part 6. We've built a program counter, a register file, an ALU, and a control unit that turns opcode bits into signals. This is the part where those pieces stop being separate diagrams and start being one machine — we trace four instructions through it, cycle by cycle, wire by wire.


Recap: The Parts on the Table

Before wiring anything together, it's worth laying out exactly what we're assuming already exists, because every trace in this post is just these five boxes exchanging values in a fixed order.

PC Instruction Memory Decoder / Control Unit control signals rs1,rs2,rd A, B Register ALU File result Data Memory write-back PC + 4 / branch target

Nothing here is new. The program counter and its update logic came from the sequential-logic and register posts. The register file with its two read ports and one write port, the ALU with its arithmetic/logic core, and the control unit that maps opcode bits to signals — all of that is prior work. What's new in this post is the wiring: showing precisely which value sits on which wire, in which cycle, for four different instructions, and what happens when you point those same wires at four different jobs.

It helps to name each box's job in one line, since every trace below is just these five sentences happening in sequence:

  • PC — a register holding "the address of the instruction to fetch this cycle." Nothing more. It doesn't know what instruction it points at.
  • Instruction memory — a read-only lookup: address in, 32-bit instruction word out, purely combinational from the PC's point of view.
  • Register file — two independent read ports (so an instruction can read two source registers in the same cycle) and one write port, all addressed by small register-number fields decoded from the instruction.
  • ALU — one arithmetic/logic core, two inputs, one output, and a handful of control bits saying which operation to perform. It has no idea what its inputs represent or what happens to its output.
  • Data memory — a read/write lookup, address in, and either data out (MemRead) or data in (MemWrite), gated so it only activates on the instructions that actually need it.

Every trace in this post is nothing but these five components exchanging values, one cycle at a time, in the order FETCH → DECODE → EXECUTE → MEMORY → WRITE BACK.


The Five-Step Model, Formalized

Every instruction in our simple ISA moves through the same five stages:

  • FETCH — read the instruction word at Memory[PC] into the instruction register, and compute PC + 4 as the default next address.
  • DECODE — split the instruction bits into opcode, source registers, destination register, and immediate; the control unit turns the opcode into the signals below; the register file drives its two read ports off rs1/rs2.
  • EXECUTE — the ALU combines two operands (a register value, and either a second register value or an immediate) using an operation selected by the control unit.
  • MEMORY — the data memory is read or written, only if the instruction actually needs it.
  • WRITE BACK — a result (from the ALU or from memory) is written into the register file, only if the instruction actually needs it, and the PC is updated for the next cycle.

The important thing to notice before we trace anything: every instruction passes through all five stages, even when a stage does nothing. ADD walks through the MEMORY stage and touches no memory. STORE walks through WRITE BACK and writes no register. The datapath doesn't have four different shapes for four different instructions — it has one fixed shape, and the control unit decides, stage by stage, whether each box actually does anything this cycle. That uniformity is what makes a single piece of hardware capable of running an entire instruction set instead of needing bespoke circuitry per opcode.

Takeaway: a single-cycle datapath has to size its clock period for the slowest instruction, not the average one. ADD finishes its useful work after EXECUTE; LOAD has to wait for EXECUTE, then a full memory access, then WRITE BACK before the cycle can end — and every instruction, including ADD, is forced to wait exactly as long. That wasted time on the fast instructions is the opening argument for pipelining, which is where this series goes next.

Instruction Formats: Why Decode Needs to Know the Shape First

One detail the five-step list glosses over: DECODE can't blindly chop the instruction word into "opcode, then three register fields" for every instruction, because our four instructions don't all carry the same fields.

ADD  R3, R1, R2       needs: opcode, rd,  rs1, rs2
LOAD R3, [R1 + 8]      needs: opcode, rd,  rs1, immediate
STORE R3, [R1 + 8]     needs: opcode, rs1, rs2 (as data), immediate
BEQ  R1, R2, target    needs: opcode, rs1, rs2, immediate (as offset)

ADD needs three register fields and no immediate. LOAD needs two register fields and an immediate. STORE needs two register fields and an immediate, but one of those "register fields" is functioning as a data source rather than an address source.

This is why real ISAs group opcodes into a small number of instruction formats:

R-type (ADD)    | opcode | rd  | rs1 | rs2 | (unused)     |
I-type (LOAD)   | opcode | rd  | rs1 | immediate          |
S-type (STORE)  | opcode | rs1 | rs2 | immediate          |
B-type (BEQ)    | opcode | rs1 | rs2 | immediate (offset) |

R-type for register-register operations, I-type for register-immediate (used by both LOAD and arithmetic-with-constant), S-type for stores, B-type for branches. Each format places the register-number and immediate fields at different bit positions.

The very first thing the control unit's decode logic does is look at the opcode to figure out which format this is. That determines where in the 32-bit word the immediate field even starts. Only after that can it hand off rs1, rs2, rd, and the immediate to the rest of the datapath.

This is a direct extension of the instruction-decode work from the earlier post in this lesson. The control unit doesn't just map opcode → signals. It maps opcode → "how do I even parse the rest of this word."

The Control Signals We're Wiring Up

The control unit from Part 5 looked at the opcode and produced a handful of one-bit (or small-encoding) signals. We'll use the same seven throughout this post, so it's worth having them in one place before we start tracing:

SignalMeaning when asserted
RegWriteWrite a value into the register file this cycle
ALUSrcALU's second operand comes from the immediate field, not a register
ALUOpWhich operation the ALU performs (ADD, SUB, …)
MemReadRead from data memory at the ALU-computed address
MemWriteWrite to data memory at the ALU-computed address
MemToRegThe write-back value comes from memory, not from the ALU
BranchThis is a branch; gate the PC mux with the ALU's zero flag

Four instructions, four different settings of these seven wires. That's the entire story of this post, and it's worth reading each table below as: given this opcode, what does the control unit set these seven signals to, and what happens as a result?


Trace 1 — ADD R3, R1, R2

The instruction that needs the least explanation, traced with full rigor so the later ones have something to diff against.

Starting state: PC = 0x1000, R1 = 10, R2 = 20, R3 = 0 (about to be overwritten).

Stage: FETCH

SignalValue
PC0x1000
Instruction = Memory[PC]ADD R3, R1, R2
PC + 4 (computed, not yet committed)0x1004

Stage: DECODE

SignalValue
opcodeADD
rs1R1
rs2R2
rdR3
RegWrite1
ALUSrc0 (second operand is a register)
ALUOpADD
MemRead0
MemWrite0
MemToReg0 (result comes from the ALU)
Branch0
Register file read: R110
Register file read: R220

Stage: EXECUTE

SignalValue
ALU operand A10
ALU operand B20 (register, since ALUSrc = 0)
ALU operationADD
ALU result30

Stage: MEMORY

SignalValue
MemRead0 — memory not touched
MemWrite0 — memory not touched
Value passed through to write-back30 (unchanged)

The instruction physically passes through the memory stage — the wire carrying 30 runs right past the data memory's input — but with MemRead and MemWrite both low, the memory simply does nothing and the value continues on unmodified. This is the "every instruction takes the same shape" point from the previous section, made concrete.

Stage: WRITE BACK

SignalValue
MemToReg0 — select ALU result, not memory output
Write-back value30
RegWrite1 — commit the write
Register file write: R330
PC ← PC + 4 (since Branch = 0)0x1004

Result: R3 = 30, PC = 0x1004. Nothing surprising — this is the baseline every other trace will be measured against.

Takeaway: for ADD, every stage after EXECUTE is basically a formality — MEMORY is a no-op and WRITE BACK just routes the ALU's output straight into the register file. Keep this shape in mind; the next three traces are all deviations from it.


Trace 2 — LOAD R3, [R1 + 8]

Here's where the ALU starts doing something the programmer wouldn't call "math."

Starting state: PC = 0x1000, R1 = 0x1000, Memory[0x1008] = 42.

Stage: FETCH

SignalValue
PC0x1000
Instruction = Memory[PC]LOAD R3, [R1 + 8]
PC + 40x1004

Stage: DECODE

SignalValue
opcodeLOAD
rs1R1
rdR3
immediate8
RegWrite1
ALUSrc1 (second operand is the immediate, not rs2)
ALUOpADD
MemRead1
MemWrite0
MemToReg1 (result comes from memory, not the ALU)
Branch0
Register file read: R10x1000

Notice LOAD only reads one register — there's no rs2 field to decode. ALUSrc = 1 is the signal that matters most here: it tells the ALU's second input mux to grab the sign-extended immediate 8 instead of a second register value.

Stage: EXECUTE

SignalValue
ALU operand A0x1000 (from R1)
ALU operand B8 (immediate, since ALUSrc = 1)
ALU operationADD
ALU result0x1008

Read that ALU result carefully: 0x1008 is not "an answer" in any mathematical sense a programmer cares about. It's a memory address. The ALU doesn't know that, and — this is the whole point of the section further down — it doesn't need to.

Stage: MEMORY

SignalValue
MemRead1
Memory address (from ALU)0x1008
Memory[0x1008]42
Data read out42

Stage: WRITE BACK

SignalValue
MemToReg1 — select the memory output, not the ALU result
Write-back value42
RegWrite1
Register file write: R342
PC ← PC + 40x1004

Result: R3 = 42, PC = 0x1004. The ALU's 0x1008 did real work this cycle — it just never made it into a register. It was consumed entirely as an address, then discarded. The MemToReg mux is the wire that decides, per instruction, whether "the answer" is what the ALU computed or what memory returned at the address the ALU computed.

One more detail worth being precise about: the immediate 8 isn't stored as a full 32-bit value in the instruction word.

Encoding it that way would waste most of the word's bits on a small number. It's packed into a narrow field — often 12 or 16 bits, depending on the ISA — and then sign-extended up to the ALU's full width during DECODE.

That's what lets LOAD R3, [R1 - 8] work just as correctly as LOAD R3, [R1 + 8]. The ALU still just adds. A negative offset is handled by making the bit pattern itself represent a negative number, using exactly the two's-complement representation from the very first post in this series.

Nothing new has to be built into the ALU to support negative offsets. It's the same reuse principle again, one level down.

Takeaway: LOAD's ALU result is an address, not an answer. MemToReg = 1 is the signal that tells write-back to reach past the ALU and grab whatever memory returned instead.


Trace 3 — STORE R3, [R1 + 8]

STORE is the mirror image of LOAD, and it exposes a detail that's easy to miss: the register file's second read port doesn't always feed the ALU.

Starting state: PC = 0x1000, R1 = 0x1000, R3 = 42.

Stage: FETCH

SignalValue
PC0x1000
Instruction = Memory[PC]STORE R3, [R1 + 8]
PC + 40x1004

Stage: DECODE

SignalValue
opcodeSTORE
rs1 (base address register)R1
rs2 (value to store)R3
immediate8
RegWrite0
ALUSrc1
ALUOpADD
MemRead0
MemWrite1
MemToRegdon't care — nothing is written back
Branch0
Register file read: R10x1000
Register file read: R342

STORE reads two registers, same as ADD, but they play completely different roles: R1 is an address operand headed for the ALU, and R3 is data headed straight for the memory's write port. It never touches the ALU at all.

Stage: EXECUTE

SignalValue
ALU operand A0x1000 (from R1)
ALU operand B8 (immediate)
ALU operationADD
ALU result (address)0x1008
Store-data value (bypasses the ALU)42 (from R3, carried on a separate wire)

Stage: MEMORY

SignalValue
MemWrite1
Memory address (from ALU)0x1008
Data in (from register file, not ALU)42
Memory[0x1008] ←42

Stage: WRITE BACK

SignalValue
RegWrite0 — no register file write this cycle
PC ← PC + 40x1004

Result: Memory[0x1008] = 42, no register changes, PC = 0x1004. STORE is the instruction that makes the five-step model's uniformity most visible: it walks all the way to WRITE BACK and simply does nothing there, because RegWrite = 0 holds the register file's write-enable low.

It's also the instruction where the instruction-format point from earlier stops being abstract: STORE's second register field isn't a rd at all, and there is no destination register in this instruction — decode has to know, purely from the opcode, that this particular field means "read this register and route it to the memory data-in bus" rather than "this is where the result goes." Get that classification wrong in the control unit's format-detection logic and the CPU would try to write a result into a register that was never meant to receive one.

Takeaway: STORE's second register read never reaches the ALU. It rides a separate wire straight to the data memory's write-data input — proof that "the register file feeds the ALU" is a simplification, not a rule.


Trace 4 — BEQ R1, R2, target

Branches are where a sixth actor shows up: the PC stops being a simple "add 4 and move on" counter and becomes the output of a two-way mux.

Starting state (taken case): PC = 0x1000, R1 = 10, R2 = 10, branch target = 0x2000.

Stage: FETCH

SignalValue
PC0x1000
Instruction = Memory[PC]BEQ R1, R2, 0x2000
PC + 4 (fall-through candidate)0x1004

Stage: DECODE

SignalValue
opcodeBEQ
rs1R1
rs2R2
target address (decoded from instruction)0x2000
RegWrite0
ALUSrc0 (comparison uses two registers)
ALUOpSUBTRACT
MemRead0
MemWrite0
Branch1
Register file read: R110
Register file read: R210

This is the classic single-cycle-datapath trick (Patterson & Hennessy's, not ours): a comparator is literally an ALU doing a subtraction and checking whether the result is zero. R1 == R2 and R1 − R2 == 0 are the same question asked two different ways, and hardware only needs to know how to ask it the second way.

Stage: EXECUTE

SignalValue
ALU operand A10
ALU operand B10
ALU operationSUBTRACT
ALU result0
Zero flag1

Stage: MEMORY

SignalValue
MemRead / MemWrite0 / 0 — untouched

Stage: WRITE BACK

SignalValue
RegWrite0 — no register write
PC mux select (Branch AND Zero)1 AND 1 = 1 → take the branch
PC ←0x2000 (target, not PC + 4)

Result: branch taken, PC = 0x2000.

The Not-Taken Case, for Contrast

Same instruction, R1 = 10, R2 = 20: the ALU computes 10 − 20 = -10, Zero = 0, so Branch AND Zero = 1 AND 0 = 0. The mux selects PC + 4 = 0x1004 instead of the target. Same hardware, same cycle shape, opposite outcome — decided entirely by one AND gate looking at two one-bit signals.

One simplification worth flagging honestly: this trace gave the branch target as a plain absolute address (0x2000), matching how we've written labels earlier in this series.

Most real ISAs don't encode branch targets as absolute addresses at all. They encode a signed offset from the current PC, computed by a small dedicated adder (PC + 4 + offset) that runs in parallel with the main ALU.

The reason is portability. Code compiled with PC-relative branches can be loaded at any base address in memory and still jump to the right place, because "17 instructions forward" means the same thing regardless of where the program starts.

We're keeping absolute targets here to avoid introducing offset encoding before we need it. The mux-and-Zero-flag mechanism above is identical either way — only the arithmetic that produces the target address changes.

Takeaway: BEQ doesn't need a separate comparator circuit. Subtract, then ask "is the result zero" — a question the ALU was already equipped to answer.

R1 ALU Zero R2 (SUBTRACT) Branch AND Taken? PC + 4 MUX Next PC Target

One ALU, Two Jobs (and Really, Three)

Here is the insight the four traces above were building toward, stated plainly:

The ALU has no idea what its output means. It adds or subtracts bit patterns. Whether that result becomes a register value, a memory address, or a yes/no branch decision is decided entirely by wires outside the ALU — never by the ALU itself.

Look back at what actually happened to the ALU's output in each trace:

InstructionALU computesWhat happens to the result
ADD10 + 20Written directly into a register (MemToReg = 0)
LOAD0x1000 + 8Used as a memory address; discarded after the memory access
STORE0x1000 + 8Used as a memory address; discarded after the memory access
BEQ10 − 10Reduced to a single Zero bit; feeds a mux select line, never a register

Four instructions, four completely different fates for "the number the ALU just produced," and the ALU's internal adder circuit did not change at all between them. It performed the same operation — binary addition, or addition of a negated operand for subtraction — every single time. The only things that changed were:

  1. Which values were routed to its inputs (ALUSrc choosing register vs. immediate for operand B), and
  2. What downstream logic did with its output (MemToReg, MemRead/MemWrite, Branch AND Zero).

This is exactly the same idea we ran into all the way back when we first talked about number representation: a bit pattern has no built-in meaning. 0x1008 is not "an address" in any sense the hardware enforces — it's thirty-two bits that happen to get driven onto the memory unit's address input this cycle. The same bits, produced by the same adder, would have been "a sum" if they'd been driven into the register file's write-data input instead. Meaning is assigned by wiring, not by the value itself.

There's a real engineering payoff here, not just a philosophical one. If address computation needed its own dedicated adder circuit, separate from the one used for arithmetic, you'd need two full adders in the datapath, doubling that piece of the silicon budget for no benefit — the two adders would be doing bit-for-bit identical work, just fed from different sources. Recognizing that "add a base register to an offset" and "add two operands together" are the same operation viewed from different callers is what lets a single ALU, a couple of multiplexers, and a handful of control bits cover arithmetic, every addressing mode, and every comparison-based branch. Reuse isn't a nice-to-have here — it's most of why a single-cycle datapath can be built from as few functional units as it is.

This isn't just a simplified-teaching-ISA trick, either. It shows up directly in real instruction sets.

x86 has an instruction, LEA ("load effective address"), that does nothing but this. It runs the exact same base-plus-offset address-generation logic a LOAD would use. But instead of feeding the result to the memory unit, it routes it straight into a general-purpose register — the MemToReg-style fork happening explicitly at the instruction-set level rather than only inside the microarchitecture.

Compilers use LEA constantly for plain integer arithmetic. An expression like x + y*4 + 12 compiles beautifully onto address-generation hardware, precisely because that hardware was already sitting there, unused, whenever the instruction wasn't actually accessing memory.

The insight in this section isn't an artifact of the toy ISA we've been tracing. It's load-bearing in production silicon.


Putting It Together — a Five-Instruction Program, Traced End to End

Individually, each instruction is a closed loop: fetch, decode, compute, maybe touch memory, maybe write back, advance the PC. The reason a CPU is useful is that instructions compose — the register a LOAD writes becomes the register an ADD reads two cycles later. Here's a small five-instruction sequence that sums one array element into a running total, stores the result, and checks whether a pointer has reached the end of the array — the shape of a loop body, even though we haven't built an actual looping construct yet. That's the subject of a later part; for now we just walk off the end of the sequence.

Starting state:

R1 = 0x2000   (pointer to current array element)
R2 = 0x3000   (address of the output/accumulator slot in memory)
R3 = 0        (scratch register)
R4 = 0        (running sum)
R6 = 4        (stride: one 4-byte word)
R7 = 0x2004   (address the pointer should equal once the array is exhausted)
 
Memory[0x2000] = 15   (the one array element in this trace)
Memory[0x3000] = 0    (output slot, not yet written)
 
PC = 0x1000
0x1000:  LOAD  R3, [R1 + 0]     ; R3 <- Memory[R1]        (load current element)
0x1004:  ADD   R4, R4, R3       ; R4 <- R4 + R3            (accumulate)
0x1008:  ADD   R1, R1, R6       ; R1 <- R1 + R6            (advance pointer by one word)
0x100C:  STORE R4, [R2 + 0]     ; Memory[R2] <- R4         (write running sum out)
0x1010:  BEQ   R1, R7, 0x1020   ; if (R1 == R7) PC <- 0x1020

Walking it instruction by instruction, using exactly the mechanics established above:

#PCInstructionALU opALU resultMemory effectRegister write-backNext PC
10x1000LOAD R3,[R1+0]0x2000 + 00x2000 (address)read Memory[0x2000] = 15R3 ← 150x1004
20x1004ADD R4,R4,R30 + 1515noneR4 ← 150x1008
30x1008ADD R1,R1,R60x2000 + 40x2004noneR1 ← 0x20040x100C
40x100CSTORE R4,[R2+0]0x3000 + 00x3000 (address)write Memory[0x3000] ← 15none0x1010
50x1010BEQ R1,R7,0x10200x2004 − 0x20040, Zero=1nonenone0x1020 (branch taken)

Watching the state evolve one instruction at a time makes the composition more concrete than the summary table alone:

After instructionR1R3R4Memory[0x2000]Memory[0x3000]
start0x200000150
1 (LOAD)0x2000150150
2 (ADD accumulate)0x20001515150
3 (ADD pointer bump)0x20041515150
4 (STORE)0x200415151515
5 (BEQ)0x200415151515

Notice that instruction 5 changes no register and no memory location at all — its entire effect is on the PC, which the table above doesn't even have a column for. That's easy to lose track of if you only look at register/memory state; branches are the one instruction class in this trace whose "output" lives entirely in control flow rather than data.

Final machine state:

R1 = 0x2004   R2 = 0x3000   R3 = 15   R4 = 15   R6 = 4   R7 = 0x2004
Memory[0x2000] = 15   (unchanged — it was only read)
Memory[0x3000] = 15   (written by instruction 4)
PC = 0x1020

A few things worth pointing at directly:

  • Instruction 1's write-back is instruction 2's read. R3 is written by LOAD in one cycle and consumed by ADD in the very next one. Nothing "remembers" this connection except the fact that both instructions name the same register — the register file is the only channel of communication between them.
  • Instruction 3 reuses the exact same ALU operation as instruction 1's address calculation, but here the result — 0x2004is the answer, written straight back into R1. Same adder, same ADD control signal, completely different downstream fate: this time it's MemToReg-style routing into a register rather than out to the memory unit's address input.
  • Instruction 5's comparison only succeeds because instruction 3 ran first. The branch isn't evaluating some property of the program in the abstract — it's comparing whatever happens to be sitting in R1 and R7 at that specific point in the sequence, which is a direct consequence of every ADD and LOAD that executed before it.

This is the entire idea of a stored-program computer in miniature: five instructions, one shared register file, one shared ALU, one shared memory — and the order in which control signals get asserted, cycle after cycle, is the entire content of the program. Nothing about the hardware "knows" this sequence computes a running sum. It's just executing FETCH → DECODE → EXECUTE → MEMORY → WRITE BACK five times, faithfully, in the order the PC visited the instructions.

Quick Reference: The Same Seven Signals, All Four Instructions

Every trace above set the same seven control wires to different values. Seeing all four side by side is the cleanest way to appreciate how much of an instruction's behavior is captured by seven bits of decode logic:

SignalADDLOADSTOREBEQ
RegWrite1100
ALUSrc0 (register)1 (immediate)1 (immediate)0 (register)
ALUOpADDADDADDSUBTRACT
MemRead0100
MemWrite0010
MemToReg0 (ALU)1 (memory)
Branch0001

Four rows of a truth table, wired to a decoder that reads five or six opcode bits — that's the entire control unit for this instruction set. Everything traced in this post is downstream of that table.


Further Reading

  • Patterson, D. A., & Hennessy, J. L., Computer Organization and Design, RISC-V Edition (2nd ed.), Morgan Kaufmann/Elsevier — the canonical treatment of the single-cycle datapath and its control signals that this post's signal names follow. Publisher page
  • MIT OpenCourseWare, 6.004 Computation Structures (Spring 2017), Lecture 21 annotated slides on instruction execution and datapath control. ocw.mit.edu
  • GeeksforGeeks, "Primary Instruction Cycles." geeksforgeeks.org
  • Wikipedia, "Instruction cycle." en.wikipedia.org
  • Lansakara, S., "What is fetch-decode-execute cycle?", Medium. medium.com
  • Hassu, "Instruction Set Architecture — RISC-V — Single Cycle Datapath," Medium. medium.com

The datapath in this post runs one instruction per clock cycle, start to finish, before the next one is even fetched — which means the clock has to be slow enough to let the slowest instruction (almost always LOAD, with its ALU-then-memory-then-writeback chain) finish every single cycle, even for instructions like ADD that could have finished much sooner. Lesson 1, Part 7 picks up exactly there: why pipelining exists, what latency and throughput actually mean once you stop treating "one instruction per cycle" as a hard rule, and the hazards that show up the moment several instructions are in flight through the same datapath at once.