The Instruction Cycle as a Finite State Machine

A processor looks, from the outside, like it is doing a great many different things — adding, branching, loading, storing. From the inside it is doing the same thing over and over, forever: a small, fixed loop of steps that reads the next instruction and carries it out. That loop is the instruction cycle, and the piece of hardware that walks it is nothing more exotic than a finite state machine. Understand that one loop and you understand the pulse of every computer ever built.

This lesson pins down two things at once: the von Neumann model — the five-part sketch of what a computer is made of — and the control unit that animates it, a state machine whose states are the phases of executing a single instruction. We use the LC-3, Patt & Patel's teaching machine, as the running example, but the shape is universal.

The von Neumann model: five parts and one radical idea

In 1945 John von Neumann's report on the EDVAC crystallised a design that every mainstream computer still follows. It has exactly five parts:

The radical idea is hidden in the first part. In a von Neumann machine, the program and its data live in the same memory, encoded the same way — as numbers. This is the stored-program concept. Before it, "reprogramming" a machine like the ENIAC meant physically rewiring it for days. After it, a program is just data you can load, which means a computer can read, write — even generate — its own instructions. Compilers, operating systems, and the whole software industry are downstream of that one decision.

Notice the two kinds of arrow. Fat data paths (buses) carry numbers — instructions and operands — between memory, the processing unit and the I/O devices. Thin control lines fan out from the control unit to everything else. The control unit sends no data; it sends decisions — "open this gate now", "load that register now" — one fresh set every clock cycle.

The six-phase instruction cycle

Undergraduate courses teach a tidy three-beat loop: fetch → decode → execute. It is true, but it hides the plumbing. Patt & Patel open the "execute" beat up into the steps the hardware actually needs, giving a six-phase cycle. Every instruction the machine will ever run is some walk through these six phases:

Not every instruction visits every phase. An \text{ADD} of two registers skips evaluate address entirely — it never touches memory. A store instruction does almost all its work in store result. The phases are a superset: a menu from which each opcode orders the courses it needs.

The control unit IS this state machine

Here is the key mental switch of the whole module. The six phases are not a flowchart in a textbook — they are the states of a finite state machine implemented in hardware. The control unit holds a small state register (Patt's LC-3 uses a 6-bit one, so 64 possible states). Each clock tick, the current state does two jobs: it drives a specific pattern of control signals out onto the datapath (this gate open, that register loaded), and it computes the next state — usually the next phase, but conditioned on the opcode just decoded and on flags like "the memory is ready". Then the clock ticks and the FSM steps.

Watch the loop close. After STORE RESULT the machine does not stop — it wraps straight back to FETCH and begins the next instruction. There is no "end". A processor at rest is just a machine spinning this loop on \text{NOP}s. Later we will see the one place the loop peeks outside itself: at the very end of the cycle it checks whether an interrupt is pending before looping back — the hook the whole I/O system hangs on.

Three phases or six? Same idea, different resolution

The two views are not rivals; the six-phase version is the three-phase version under a microscope. It is worth seeing exactly where the extra detail lands:

Undergrad 3-beatPatt & Patel 6-phaseWhy the split matters
FetchFETCHone memory read, then bump the PC
DecodeDECODEthe opcode chooses the rest of the path
ExecuteEVALUATE ADDRESSaddress arithmetic is its own step on a real datapath
FETCH OPERANDSreading memory can take many cycles (it waits)
EXECUTEthe ALU operation proper
STORE RESULTwriting back is a separate datapath action

The split is not pedantry. Each extra phase is at least one clock cycle on a real machine, and "fetch operands" from memory can stall for hundreds of cycles on a cache miss. Counting phases is how you start counting cycles — and cycles are what the iron law multiplies into time.

Simulate it: step one instruction through the phases

Let's drive the six-phase FSM by hand over a two-instruction program: an \text{ADD} that never touches memory, then an \text{LDR} load that does. Watch evaluate address get skipped by the ADD and used by the LDR, and watch the \text{PC} advance in FETCH.

// A toy von Neumann machine walked through Patt's six phases. type Op = "ADD" | "LDR"; interface Instr { op: Op; dr: number; a: number; b?: number; base?: number; offset?: number; } const reg = [0, 5, 3, 0, 0, 0, 100, 0]; // R1=5, R2=3, R6=100 (a base address) const mem: number[] = []; mem[105] = 42; // data sitting at address 105 const program: Instr[] = [ { op: "ADD", dr: 3, a: 1, b: 2 }, // R3 <- R1 + R2 { op: "LDR", dr: 4, a: 6, base: 6, offset: 5 },// R4 <- mem[R6 + 5] ]; let PC = 0; const PHASES = ["FETCH", "DECODE", "EVAL_ADDR", "FETCH_OP", "EXECUTE", "STORE"] as const; function cycle(): void { let ir: Instr = program[0]; // set for real in FETCH let ea = 0, x = 0, y = 0, res = 0; for (const phase of PHASES) { if (phase === "FETCH") { ir = program[PC]; PC += 1; console.log(` FETCH IR <- mem[PC]; PC -> ${PC}`); } else if (phase === "DECODE") { console.log(` DECODE opcode = ${ir.op}`); } else if (phase === "EVAL_ADDR") { if (ir.op === "LDR") { ea = reg[ir.base as number] + (ir.offset as number); console.log(` EVAL_ADDR addr = R${ir.base} + ${ir.offset} = ${ea}`); } else console.log(` EVAL_ADDR (skipped -- ADD needs no address)`); } else if (phase === "FETCH_OP") { if (ir.op === "ADD") { x = reg[ir.a]; y = reg[ir.b as number]; console.log(` FETCH_OP R${ir.a}=${x}, R${ir.b}=${y}`); } else { x = mem[ea] ?? 0; console.log(` FETCH_OP data = mem[${ea}] = ${x}`); } } else if (phase === "EXECUTE") { res = ir.op === "ADD" ? x + y : x; console.log(` EXECUTE result = ${res}`); } else { reg[ir.dr] = res; console.log(` STORE R${ir.dr} <- ${res}`); } } } console.log("=== Instruction 1 ==="); cycle(); console.log("=== Instruction 2 ==="); cycle(); console.log(`Final registers: R3 = ${reg[3]}, R4 = ${reg[4]}`);

Nothing but the \text{PC}. The stored-program idea means instructions and data are indistinguishable in memory — the machine only fetches a word as an instruction because the \text{PC} happened to point there. This is a superpower (compilers, JITs, and self-modifying code all exploit it) and a curse: a buffer overflow that overwrites a return address can trick the FETCH phase into pointing at attacker-supplied "data", which the CPU then dutifully executes as code. Modern chips fight back with a no-execute (NX) bit that marks pages as data-only — a small crack in the pure von Neumann model, patching a security hole that the model's own elegance opened.

A classic beginner slip is to imagine the answer is produced in DECODE. It is not. DECODE reads the opcode bits and selects which sequence of later phases (and which control signals) will run — it is a multiplexer, a fork in the FSM, not an ALU. The real work happens in EXECUTE; the data is gathered in FETCH OPERANDS; the answer is written in STORE RESULT. Keep the phases' jobs distinct: FETCH gets the instruction, FETCH OPERANDS gets the data, DECODE just points the machine down the right corridor.

Why this framing runs the whole module

Every lesson that follows is a zoom-in on one part of this picture. The single-bus datapath is the wires the control signals steer. Hardwired vs microprogrammed control is two ways to build this FSM. Memory-mapped I/O is how the Input and Output boxes plug into that same memory. Keep the loop in your head — six phases, forever — and none of it will feel like disconnected trivia.