Opening: from a bit-counting circuit to a "brain" that reads programs
Lesson 1 built an ALU that can add, subtract and compare β but that ALU only computes ONE operation, and only when somebody hands it exactly two operands. The bigger question: how does a whole sequence of operations β a PROGRAM β flow through that ALU automatically, in the right order, without a human pressing a button for each step? The answer is a small machine called a CPU, and one three-step infinite loop that has driven every computer since 1945: Fetch β Decode β Execute.
This lesson builds the series' first Toy CPU β a minimal machine that runs exactly that loop over a mini instruction set we design ourselves, reusing Lesson 1's ALU directly for the arithmetic instructions. Along the way come two foundational architecture questions: should instructions and data SHARE one memory or live in SEPARATE ones (Von Neumann vs Harvard), and should an instruction set be SIMPLE or COMPLEX (RISC vs CISC).
aluExecute() directly for ADD/SUB).
1. Von Neumann vs Harvard
This whole section turns on one word, so let us define it first: a bus is the physical bundle of wires the CPU and memory use to move data between each other. It is the road, not the warehouse. Picture a single-lane bridge joining a factory (the CPU) to a storehouse (memory): every crate crossing that bridge has to queue, whatever is inside it. A bus is the same β at any given moment it can carry exactly one transfer.
Why that matters: the CPU needs two different kinds of thing from memory. The first is an instruction β what to do next. The second is data β the number that instruction has to read or write. If both kinds cross the SAME bridge, they must wait for each other; if there are TWO separate bridges, they travel in parallel. That is the entire difference between the two models below.
In the Von Neumann architecture (John von Neumann, 1945), program instructions and data live in ONE shared memory, reached over ONE shared bus β the CPU cannot tell whether it is reading "an instruction" or "a number" until it actually uses the value. The Harvard architecture is the opposite: two physically separate memories, two separate buses for instructions and data β so the CPU can fetch the NEXT instruction while simultaneously reading or writing the CURRENT instruction's data, avoiding the Von Neumann bottleneck (one shared bus caps total bandwidth).
This lesson's Toy CPU follows the Von Neumann model exactly: a SINGLE ram array holding both
instruction objects and plain data values β with no hard boundary separating an "instruction region" from
a "data region", which is precisely what the original model's "shared bus" means in practice.
// VON NEUMANN: ONE memory array, shared by both instructions and data.
// The CPU cannot tell whether a cell holds "an instruction" or "a number"
// until the moment it uses that cell.
uint32_t unified_memory[MEM_SIZE]; // program and variables both live here
// HARVARD: two PHYSICALLY separate memory arrays, two separate buses.
// Fetching the NEXT instruction and reading/writing the CURRENT one's data
// can happen AT THE SAME TIME.
uint32_t instruction_memory[PROG_SIZE]; // only instruction fetch reads it; never written
uint32_t data_memory[DATA_SIZE]; // freely read/written; never fetched as code
STORE instruction from writing data
straight OVER an address that holds another instruction of that same program. Verified for real (not
reasoned about): running a program whose STORE R0, [2] targets address 2 while address 2
holds the instruction LOADI R1, 7 β the CPU fetches address 2, meets a bare number (99)
instead of a valid instruction object, and genuinely crashes with
"Invalid instruction: undefined". This is the most serious class of security and
undefined-behaviour bug that self-modifying code produces β and exactly why modern operating systems
mark code pages read-only and non-writable (the NX/XD bit) right down at the MMU
hardware level.
2. The Fetch-Decode-Execute cycle
This is the infinite loop that drives every CPU from the moment the chip powers up until it shuts down:
- Fetch: read the instruction at the address the PC (Program Counter) register points to, and load it into the IR (Instruction Register).
-
Decode: interpret
IRto find out which instruction this is and which registers or addresses it needs. - Execute: carry out the real effect (update a register, memory, or the PC itself).
function cpuStep(state) {
const instr = state.ram[state.pc]; // FETCH
state.ir = instr;
state.pc = state.pc + 1; // PC advances NOW - BEFORE decode/execute!
switch (instr.op) { // DECODE
case 'JMP': state.pc = instr.addr; break; // EXECUTE: overwrites the bumped PC
case 'ADD': /* ... calls aluExecute() from Lesson 1 directly ... */ break;
// ...
}
}
The smallest program that runs on this machine is a straight line of instructions with no jumps: load two numbers, add them, write the result to memory, then stop. It is the right example for counting cycles, because in a straight-line program the cycle count equals the instruction count exactly:
const LINEAR = [
{ op: 'LOADI', rd: 0, imm: 5 }, // 0: R0 = 5
{ op: 'LOADI', rd: 1, imm: 3 }, // 1: R1 = 3
{ op: 'ADD', rd: 2, rs1: 0, rs2: 1 }, // 2: R2 = R0 + R1 (via aluExecute, Lesson 1)
{ op: 'STORE', rs: 2, addr: 10 }, // 3: RAM[10] = R2 (STORE names its source "rs")
{ op: 'HALT' }, // 4: stop the loop
];
// Verified by running it: runProgram(LINEAR) -> 5 cycles, R2 = 8, RAM[10] = 8,
// and PC stops at 5.
Two numbers here are worth reading carefully. 5 cycles β exactly the 5 instructions,
because nothing jumps, so each instruction is fetched exactly once. And
the PC stops at 5, not at 4 β because fetching HALT (at address 4) still
bumps the PC to 5 before the machine stops. Put differently, the final PC tells you how many instructions
were fetched, not the address of the last one. That small detail is the root of the pitfall immediately
below.
JMP/BEQ), that freshly incremented PC is
OVERWRITTEN during execute with the absolute target address. Get the order backwards
(assuming the PC increments AFTER the jump) and your microarchitecture computes the wrong target β
verified with the loop that sums $1+2+3+4+5$: the JMP 4 sitting at address 7 must land on
address 4 exactly (not 4+1=5) for the stop-condition test to run again correctly.
Verified with a real loop: a program that computes $1+2+3+4+5$ using a countdown counter plus
BEQ/JMP produces exactly $R_{sum} = 15$, leaves the counter at exactly $0$, and
takes exactly 26 fetch-decode-execute cycles β a number the engine itself produced, not
one written down by hand.
3. The instruction set architecture (ISA)
An ISA (Instruction Set Architecture) is the "contract" between software and hardware: every instruction the CPU understands, together with the parameters each one takes. That contract is the only thing a program author is allowed to rely on β as long as the CPU honours it, the manufacturer is free to rearrange everything inside.
3.1 The Toy CPU's instruction set β the contract we just wrote
In the two sections above you have already seen LOADI, ADD, STORE,
BEQ, JMP and HALT appear inside programs without anyone saying what
they do. That is the Toy CPU's ISA, and this is all of it β 8 instructions, no more:
| Instruction | Parameters | Meaning |
|---|---|---|
LOADI |
rd, imm |
Load a constant written inside the instruction itself into a register:
R[rd] = imm. The "I" stands for immediate β the value sits in the instruction
rather than being fetched from memory.
|
ADD |
rd, rs1, rs2 |
R[rd] = R[rs1] + R[rs2], calling Lesson 1's aluExecute() directly and
updating the status flags too.
|
SUB |
rd, rs1, rs2 |
R[rd] = R[rs1] β R[rs2], also through Lesson 1's ALU. |
LOAD |
rd, addr |
Read memory into a register: R[rd] = RAM[addr]. |
STORE |
rs, addr |
Write a register out to memory: RAM[addr] = R[rs]. Note the parameter is named
rs (a single source), not rs1 as on the arithmetic instructions.
|
JMP |
addr |
Unconditional jump: PC = addr. An ABSOLUTE address, not an offset. |
BEQ |
rs1, rs2, addr |
Conditional jump: if R[rs1] == R[rs2] then PC = addr, otherwise do nothing
and fall through to the next instruction. "BEQ" is branch if equal.
|
HALT |
β | Stop the fetch-decode-execute loop. |
Read the loop program in Section 4 with this table in hand and the line
{ op: 'BEQ', rs1: 0, rs2: 2, addr: 8 } becomes immediately legible: "if R0 equals R2, jump to
address 8". Since R2 is held at 0 throughout the loop, that line says "if the counter has reached 0,
leave".
3.2 Two ISA design philosophies: RISC vs CISC
The Toy CPU above has 8 instructions, each doing exactly one job β that was a design choice, not an inevitability. Out in the world there are two opposing schools. RISC (Reduced Instruction Set Computer β ARM and RISC-V, for example) picks a small, simple, fixed-length instruction set that is easy to decode with minimal hardware. CISC (Complex Instruction Set Computer β x86, for example) allows complex, variable-length instructions where one instruction can do the work of several RISC ones combined.
| Criterion | RISC (ARM, RISC-V) | CISC (x86) |
|---|---|---|
| Instruction length | Fixed (RISC-V: always 32-bit) | Variable (x86: 1β15 bytes) |
| Hardware decode complexity | Simple, fewer transistors in the decoder | Complex β needs a decoder that turns instructions into micro-ops |
| Instructions per task | More, simpler instructions | Fewer instructions, each doing more |
| Practical advantage | Power efficiency, simpler chip design (mobile) | Extremely strong backward compatibility (decades of old software) |
; CISC (x86) - ONE instruction does it all: read, add, write back
add DWORD PTR [rax], 5 ; 1 instruction, variable length; hardware does read-add-write
; RISC (RISC-V) - MUST be split into simple instructions, one job each
lw t0, 0(a0) ; read the value from memory into a register
addi t0, t0, 5 ; add the constant IN THE REGISTER (no memory involved)
sw t0, 0(a0) ; write the result back to memory
4. Hands-on: a simple Toy CPU simulator
This runs exactly the $1+2+3+4+5$ loop program verified in Section 2 β press "Step" to watch the PC, IR and registers update by exactly one fetch-decode-execute cycle per press, or "Run all" to watch all 26 cycles happen at once:
const PROGRAM = [
{ op: 'LOADI', rd: 0, imm: 5 }, // 0: R0 = counter = 5
{ op: 'LOADI', rd: 1, imm: 0 }, // 1: R1 = sum = 0
{ op: 'LOADI', rd: 2, imm: 0 }, // 2: R2 = 0 (the constant BEQ compares against)
{ op: 'LOADI', rd: 3, imm: 1 }, // 3: R3 = 1 (the constant we subtract)
{ op: 'BEQ', rs1: 0, rs2: 2, addr: 8 }, // 4: counter == 0 -> leave the loop
{ op: 'ADD', rd: 1, rs1: 1, rs2: 0 }, // 5: sum += counter
{ op: 'SUB', rd: 0, rs1: 0, rs2: 3 }, // 6: counter -= 1
{ op: 'JMP', addr: 4 }, // 7: go back and re-test the condition
{ op: 'HALT' }, // 8
];
// Verified: runProgram(PROGRAM) -> R1 (sum) = 15, R0 (counter) = 0, exactly 26 cycles.
RAM (Von Neumann β instructions & data together)
Summary
- β A bus is the shared road between CPU and memory β a single-lane bridge, so every transfer has to queue.
- β Von Neumann SHARES memory and bus between instructions and data; Harvard separates them to avoid the bottleneck.
- β Verified: self-modifying code (a STORE overwriting the code region) makes the next fetch read garbage and genuinely crash β exactly why a modern MMU marks code pages read-only.
- β Fetch-Decode-Execute is an infinite loop; the PC increments RIGHT after the fetch, BEFORE a jump gets any chance to overwrite it.
-
β
Verified: the straight-line 5+3 program runs in exactly 5 cycles, writes
RAM[10] = 8and leaves the PC at 5; the BEQ/JMP loop sums 1..5=15 in exactly 26 cycles. - β An ISA is the softwareβhardware contract. The Toy CPU has exactly 8 instructions (LOADI/ADD/SUB/LOAD/STORE/JMP/BEQ/HALT), enough to write a conditional loop.
- β RISC (simple, fixed-length) vs CISC (complex, variable-length) β modern CISC quietly translates down to RISC-style micro-ops.
Review quiz
Question 1
Why does the Von Neumann model create a "bottleneck"?
Question 2
Verified: a STORE instruction overwriting an address that holds another instruction makes
the CPU crash on the next fetch. What is this phenomenon called?
Question 3
In the Fetch-Decode-Execute cycle, at which point does the PC (Program Counter) increment?
Question 4
Which statement about RISC and CISC is CORRECT?
Download the practice code for this lesson
The CPUJS JavaScript file β a mini computer-architecture library used across all 12
lessons. Lesson 2 adds createCpuState(), cpuStep() and
runProgram() β a fetch-decode-execute Toy CPU over a mini instruction set
(LOADI/ADD/SUB/STORE/LOAD/JMP/BEQ/HALT), plus a self-test that checks every number quoted in this lesson
(run node cpu-core.js; nothing to install):
π References
- Core textbook: Computer Organization and Design, MIPS Edition (Patterson & Hennessy) β Chapter 4 covers the fetch-decode-execute cycle and the datapath in detail.
- Von Neumann vs Harvard: Wikipedia β Von Neumann architecture β historical origin and the comparison with the Harvard architecture.
- RISC vs CISC: Wikipedia β Reduced instruction set computer β the history of the RISC/CISC debate and what modern microarchitecture actually does.
Comments