Instruction Cycle -- Fetch, Decode, Execute, Write-Back

The CPU follows the same four-step process for every instruction, like an unending pipeline beat. This lecture will take you deep into what each of these four steps does, and use Python to simulate the instruction execution process of a complete CPU.


Life analogy: restaurant kitchen

Imagine you are the head chef at a restaurant. After a customer places an order, you prepare the dish in the following steps:

  1. Fetch: Take a new order ticket from the front desk and see what the customer ordered
  2. Decode: Analyze the menu -- "Braised Pork" needs pork belly, soy sauce, and rock sugar; "Stir-fried Vegetables" needs greens and minced garlic
  3. Cooking (Execute): Actually start cooking -- chop vegetables, stir-fry, and season
  4. Serve (Write Back): Plate the finished dish and hand it to the waiter to serve

Every order ticket goes through these four steps. After finishing one dish, go pick up the next ticket. The CPU works the same way—fetch an instruction, decode an instruction, execute an instruction, and write back the result, in a never-ending cycle.

This four-step cycle is the CPU's core working rhythm. Every second from the moment your computer powers on to the moment it shuts down, the CPU continuously repeats the "fetch-decode-execute-write-back" cycle billions of times per second.


Detailed explanation of the four stages

Stage 1: Fetch

The CPU first "fetches" the next instruction to be executed from memory.

There is a special register calledProgram Counter (PC, Program Counter), which always holds the address in memory of the next instruction.

What the fetch stage does:

  1. The CPU places the address from the PC onto the address bus
  2. The CPU sends a "read" signal on the control bus
  3. The memory returns the instruction data at that address
  4. The instruction is placed into the Instruction Register (IR)
  5. The PC automatically increments by 1, pointing to the next instruction

There is a key design here:PC auto-increments. This means the CPU executes instructions in order by default, unless it encounters a jump instruction (such as if-else or loops) that changes the PC value.

Stage 2: Decode

The fetched instruction is a string of binary digits (for example0001 0010 0011), the CPU needs to "translate" it.

The core work of the decode stage:

  • Identify opcode (Opcode): The first few bits of the instruction tell the CPU what to do -- is it addition, subtraction, loading data, or a jump?
  • Extract Operands: The remaining bits specify the operand -- which register to use? Which address in memory?
  • Set control signal: Based on the decode result, the controller issues corresponding control signals to components such as the ALU, register file, and bus.

For example, an instructionADD R1, R2, R3, after decoding, the CPU knows: have the ALU perform an addition operation, with inputs from registers R2 and R3, and store the result in R1.

Stage 3: Execute

This is the stage where the instruction actually "does the work." Depending on the instruction type, the execute stage does different things:

Instruction typeWhat does the execution phase doHardware involved
Arithmetic operations (ADD, SUB, etc.)The ALU performs arithmetic operations on the operandsALU, register file
Logical operations (AND, OR, etc.)The ALU performs bitwise operations on the operandsALU, register file
Memory read (LOAD)Access memory through the address bus to read dataAddress bus, data bus, memory
Memory write (STORE)Access memory through the address bus to write dataAddress bus, data bus, memory
Jump (JUMP, BRANCH)Modify the PC value to change the execution flowController, PC register

Stage 4: Write Back

The result produced in the execution stage needs to be "written" to the target location.

The write-back targets can be:

  • register: Store the operation result into a general-purpose register (most common)
  • Memory: store the result to an address in memory
  • Flag register: Update status flags (such as whether the result is zero, whether it overflows, whether it is negative, etc.)

Once write-back is complete, the execution cycle of one instruction officially ends. If the PC still points to a valid instruction address, the CPU automatically returns to the fetch stage and begins the next instruction.


Timeline view of the instruction cycle

The timeline below shows the complete process of three instructions executing in sequence:

时钟周期:  1      2      3      4      5      6      7      8      9      10     11     12
指令1:    [取指] [译码] [执行] [写回]
指令2:                           [取指] [译码] [执行] [写回]
指令3:                                                  [取指] [译码] [执行] [写回]

         ↑ 指令1 完成后才开始指令2,完全串行执行

Note that each instruction here takes 4 cycles, and the three instructions together take 12 cycles. Later, when we cover pipelining, you'll see how this time can be drastically reduced.


Interactive Demo: SimpleCPU Simulator

The Python program below simulates the complete instruction cycle of a minimalist CPU. It supports LOAD (load value), ADD (addition), MUL (multiplication), CMP (compare + conditional jump), and STORE (print output) instructions, and prints detailed logs for each stage.

Example

"""
SimpleCPU Simulator — Full Instruction Cycle Demo
Supported instructions: LOAD, ADD, MUL, CMP+JGT, STORE, HALT
Every instruction goes through: Fetch (IF) → Decode (ID) → Execute (EX) → Write-back (WB)
"""


class SimpleCPU:
    """A teaching CPU simulator demonstrating the four stages of the instruction cycle"""

    def __init__(self):
        # General-purpose registers: R0~R3, initial values are all 0
        self.registers = {'R0': 0, 'R1': 0, 'R2': 0, 'R3': 0}
        # Flag Register: Records comparison results
        self.flags = {'ZERO': False, 'GREATER': False}
        # Program counter: points to the memory location of the next instruction
        self.pc = 0
        # Instruction register: stores the currently executing instruction
        self.ir = None
        # Memory: Stores the program's instruction sequence
        self.memory = []
        # Count the number of executions at each stage
        self.stats = {'fetch': 0, 'decode': 0, 'execute': 0, 'writeback': 0}
        self.total_cycles = 0
        # Control flags
        self.running = False

    def load_program(self, instructions):
        """Load the instruction list into memory"""
        self.memory = instructions
        self.pc = 0
        self.running = True
        print(f"[System] Program loaded, total {len(instructions)} instructions")
        print(f"[System] Initial registers: {self.registers}")
        print("=" * 60)

    def fetch(self):
        """Stage 1: Instruction Fetch"""
        self.total_cycles += 1
        if self.pc >= len(self.memory):
            self.running = False
            return None

        # Read the instruction pointed to by PC from memory
        instruction = self.memory[self.pc]
        self.ir = instruction
        old_pc = self.pc
        self.pc += 1  # PC auto-increment
        self.stats['fetch'] += 1

        print(f"[Cycle {self.total_cycles}] Instruction Fetch (IF)")
        print(fPC={old_pc} → read instruction from memory address {old_pc})
        print(f" Instruction content:\"{instruction}\"")
        print(fPC update: {old_pc} → {self.pc})
        print(f"  IR ← \"{instruction}\"")
        return instruction

    def decode(self, instruction):
        """Stage 2: Decode (Instruction Decode)"""
        self.total_cycles += 1
        self.stats['decode'] += 1

        # Parse opcode and operand
        parts = instruction.split()
        opcode = parts[0].upper()  # opcode
        operands = parts[1:] if len(parts) > 1 else []  # operand

        print(f"[Cycle {self.total_cycles}] Decode (ID)")
        print(fOpcode: {opcode})
        print(f" Operands: {operands}")

        # Explain the meaning of the opcode
        opcode_meaning = {
            'LOAD': 'Load immediate value into register',
            'ADD':  'Register Addition',
            'MUL':  'Register Multiplication',
            'CMP':  'Compare the values of two registers',
            'JGT':  'Conditional jump (jump when greater)',
            'STORE':'Output register value',
            'HALT': 'Halt execution',
        }
        if opcode in opcode_meaning:
            print(fMeaning: {opcode_meaning[opcode]})

        return opcode, operands

    def execute(self, opcode, operands):
        """Stage 3: Execute"""
        self.total_cycles += 1
        self.stats['execute'] += 1

        print(f[Cycle {self.total_cycles}] Execute (EX))

        if opcode == 'HALT':
            print(f" HALT: End program execution")
            self.running = False

        elif opcode == 'LOAD':
            # LOAD Rx, value → store the immediate value into register Rx
            reg = operands[0]
            value = int(operands[1])
            print(f" Operation: load the immediate value {value} into register {reg}")
            print(fInvolved components: data bus (transfers immediate value {value}))
            # Temporarily store the result; update registers in the write-back stage
            return ('LOAD', reg, value)

        elif opcode == 'ADD':
            # ADD Rx, Ry → Rx = Rx + Ry
            rx, ry = operands[0], operands[1]
            a = self.registers[rx]
            b = self.registers[ry]
            result = a + b
            print(fOperation: ALU executes {rx}({a}) + {ry}({b}) = {result})
            print(f" Components involved: ALU (arithmetic unit), register file")
            return ('WRITE_REG', rx, result)

        elif opcode == 'MUL':
            # MUL Rx, Ry → Rx = Rx * Ry
            rx, ry = operands[0], operands[1]
            a = self.registers[rx]
            b = self.registers[ry]
            result = a * b
            print(fOperation: ALU executes {rx}({a}) * {ry}({b}) = {result})
            print(f" Components involved: ALU (arithmetic unit), register file")
            return ('WRITE_REG', rx, result)

        elif opcode == 'CMP':
            # CMP Rx, Ry → Compare two registers, set flags.
            rx, ry = operands[0], operands[1]
            a = self.registers[rx]
            b = self.registers[ry]
            self.flags['ZERO'] = (a == b)
            self.flags['GREATER'] = (a > b)
            print(fOperation: compare {rx}({a}) and {ry}({b}))
            print(f"  Result: ZERO={self.flags['ZERO']}, GREATER={self.flags['GREATER']}")
            print(f" Components involved: ALU (comparator), flag register")
            return ('FLAGS', None, None)

        elif opcode == 'JGT':
            # JGT address → If the GREATER flag is true, jump to address
            target = int(operands[0])
            if self.flags['GREATER']:
                old_pc = self.pc
                self.pc = target
                print(fCondition met (GREATER=True): PC jump {old_pc} → {target})
                print(f" Involved components: Controller, PC register")
            else:
                print(fCondition not satisfied (GREATER=False): no jump, PC remains {self.pc})
            return None

        elif opcode == 'STORE':
            # STORE Rx → Output the value of the register
            reg = operands[0]
            value = self.registers[reg]
            print(fOperation: Output the value of register {reg})
            return ('OUTPUT', reg, value)

        return None

    def writeback(self, action):
        Stage 4: Write Back
        self.total_cycles += 1
        self.stats['writeback'] += 1

        if action is None:
            print(f[Cycle {self.total_cycles}] Write-Back (WB) - No operation)
            return

        action_type = action[0]
        print(f[Cycle {self.total_cycles}] Write-back (WB))

        if action_type == 'LOAD':
            reg, value = action[1], action[2]
            old_val = self.registers[reg]
            self.registers[reg] = value
            print(f" Target: register {reg}")
            print(f" Data: {old_val} → {value}")
            print(f" Channel: data bus → register file")

        elif action_type == 'WRITE_REG':
            reg, result = action[1], action[2]
            old_val = self.registers[reg]
            self.registers[reg] = result
            print(f" Target: register {reg}")
            print(fData: {old_val} → {result})
            print(f" Channel: ALU output → register file (internal bus)")

        elif action_type == 'OUTPUT':
            reg, value = action[1], action[2]
            print(f" Output: register {reg} = {value}")
            print(fChannel: Register file → Data bus → I/O interface)

        print(fCurrent register state: {self.registers})
        print(fFlags: {self.flags})
        print("-" * 60)

    def run(self):
        """Run the entire program, executing instructions one by one"""
        print("=" * 60)
        print(SimpleCPU simulator startup — instruction cycle demo (example))
        print("=" * 60)

        while self.running:
            # Four-stage cycle
            instruction = self.fetch()          # Stage 1: Fetch
            if instruction is None:
                break

            opcode, operands = self.decode(instruction)  # Stage 2: Decode
            action = self.execute(opcode, operands)      # Stage 3: Execute
            self.writeback(action)                       # Stage 4: Write Back

        # Execution complete, print statistics
        print("\n" + "=" * 60)
        print("Program execution complete! Statistics:")
        print(fTotal clock cycles: {self.total_cycles})
        print(fFetch stage executed: {self.stats['fetch']} times)
        print(fDecode stage execution: {self.stats['decode']} times)
        print(fExecute phase executed: {self.stats['execute']} times)
        print(fWrite-back stage executed: {self.stats['writeback']} times)
        print(f" Final registers: {self.registers}")
        print(f" Final flags: {self.flags}")
        print(f" Final PC: {self.pc}")
        print("=" * 60)


# ===== Run the demo program =====
# Program logic: calculate the result of 1+2, then multiply by 3, and compare with 10.
# LOAD R0, 1   → R0 = 1
# LOAD R1, 2   → R1 = 2
# ADD R0, R1   → R0 = R0 + R1 = 1 + 2 = 3
# LOAD R2, 3   → R2 = 3
# MUL R0, R2   → R0 = R0 * R2 = 3 * 3 = 9
# CMP R0, R2 → Compare R0(9) and R2(3), set GREATER=True
# STORE R0 → output the value of R0
# HALT → stop

cpu = SimpleCPU()
program = [
    "LOAD R0 1",      # R0 = 1
    "LOAD R1 2",      # R1 = 2
    "ADD R0 R1",      # R0 = R0 + R1 = 3
    "LOAD R2 3",      # R2 = 3
    "MUL R0 R2",      # R0 = R0 * R2 = 9
    "CMP R0 R2",      # Compare 9 and 3, GREATER=True
    "JGT 8",          # If GREATER, jump to instruction 8
    "LOAD R3 0",      # (This won't execute because JGT skipped it)
    "STORE R0",       # Output R0 = 9
    "HALT",           # Stop
]
cpu.load_program(program)
cpu.run()

The code above prints detailed logs for every stage of every instruction, so you can clearly see what the CPU does internally in each clock cycle. Observe the following:

  • PC auto-increments: Each time an instruction executes, the PC automatically increments by 1 (fetch stage)
  • Conditional jump: The CMP+JGT combination implements an if statement -- if R0 > R2, it jumps to instruction 8, skipping instruction 7
  • Registers as transfer stations: All data must first be loaded into registers; the ALU reads from registers, computes, and writes the result back to a register

Interactive Demo: CPU Internal Structure & Step-by-Step Instruction Cycle Playback

Below is a simplified CPU internal structure diagram drawn with vis-network. Click the "Next" button to observe which hardware components are used in each of the four instruction stages; activated components are highlighted in the corresponding stage color.

CPU Internal Structure & Instruction Cycle Step-by-Step Demo (example)

Instruction fetch IF
Decode ID
Execute EX
Write back WB
Click 'Next' to start the instruction cycle demo and observe which components inside the CPU are activated at each stage.
other extensions