The Fetch-Decode-Execute Cycle: Tracing ADD, LOAD, STORE, and BEQ
Part 6 of 15
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.
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 computePC + 4as 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.
ADDfinishes its useful work after EXECUTE;LOADhas to wait for EXECUTE, then a full memory access, then WRITE BACK before the cycle can end — and every instruction, includingADD, 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:
| Signal | Meaning when asserted |
|---|---|
RegWrite | Write a value into the register file this cycle |
ALUSrc | ALU's second operand comes from the immediate field, not a register |
ALUOp | Which operation the ALU performs (ADD, SUB, …) |
MemRead | Read from data memory at the ALU-computed address |
MemWrite | Write to data memory at the ALU-computed address |
MemToReg | The write-back value comes from memory, not from the ALU |
Branch | This 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
| Signal | Value |
|---|---|
PC | 0x1000 |
Instruction = Memory[PC] | ADD R3, R1, R2 |
PC + 4 (computed, not yet committed) | 0x1004 |
Stage: DECODE
| Signal | Value |
|---|---|
opcode | ADD |
rs1 | R1 |
rs2 | R2 |
rd | R3 |
RegWrite | 1 |
ALUSrc | 0 (second operand is a register) |
ALUOp | ADD |
MemRead | 0 |
MemWrite | 0 |
MemToReg | 0 (result comes from the ALU) |
Branch | 0 |
Register file read: R1 | 10 |
Register file read: R2 | 20 |
Stage: EXECUTE
| Signal | Value |
|---|---|
ALU operand A | 10 |
ALU operand B | 20 (register, since ALUSrc = 0) |
| ALU operation | ADD |
| ALU result | 30 |
Stage: MEMORY
| Signal | Value |
|---|---|
MemRead | 0 — memory not touched |
MemWrite | 0 — memory not touched |
| Value passed through to write-back | 30 (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
| Signal | Value |
|---|---|
MemToReg | 0 — select ALU result, not memory output |
| Write-back value | 30 |
RegWrite | 1 — commit the write |
Register file write: R3 | 30 |
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
| Signal | Value |
|---|---|
PC | 0x1000 |
Instruction = Memory[PC] | LOAD R3, [R1 + 8] |
PC + 4 | 0x1004 |
Stage: DECODE
| Signal | Value |
|---|---|
opcode | LOAD |
rs1 | R1 |
rd | R3 |
immediate | 8 |
RegWrite | 1 |
ALUSrc | 1 (second operand is the immediate, not rs2) |
ALUOp | ADD |
MemRead | 1 |
MemWrite | 0 |
MemToReg | 1 (result comes from memory, not the ALU) |
Branch | 0 |
Register file read: R1 | 0x1000 |
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
| Signal | Value |
|---|---|
ALU operand A | 0x1000 (from R1) |
ALU operand B | 8 (immediate, since ALUSrc = 1) |
| ALU operation | ADD |
| ALU result | 0x1008 |
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
| Signal | Value |
|---|---|
MemRead | 1 |
| Memory address (from ALU) | 0x1008 |
Memory[0x1008] | 42 |
| Data read out | 42 |
Stage: WRITE BACK
| Signal | Value |
|---|---|
MemToReg | 1 — select the memory output, not the ALU result |
| Write-back value | 42 |
RegWrite | 1 |
Register file write: R3 | 42 |
PC ← PC + 4 | 0x1004 |
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 = 1is 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
| Signal | Value |
|---|---|
PC | 0x1000 |
Instruction = Memory[PC] | STORE R3, [R1 + 8] |
PC + 4 | 0x1004 |
Stage: DECODE
| Signal | Value |
|---|---|
opcode | STORE |
rs1 (base address register) | R1 |
rs2 (value to store) | R3 |
immediate | 8 |
RegWrite | 0 |
ALUSrc | 1 |
ALUOp | ADD |
MemRead | 0 |
MemWrite | 1 |
MemToReg | don't care — nothing is written back |
Branch | 0 |
Register file read: R1 | 0x1000 |
Register file read: R3 | 42 |
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
| Signal | Value |
|---|---|
ALU operand A | 0x1000 (from R1) |
ALU operand B | 8 (immediate) |
| ALU operation | ADD |
| ALU result (address) | 0x1008 |
| Store-data value (bypasses the ALU) | 42 (from R3, carried on a separate wire) |
Stage: MEMORY
| Signal | Value |
|---|---|
MemWrite | 1 |
| Memory address (from ALU) | 0x1008 |
| Data in (from register file, not ALU) | 42 |
Memory[0x1008] ← | 42 |
Stage: WRITE BACK
| Signal | Value |
|---|---|
RegWrite | 0 — no register file write this cycle |
PC ← PC + 4 | 0x1004 |
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
| Signal | Value |
|---|---|
PC | 0x1000 |
Instruction = Memory[PC] | BEQ R1, R2, 0x2000 |
PC + 4 (fall-through candidate) | 0x1004 |
Stage: DECODE
| Signal | Value |
|---|---|
opcode | BEQ |
rs1 | R1 |
rs2 | R2 |
| target address (decoded from instruction) | 0x2000 |
RegWrite | 0 |
ALUSrc | 0 (comparison uses two registers) |
ALUOp | SUBTRACT |
MemRead | 0 |
MemWrite | 0 |
Branch | 1 |
Register file read: R1 | 10 |
Register file read: R2 | 10 |
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
| Signal | Value |
|---|---|
ALU operand A | 10 |
ALU operand B | 10 |
| ALU operation | SUBTRACT |
| ALU result | 0 |
Zero flag | 1 |
Stage: MEMORY
| Signal | Value |
|---|---|
MemRead / MemWrite | 0 / 0 — untouched |
Stage: WRITE BACK
| Signal | Value |
|---|---|
RegWrite | 0 — 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:
BEQdoesn't need a separate comparator circuit. Subtract, then ask "is the result zero" — a question the ALU was already equipped to answer.
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:
| Instruction | ALU computes | What happens to the result |
|---|---|---|
ADD | 10 + 20 | Written directly into a register (MemToReg = 0) |
LOAD | 0x1000 + 8 | Used as a memory address; discarded after the memory access |
STORE | 0x1000 + 8 | Used as a memory address; discarded after the memory access |
BEQ | 10 − 10 | Reduced 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:
- Which values were routed to its inputs (
ALUSrcchoosing register vs. immediate for operand B), and - 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 = 0x10000x1000: 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 <- 0x1020Walking it instruction by instruction, using exactly the mechanics established above:
| # | PC | Instruction | ALU op | ALU result | Memory effect | Register write-back | Next PC |
|---|---|---|---|---|---|---|---|
| 1 | 0x1000 | LOAD R3,[R1+0] | 0x2000 + 0 | 0x2000 (address) | read Memory[0x2000] = 15 | R3 ← 15 | 0x1004 |
| 2 | 0x1004 | ADD R4,R4,R3 | 0 + 15 | 15 | none | R4 ← 15 | 0x1008 |
| 3 | 0x1008 | ADD R1,R1,R6 | 0x2000 + 4 | 0x2004 | none | R1 ← 0x2004 | 0x100C |
| 4 | 0x100C | STORE R4,[R2+0] | 0x3000 + 0 | 0x3000 (address) | write Memory[0x3000] ← 15 | none | 0x1010 |
| 5 | 0x1010 | BEQ R1,R7,0x1020 | 0x2004 − 0x2004 | 0, Zero=1 | none | none | 0x1020 (branch taken) |
Watching the state evolve one instruction at a time makes the composition more concrete than the summary table alone:
| After instruction | R1 | R3 | R4 | Memory[0x2000] | Memory[0x3000] |
|---|---|---|---|---|---|
| start | 0x2000 | 0 | 0 | 15 | 0 |
1 (LOAD) | 0x2000 | 15 | 0 | 15 | 0 |
2 (ADD accumulate) | 0x2000 | 15 | 15 | 15 | 0 |
3 (ADD pointer bump) | 0x2004 | 15 | 15 | 15 | 0 |
4 (STORE) | 0x2004 | 15 | 15 | 15 | 15 |
5 (BEQ) | 0x2004 | 15 | 15 | 15 | 15 |
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 = 0x1020A few things worth pointing at directly:
- Instruction 1's write-back is instruction 2's read.
R3is written byLOADin one cycle and consumed byADDin 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 —
0x2004— is the answer, written straight back intoR1. Same adder, sameADDcontrol signal, completely different downstream fate: this time it'sMemToReg-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
R1andR7at 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:
| Signal | ADD | LOAD | STORE | BEQ |
|---|---|---|---|---|
RegWrite | 1 | 1 | 0 | 0 |
ALUSrc | 0 (register) | 1 (immediate) | 1 (immediate) | 0 (register) |
ALUOp | ADD | ADD | ADD | SUBTRACT |
MemRead | 0 | 1 | 0 | 0 |
MemWrite | 0 | 0 | 1 | 0 |
MemToReg | 0 (ALU) | 1 (memory) | — | — |
Branch | 0 | 0 | 0 | 1 |
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.