The Fetch–Decode–Execute Cycle

Concept

The Instruction Cycle A CPU does one thing forever:

Fetch: read the instruction at the address in the PC into the IR; advance the PC.

Decode: split the instruction into fields (opcode, register numbers, immediate) and generate control signals.

Execute: the ALU computes a result, an address, or a branch condition.

Memory access: loads read memory, stores write it.

Write back: the result goes into the destination register.

Branches simply write a new value into the PC. Interrupts are checked between instructions (Module 12).

Single-Cycle, Multi-Cycle and Pipelined CPUs Single-cycle: every instruction completes in one long clock cycle (clock limited by the slowest instruction).

Multi-cycle: each step takes one short cycle; simple instructions finish sooner.

Pipelined: the five steps of *different* instructions overlap like an assembly line (Module 24) — today's standard.

Machine Code and Encoding An instruction is just a bit pattern with fields. In the tiny CPU below, a 16-bit instruction is [opcode:4][rd:2][rs:2][imm:8]; 0x2100 = 0010 00 01 00000000 = ADD r0, r1. Real ISAs work the same way with more fields (RISC-V R-type: funct7 | rs2 | rs1 | funct3 | rd | opcode).

flowchart LR
    F["Fetch: IR = memory[PC]; PC = PC + length"] --> D["Decode: opcode, registers, immediate"]
    D --> E["Execute: ALU operation or address calculation"]
    E --> M["Memory access: load / store"]
    M --> W["Write back: result to register"]
    W --> F

Why It Matters

Everything else in architecture — pipelines, hazards, branch prediction, caches — is an optimization of this loop. Debuggers, emulators (QEMU) and virtual machines (JVM, Python bytecode) implement exactly this cycle in software.

Syntax

In pseudo-code: while (1) { ir = mem[pc]; pc += len; decode(ir); execute(); }.

Example

6.1 A complete CPU simulator running a real machine-code program

The program sums 5 + 4 + 3 + 2 + 1 with a loop and stores the result in memory. The trace prints each instruction's address and encoding and the registers after it executes.

#include <stdio.h>
#include <stdint.h>

/* TinyCPU: 4 registers, 256 bytes of memory, 16-bit instructions [opcode:4][rd:2][rs:2][imm:8].
   The same memory holds the program and the data (von Neumann architecture). */
enum { HALT, LOADI, ADD, SUB, JNZ, STORE, LOAD, ADDI };
static const char *NAMES[] = {"HALT", "LOADI", "ADD", "SUB", "JNZ", "STORE", "LOAD", "ADDI"};

typedef struct {
    uint8_t reg[4];
    uint8_t pc;          /* program counter: address of the next instruction */
    uint16_t ir;         /* instruction register: the instruction being executed */
    int zero;            /* flag set by ADD/SUB/ADDI */
    uint8_t mem[256];
    long cycles;
} Cpu;

#define INSTR(op, rd, rs, imm) (uint16_t)(((op) << 12) | ((rd) << 10) | ((rs) << 8) | ((imm) & 0xFF))

static void store16(Cpu *c, int addr, uint16_t v) { c->mem[addr] = v >> 8; c->mem[addr + 1] = v & 0xFF; }

static void run(Cpu *c, int trace)
{
    for (;;) {
        /* FETCH: read the instruction at PC into IR, advance PC */
        c->ir = (uint16_t)(c->mem[c->pc] << 8 | c->mem[c->pc + 1]);
        uint8_t at = c->pc;
        c->pc += 2;
        /* DECODE: split the bit fields */
        int op = c->ir >> 12, rd = (c-
…

Expected Result

The program is just 16 bytes of data in memory (von Neumann: code and data share memory).

The loop body (addresses 06–0A) ran 5 times; JNZ wrote 06 back into the PC until SUB set Z=1.

19 instructions executed for a 7-instruction loop body plus setup — dynamic instruction count differs from static count.

program bytes: 10 00 14 05 18 01 21 00 36 00 40 06 50 80 00 00 

trace (addr: instruction  mnemonic -> registers after execute):
  00: 1000  LOADI r0,r0,0   -> r0=0   r1=0   r2=0   Z=0 PC=02
  02: 1405  LOADI r1,r0,5   -> r0=0   r1=5   r2=0   Z=0 PC=04
  04: 1801  LOADI r2,r0,1   -> r0=0   r1=5   r2=1   Z=0 PC=06
  06: 2100  ADD   r0,r1,0   -> r0=5   r1=5   r2=1   Z=0 PC=08
  08: 3600  SUB   r1,r2,0   -> r0=5   r1=4   r2=1   Z=0 PC=0A
  0A: 4006  JNZ   r0,r0,6   -> r0=5   r1=4   r2=1   Z=0 PC=06
  06: 2100  ADD   r0,r1,0   -> r0=9   r1=4   r2=1   Z=0 PC=08
  08: 3600  SUB   r1,r2,0   -> r0=9   r1=3   r2=1   Z=0 PC=0A
  0A: 4006  JNZ   r0,r0,6   -> r0=9   r1=3   r2=1   Z=0 PC=06
  06: 2100  ADD   r0,r1,0   -> r0=12  r1=3   r2=1   Z=0 PC=08
  08: 3600  SUB   r1,r2,0   -> r0=12  r1=2   r2=1   Z=0 PC=0A
  0A: 4006  JNZ   r0,r0,6   -> r0=12  r1=2   r2=1   Z=0 PC=06
  06: 2100  ADD   r0,r1,0   -> r0=14  r1=2   r2=1   Z=0 PC=08
  08: 3600  SUB   r1,r2,0   -> r0=14  r1=1   r2=1   Z=0 PC=0A
  0A: 4006  JNZ   r0,r0,6   -> r0=14  r1=1   r2=1   Z=0 PC=06
  06: 2100  ADD   r0,r1,0   -> r0=15  r1=1   r2=1   Z=0 PC=08
  08: 3600  SUB   r1,r2,0   -> r0=15  r1=0   r2=1   Z=1 PC=0A
  0A: 4006  JNZ  
…

Common Mistakes

Forgetting that the PC is advanced during fetch, so branch targets and PC-relative addresses are relative to the *next* instruction on many ISAs (x86 rip-relative addressing uses the address of the next instruction).

Confusing the static program size with the number of executed instructions.

Thinking decode is trivial: for x86 it is one of the most complex parts of the CPU.

Best Practices

When writing an emulator, separate fetch, decode and execute into functions and test each opcode in isolation.

Use a trace (like above) to debug; real CPUs offer similar hardware tracing (Intel PT, ARM ETM).

Interview Relevance

High: "explain the fetch-decode-execute cycle", "what is the program counter?", "what happens on a branch?", "how would you write a CPU emulator?".

Practice Questions

Add a MUL instruction and compute 5! with the simulator.

Add CALL and RET instructions using a stack pointer register.

Encode ADD r2, r1 and LOADI r3, 200 by hand in the simulator's format.

Count how many cycles the program would take on a 5-stage non-pipelined CPU (5 cycles per instruction) vs an ideal pipeline.

Quick Quiz

Q1. (Multiple choice) Which register holds the address of the next instruction? A) IR B) MAR C) PC D) SP Answer: C.

Q2. (Predict the output) The encoding 0x2100 in the simulator decodes to?

Answer: ADD r0, r1 (verified in the trace).

Q3. (True/False) A branch instruction works by changing the program counter.

Answer: True (verified: JNZ set PC=06).

Q4. (Find the bug) The simulator's JNZ sets pc = imm *before* pc += 2 in fetch. What happens?

Answer: Every taken branch would land 2 bytes too far and skip an instruction; the PC must be advanced during fetch, before execute.

Coding Challenge

Write a minimal stack-machine interpreter with instructions PUSH n, ADD, MUL and PRINT, and run the program computing (2 + 3) × 4.

#include <stdio.h>

enum { PUSH, ADD, MUL, PRINT, HALT };

int main(void)
{
    int program[] = {PUSH, 2, PUSH, 3, ADD, PUSH, 4, MUL, PRINT, HALT};
    int stack[16], sp = 0, pc = 0;
    for (;;) {
        int op = program[pc++];                 /* fetch + advance */
        switch (op) {                           /* decode + execute */
            case PUSH:  stack[sp++] = program[pc++]; break;
            case ADD:   sp--; stack[sp - 1] += stack[sp]; break;
            case MUL:   sp--; stack[sp - 1] *= stack[sp]; break;
            case PRINT: printf("result: %d\n", stack[sp - 1]); break;
            case HALT:  return 0;
        }
    }
}