Ciclo de instrucción -- Fetch, Decode, Execute, Write Back
La CPU ejecuta cada instrucción siguiendo el mismo proceso de cuatro pasos, como un ritmo de pipeline interminable. Esta lección te llevará a comprender en profundidad qué hace cada uno de los cuatro pasos y usará Python para simular el proceso completo de ejecución de instrucciones de una CPU.
Analogía cotidiana: la cocina de un restaurante
Imagina que eres el chef principal de un restaurante. Cuando el cliente hace un pedido, sigues estos pasos para preparar el plato:
- Tomar el pedido (Fetch): tomar una nueva comanda del mostrador y ver qué ha pedido el cliente
- Leer la comanda (Decode): analizar el menú — el «cerdo estofado» necesita panceta, salsa de soja y azúcar de roca; el «salteado de verduras» necesita verduras y ajo picado.
- Cocinar (Execute): encender el fuego y cocinar de verdad: cortar, saltear y sazonar
- Servir el plato (Write Back): emplatar el plato cocinado y entregarlo al camarero para que lo lleve
Cada comanda debe pasar por estos cuatro pasos. Cuando terminas un plato, tomas la siguiente comanda. La CPU también funciona así: toma una instrucción, la decodifica, la ejecuta, escribe el resultado y repite el ciclo.
Este ciclo de cuatro pasos es el ritmo de trabajo más esencial de la CPU. Cada segundo, desde el encendido hasta el apagado del ordenador, la CPU repite continuamente el ciclo «Fetch-Decode-Execute-Write Back», miles de millones de veces por segundo.
Explicación detallada de las cuatro etapas
Etapa 1: Búsqueda (Fetch)
La CPU primero debe «tomar» de la memoria la siguiente instrucción a ejecutar.
Hay un registro especial llamadocontador de programa (PC, Program Counter), que siempre guarda la dirección en memoria de la siguiente instrucción.
¿Qué hace la etapa de búsqueda (Fetch)?
- La CPU coloca la dirección del PC en el bus de direcciones
- La CPU envía una señal de «lectura» por el bus de control
- La memoria devuelve los datos de la instrucción en esa dirección
- La instrucción se coloca en el registro de instrucción (IR, Instruction Register)
- El PC se incrementa automáticamente en 1, apuntando a la siguiente instrucción
Aquí hay un diseño clave:El PC se incrementa automáticamente. Esto significa que, por defecto, la CPU ejecuta las instrucciones en orden, a menos que encuentre una instrucción de salto (como if-else, bucles) que modifique el valor del PC.
Etapa 2: Decodificación (Decode)
La instrucción extraída es una cadena de números binarios (por ejemplo,0001 0010 0011), la CPU necesita «traducirla».
El trabajo principal de la etapa de decodificación:
- Identificar el código de operación (Opcode): los primeros bits de la instrucción le dicen a la CPU qué hacer — ¿suma, resta, carga de datos o salto?
- Extraer los operandos (Operands): los bits posteriores de la instrucción indican los objetos de la operación — ¿qué registro? ¿qué dirección de memoria?
- Establecer las señales de control: el controlador, según el resultado de la decodificación, envía las señales de control correspondientes a la ALU, al banco de registros, a los buses, etc.
Por ejemplo, una instrucciónADD R1, R2, R3, tras decodificarla la CPU sabe: hacer que la ALU realice una suma, con entradas de los registros R2 y R3, y guardar el resultado en R1.
Etapa 3: Ejecución (Execute)
Esta es la etapa en la que la instrucción realmente «trabaja». Según el tipo de instrucción, las acciones en la etapa de ejecución son diferentes:
| Tipo de instrucción | Qué hace la etapa de ejecución | Hardware involucrado |
|---|---|---|
| Operaciones aritméticas (ADD, SUB, etc.) | La ALU realiza la operación con los operandos | ALU, banco de registros |
| Operaciones lógicas (AND, OR, etc.) | La ALU realiza operaciones de bits sobre los operandos | ALU, banco de registros |
| Lectura de memoria (LOAD) | Accede a la memoria a través del bus de direcciones y lee los datos | Bus de direcciones, bus de datos, memoria |
| Escritura en memoria (STORE) | Accede a la memoria a través del bus de direcciones y escribe los datos | Bus de direcciones, bus de datos, memoria |
| Salto (JUMP, BRANCH) | Modifica el valor del PC y cambia el flujo de ejecución | Controlador, registro PC |
Etapa 4: Escritura (Write Back)
El resultado producido en la etapa de ejecución debe «escribirse» en la posición de destino.
Los destinos de la escritura pueden ser:
- Registro: guardar el resultado de la operación en un registro de propósito general (el más común)
- Memoria: guardar el resultado en alguna dirección de la memoria
- Registro de banderas: actualizar las banderas de estado (por ejemplo, si el resultado es cero, si hay desbordamiento, si es negativo, etc.)
Una vez completada la escritura, el ciclo de ejecución de la instrucción termina formalmente. Si el PC todavía apunta a una dirección de instrucción válida, la CPU vuelve automáticamente a la etapa de Fetch y comienza la siguiente instrucción.
Vista de línea de tiempo del ciclo de instrucción
La siguiente línea de tiempo muestra el proceso completo de ejecución secuencial de 3 instrucciones:
时钟周期: 1 2 3 4 5 6 7 8 9 10 11 12
指令1: [取指] [译码] [执行] [写回]
指令2: [取指] [译码] [执行] [写回]
指令3: [取指] [译码] [执行] [写回]
↑ 指令1 完成后才开始指令2,完全串行执行
Observa que aquí cada instrucción usa 4 ciclos, y las 3 instrucciones usan 12 ciclos en total. Más adelante, cuando se trate el pipeline, verás cómo se puede reducir drásticamente ese tiempo.
Demostración interactiva: simulador SimpleCPU
El siguiente programa en Python simula el ciclo de instrucción completo de una CPU muy sencilla. Admite las instrucciones LOAD (cargar valor), ADD (sumar), MUL (multiplicar), CMP (comparar + salto condicional) y STORE (imprimir salida), e imprime el registro de cada etapa en detalle.
Ejemplo
Simulador SimpleCPU — demostración del ciclo de instrucción completo
Instrucciones admitidas: LOAD, ADD, MUL, CMP+JGT, STORE, HALT
Cada instrucción pasa por: Fetch (IF) → Decode (ID) → Execute (EX) → Write Back (WB)
"""
class SimpleCPU:
"""Un simulador de CPU con fines educativos que muestra las cuatro etapas del ciclo de instrucción"""
def __init__(self):
# Registros de propósito general: R0~R3, valores iniciales todos 0
self.registers = {'R0': 0, 'R1': 0, 'R2': 0, 'R3': 0}
# Registro de banderas: registra el resultado de la comparación
self.flags = {'ZERO': False, 'GREATER': False}
# Contador de programa: apunta a la posición en memoria de la siguiente instrucción
self.pc = 0
# Registro de instrucción: almacena la instrucción que se está ejecutando actualmente
self.ir = None
# Memoria: almacena la secuencia de instrucciones del programa
self.memory = []
# Cuenta el número de veces que se ejecuta cada etapa
self.stats = {'fetch': 0, 'decode': 0, 'execute': 0, 'writeback': 0}
self.total_cycles = 0
# Banderas de control
self.running = False
def load_program(self, instructions):
"""Carga la lista de instrucciones en la memoria"""
self.memory = instructions
self.pc = 0
self.running = True
print(f"[Sistema] Programa cargado correctamente, {len(instructions)} instrucciones en total")
print(f"[Sistema] Registros iniciales: {self.registers}")
print("=" * 60)
def fetch(self):
"""Etapa 1: Búsqueda (Instruction Fetch)"""
self.total_cycles += 1
if self.pc >= len(self.memory):
self.running = False
return None
# Leer de la memoria la instrucción a la que apunta el PC
instruction = self.memory[self.pc]
self.ir = instruction
old_pc = self.pc
self.pc += 1 # El PC se incrementa automáticamente
self.stats['fetch'] += 1
print(f"[Ciclo {self.total_cycles}] Fetch (IF)")
print(f" PC={old_pc} → leer instrucción desde la dirección de memoria {old_pc}")
print(f" Contenido de la instrucción:\"{instruction}\"")
print(f" Actualización del PC: {old_pc} → {self.pc}")
print(f" IR ← \"{instruction}\"")
return instruction
def decode(self, instruction):
"""Etapa 2: Decodificación (Instruction Decode)"""
self.total_cycles += 1
self.stats['decode'] += 1
# Analizar el código de operación y los operandos
parts = instruction.split()
opcode = parts[0].upper() # Código de operación
operands = parts[1:] if len(parts) > 1 else [] # Operandos
print(f"[Ciclo {self.total_cycles}] Decodificación (ID)")
print(f" Código de operación (Opcode): {opcode}")
print(f" Operandos: {operands}")
# Explicar el significado del código de operación
opcode_meaning = {
'LOAD': 'Cargar un valor inmediato en un registro',
'ADD': 'Suma de registros',
'MUL': 'Multiplicación de registros',
'CMP': 'Comparar el valor de dos registros',
'JGT': 'Salto condicional (salto cuando es mayor)',
'STORE':'Mostrar el valor del registro',
'HALT': 'Detener la ejecución',
}
if opcode in opcode_meaning:
print(f" Significado: {opcode_meaning[opcode]}")
return opcode, operands
def execute(self, opcode, operands):
"""Etapa 3: Ejecución (Execute)"""
self.total_cycles += 1
self.stats['execute'] += 1
print(f"[Ciclo {self.total_cycles}] Ejecución (EX)")
if opcode == 'HALT':
print(f" HALT: Terminar la ejecución del programa")
self.running = False
elif opcode == 'LOAD':
# LOAD Rx, value → Almacena el valor inmediato value en el registro Rx
reg = operands[0]
value = int(operands[1])
print(f" Operación: Cargar el valor inmediato {value} en el registro {reg}")
print(f" Componentes involucrados: bus de datos (transmite el valor inmediato {value})")
# Almacenar temporalmente el resultado; la fase de escritura actualizará el registro
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(f" Operación: La ALU ejecuta {rx}({a}) + {ry}({b}) = {result}")
print(f" Componentes involucrados: ALU (unidad aritmético-lógica), banco de registros")
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(f" Operación: La ALU ejecuta {rx}({a}) * {ry}({b}) = {result}")
print(f" Componentes involucrados: ALU (unidad aritmético-lógica), banco de registros")
return ('WRITE_REG', rx, result)
elif opcode == 'CMP':
# CMP Rx, Ry → Compara dos registros y establece las banderas
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(f" Operación: Comparar {rx}({a}) y {ry}({b})")
print(f" Resultado: ZERO={self.flags['ZERO']}, GREATER={self.flags['GREATER']}")
print(f" Componentes involucrados: ALU (comparador), registro de banderas")
return ('FLAGS', None, None)
elif opcode == 'JGT':
# JGT address → Si la bandera GREATER es verdadera, salta a address
target = int(operands[0])
if self.flags['GREATER']:
old_pc = self.pc
self.pc = target
print(f" Condición cumplida (GREATER=True): PC salta {old_pc} → {target}")
print(f" Componentes involucrados: unidad de control, registro PC")
else:
print(f" Condición no cumplida (GREATER=False): no salta, PC permanece en {self.pc}")
return None
elif opcode == 'STORE':
# STORE Rx → Muestra el valor del registro
reg = operands[0]
value = self.registers[reg]
print(f" Operación: Mostrar el valor del registro {reg}")
return ('OUTPUT', reg, value)
return None
def writeback(self, action):
"""Etapa 4: Escritura (Write Back)"""
self.total_cycles += 1
self.stats['writeback'] += 1
if action is None:
print(f"[Ciclo {self.total_cycles}] Escritura (WB) - sin operación")
return
action_type = action[0]
print(f"[Ciclo {self.total_cycles}] Escritura (WB)")
if action_type == 'LOAD':
reg, value = action[1], action[2]
old_val = self.registers[reg]
self.registers[reg] = value
print(f" Destino: registro {reg}")
print(f" Datos: {old_val} → {value}")
print(f" Canal: bus de datos → banco de registros")
elif action_type == 'WRITE_REG':
reg, result = action[1], action[2]
old_val = self.registers[reg]
self.registers[reg] = result
print(f" Destino: registro {reg}")
print(f" Datos: {old_val} → {result}")
print(f" Canal: salida de la ALU → banco de registros (bus interno)")
elif action_type == 'OUTPUT':
reg, value = action[1], action[2]
print(f" Salida: registro {reg} = {value}")
print(f" Canal: banco de registros → bus de datos → interfaz de E/S")
print(f" Estado actual de los registros: {self.registers}")
print(f" Banderas: {self.flags}")
print("-" * 60)
def run(self):
"""Ejecutar todo el programa, instrucción por instrucción"""
print("=" * 60)
print("Iniciando el simulador SimpleCPU — Demostración del ciclo de instrucción (ejemplo)")
print("=" * 60)
while self.running:
# Bucle de las cuatro etapas
instruction = self.fetch() # Etapa 1: Captación (fetch)
if instruction is None:
break
opcode, operands = self.decode(instruction) # Etapa 2: Decodificación (decode)
action = self.execute(opcode, operands) # Etapa 3: Ejecución (execute)
self.writeback(action) # Etapa 4: Escritura (write back)
# Ejecución completada, imprimir estadísticas
print("\n" + "=" * 60)
print("¡Programa ejecutado! Estadísticas:")
print(f" Número total de ciclos de reloj: {self.total_cycles}")
print(f" Ejecuciones de la etapa de captación: {self.stats['fetch']} veces")
print(f" Ejecuciones de la etapa de decodificación: {self.stats['decode']} veces")
print(f" Ejecuciones de la etapa de ejecución: {self.stats['execute']} veces")
print(f" Ejecuciones de la etapa de escritura: {self.stats['writeback']} veces")
print(f" Registros finales: {self.registers}")
print(f" Banderas finales: {self.flags}")
print(f" PC final: {self.pc}")
print("=" * 60)
# ===== Ejecutar programa de demostración =====
# Lógica del programa: calcular el resultado de 1+2, luego multiplicar por 3 y comparar con 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 → Compara R0(9) y R2(3), establece GREATER=True
# STORE R0 → Muestra el valor de R0
# HALT → Detener
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", # Compara 9 y 3, GREATER=True
"JGT 8", # Si GREATER es verdadero, salta a la instrucción 8
"LOAD R3 0", # (Esta no se ejecutará porque JGT la salta)
"STORE R0", # Muestra R0 = 9
"HALT", # Detener
]
cpu.load_program(program)
cpu.run()
El código anterior imprime registros detallados en cada etapa de cada instrucción. Puedes ver claramente qué hace la CPU internamente en cada ciclo de reloj. Presta atención a:
- Incremento automático del PC: Cada vez que se ejecuta una instrucción, el PC se incrementa automáticamente en +1 (etapa de captación)
- Salto condicional: La combinación CMP+JGT implementa una comprobación if: si R0 > R2 salta a la instrucción 8, omitiendo la instrucción 7
- Los registros como intermediarios: Todos los datos deben cargarse primero en los registros; la ALU toma datos de los registros y, después de calcular, escribe el resultado de vuelta en los registros
Demostración interactiva: estructura interna de la CPU y reproducción paso a paso del ciclo de instrucción
A continuación se muestra un diagrama simplificado de la estructura interna de la CPU dibujado con vis-network. Haz clic en el botón «Siguiente» para observar qué componentes de hardware utiliza la instrucción en cada una de las cuatro etapas. Los componentes activados se resaltan con el color correspondiente a cada etapa.