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).


πŸ“š Prerequisites
Read Lesson 1 first (the 4-bit ALU and its status flags β€” this lesson's Toy CPU calls 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_vs_harvard.c (how the memory is laid out)
// 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
⚠️ Pitfall: self-modifying code β€” when data accidentally overwrites code
Because Von Neumann shares one memory, nothing stops a 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:

  1. Fetch: read the instruction at the address the PC (Program Counter) register points to, and load it into the IR (Instruction Register).
  2. Decode: interpret IR to find out which instruction this is and which registers or addresses it needs.
  3. Execute: carry out the real effect (update a register, memory, or the PC itself).
cpu_step.js (excerpt from the shared cpu-core.js engine)
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:

linear_add_program.js (the straight-line 5+3 program)
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.

⚠️ Pitfall: the PC increments before the jump runs
The PC goes up by 1 IMMEDIATELY after the fetch step β€” BEFORE the instruction is decoded or executed. For a jump instruction (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".

πŸ“ Why this is not yet a real ISA
The Toy CPU stores each instruction as a JavaScript object β€” a real CPU does not: it has nothing but numbers. A RISC-V instruction is exactly 32 bits, and pulling those 32 bits apart into an opcode, register numbers and a constant is what decode genuinely means. Lesson 3 replaces this invented instruction set with RV32I β€” a real one β€” encoded properly into 32-bit numbers.

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)
risc_vs_cisc_add_const.asm (same task: add a constant to a memory cell)
; 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
⚠️ "RISC is always faster than CISC" β€” not quite true
This is a common misconception. Modern x86 (CISC) CPUs do NOT execute complex instructions directly β€” they have a hidden decode stage that translates each CISC instruction into one or more simple micro-ops, then runs those micro-ops on an execution core that is RISC in spirit. In other words, most modern x86 CPUs are "CISC on the outside, RISC at the core" β€” real performance differences come from the specific microarchitecture, not from the RISC/CISC label alone.

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:

loop_sum_program.js (the program running in the demo below)
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.
πŸ“Ÿ Toy CPU simulator β€” Fetch-Decode-Execute

RAM (Von Neumann β€” instructions & data together)

    PC:0
    IR:β€”
    Registers:R0=0 R1=0 R2=0 R3=0
    Initialising…

    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] = 8 and 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):

    Download cpu-core.js

    πŸ“– References

    Related lessons in this series

    Lesson 1: From Logic Gates to the ALU Lesson 3: RISC-V Assembly & the Datapath Back to the Computer Architecture roadmap

    Comments