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:
- Fetch: Take a new order ticket from the front desk and see what the customer ordered
- Decode: Analyze the menu -- "Braised Pork" needs pork belly, soy sauce, and rock sugar; "Stir-fried Vegetables" needs greens and minced garlic
- Cooking (Execute): Actually start cooking -- chop vegetables, stir-fry, and season
- 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:
- The CPU places the address from the PC onto the address bus
- The CPU sends a "read" signal on the control bus
- The memory returns the instruction data at that address
- The instruction is placed into the Instruction Register (IR)
- 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 type | What does the execution phase do | Hardware involved |
|---|---|---|
| Arithmetic operations (ADD, SUB, etc.) | The ALU performs arithmetic operations on the operands | ALU, register file |
| Logical operations (AND, OR, etc.) | The ALU performs bitwise operations on the operands | ALU, register file |
| Memory read (LOAD) | Access memory through the address bus to read data | Address bus, data bus, memory |
| Memory write (STORE) | Access memory through the address bus to write data | Address bus, data bus, memory |
| Jump (JUMP, BRANCH) | Modify the PC value to change the execution flow | Controller, 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.