← Volver al blogArquitectura de CPU

Construye una CPU desde cero en un simulador

Guía para una CPU didáctica de 8 bits con registros, ALU, bus compartido, RAM de bytes, instrucciones de 16 bits, banderas y control microsecuenciado.

DennyDenny
14 min de lectura
Chalkboard feature illustration for “Construye una CPU desde cero en un simulador”

Resumen: Esta guía define una arquitectura didáctica completa: ruta de datos y espacio de direcciones de 8 bits, RAM de bytes, instrucciones de 16 bits, cuatro registros, ALU, banderas, bus compartido y controlador microsecuenciado.

«Construir una CPU» puede significar desde conectar bloques ya preparados hasta diseñar transistores. Este tutorial se sitúa entre ambos extremos. Construirás un procesador a partir de bloques digitales —registros, multiplexores, decodificadores, una ALU, memoria y un almacén de control— y tratarás las puertas internas de cada bloque como un diseño que se puede probar por separado.

El objetivo no es imitar un núcleo comercial moderno. Es exponer el contrato que debe cumplir la implementación de cada instrucción:

  1. obtener los bytes correctos de la instrucción;
  2. conservarlos durante la decodificación;
  3. mover exactamente los operandos previstos;
  4. realizar la operación definida;
  5. actualizar únicamente el estado arquitectónico permitido; y
  6. seleccionar la dirección correcta de la siguiente instrucción.

1. Fija primero la arquitectura

Muchos proyectos de CPU fallan porque la anchura de memoria, la anchura de instrucción y el incremento del PC cambian a mitad de la construcción. Usa esta especificación de forma coherente:

PropiedadDefinición de la CPU didáctica
Anchura de datos8 bits
Anchura de direcciones8 bits
Memoria256 posiciones direccionadas por byte, con instrucciones y datos unificados
Anchura de instrucción16 bits, almacenados como dos bytes consecutivos
Orden de bytes de instrucciónByte alto primero en la dirección inicial de la instrucción
Avance secuencial del PCDos incrementos de un byte durante la obtención
Registros generalesCuatro registros de 8 bits: R0–R3
Registros de operandosA y B, de 8 bits cada uno
Registro de instrucciones16 bits
BanderasZ (cero), N (negativo), C (acarreo), V (desbordamiento con signo)
InterconexiónUn bus didáctico de 8 bits
ControlContador de micro pasos más ROM de control

Una dirección de 8 bits selecciona 256 posiciones; la memoria tiene 256 bytes porque esta arquitectura define explícitamente que cada posición ocupa un byte. Las direcciones iniciales de las instrucciones suelen ser pares porque cada instrucción ocupa dos bytes.

Diagrama de bloques

                         ┌──────────────────────┐
                         │   control ROM +      │
                         │ microstep sequencer  │
                         └──────────┬───────────┘
                                    │ control signals
                                    ▼
 ┌────┐   ┌─────┐   ┌────────────── 8-bit shared bus ───────────────┐
 │ PC │──▶│ MAR │   │       │       │       │       │       │      │
 └────┘   └──┬──┘  ┌▼──┐  ┌▼──┐  ┌▼──┐  ┌▼──┐  ┌▼──┐  ┌▼──┐  ┌▼───┐
             │     │MDR│  │ R0│  │ R1│  │ R2│  │ R3│  │ A │  │ B │
             ▼     └─┬─┘  └───┘  └───┘  └───┘  └───┘  └┬──┘  └┬───┘
        ┌────────┐   │                                    └──┬───┘
        │256 × 8 │   │                                  ┌────▼────┐
        │  RAM   │   │                                  │ 8-bit   │
        └────────┘   │                                  │  ALU    │
                     │                                  └────┬────┘
                     └──────▶ 16-bit IR ◀────────────────────┘
                                      │
                                      ▼
                                opcode / fields

El dibujo es conceptual. El IR carga dos bytes separados desde el bus, y el resultado de la ALU llega al bus a través de su propia ruta de salida seleccionada.

Abre el ejecutor secuencial de instrucciones como referencia, pero considera esta arquitectura escrita como la autoridad del ejercicio. Si la plantilla usa una convención interna distinta, documenta la diferencia en lugar de combinar ambas en silencio.

2. Define los formatos de instrucción

La ISA personalizada usa dos formatos de 16 bits.

Formato de registro

15            12 11             8 7              4 3              0
┌───────────────┬────────────────┬────────────────┬────────────────┐
│ opcode [3:0]  │ RA [3:0]       │ RB [3:0]       │ RD [3:0]       │
└───────────────┴────────────────┴────────────────┴────────────────┘

Formato inmediato/de dirección

15            12 11             8 7                               0
┌───────────────┬────────────────┬─────────────────────────────────┐
│ opcode [3:0]  │ RD [3:0]       │ immediate / address [7:0]       │
└───────────────┴────────────────┴─────────────────────────────────┘

En esta máquina de cuatro registros solo son legales los códigos de registro del 0 al 3. El decodificador debe rechazar o llevar de forma segura a una trampa los valores 4–15, en lugar de seleccionar accidentalmente una línea de control.

OpcodeMnemónicoFormatoEfecto definido
0NOP—Sin cambio de estado arquitectónico, salvo que el PC ya avanzó
1LDIInmediatoRD ← imm8
2LDInmediatoRD ← RAM[addr8]
3STInmediatoRAM[addr8] ← RD
4ADDRegistroRD ← RA + RB; actualiza banderas
5SUBRegistroRD ← RA − RB; actualiza banderas
6JMPInmediatoPC ← addr8
7JZInmediatoSi Z = 1, PC ← addr8
8HLT—Entra en el estado detenido del controlador

Todos los demás opcodes son ilegales. Añade una ruta explícita illegal_instruction o asígnalos a una trampa/parada segura durante la depuración.

3. Construye registros sin bloquear el reloj

Un registro de 8 bits son ocho biestables D activados por flanco que comparten un reloj y una habilitación lógica de carga.

Para cada bit:

Dff={Dbus,if load=1Q,if load=0D_{\text{ff}} = \begin{cases} D_{\text{bus}}, & \text{if load}=1 \\ Q, & \text{if load}=0 \end{cases}

Implementa esa ecuación con un multiplexor 2:1 antes de la entrada D, o usa la función de habilitación de reloj específica del dispositivo. Mantén todos los elementos de estado en el mismo reloj global.

No crees un reloj local aplicando AND al reloj global con load. El bloqueo de reloj sin control puede introducir sesgo y glitches en el hardware. Las celdas integradas de bloqueo de reloj y los recursos de control de reloj de FPGA son mecanismos distintos y conscientes de la temporización.

Cada registro conectado al bus necesita:

  • una entrada de 8 bits desde el bus;
  • una salida almacenada de 8 bits;
  • un control load_x; y
  • una ruta oe_x autorizada para controlar el bus didáctico.

El PC necesita además una ruta de incremento y una ruta de carga explícita. El IR necesita cargas independientes para el byte alto y el bajo. MAR contiene una dirección de memoria; MDR captura o proporciona un byte de datos de memoria.

Verifica un registro antes de clonarlo

Prueba estos casos:

  1. load = 0: varios relojes dejan Q sin cambios.
  2. load = 1: Q captura el bus en el flanco activo.
  3. Los datos cambian lejos del flanco: Q cambia solo en el siguiente flanco habilitado.
  4. Reinicio, si se proporciona: Q alcanza el estado documentado.
  5. Habilitación de salida desactivada: el registro no controla el bus.

Un simulador digital puede verificar la secuencia lógica de este modelo. No reproduce la metastabilidad analógica ni demuestra la temporización física de configuración y mantenimiento.

4. Construye la ALU y define cada bandera

Usa A y B como registros estables de operandos. La ALU es combinacional: no debe contener estado arquitectónico oculto.

alu_opOperaciónResultado
000ADDA + B
001SUBA − B
010ANDA & B
011OR`A
100XORA ^ B
101NOT~A
110SHLA << 1, relleno con ceros
111SHRA >> 1 lógico, relleno con ceros

La guía de la ALU desarrolla en detalle las rutas y el multiplexor de salida. El diseño aritmético central es:

  • una cadena de ocho etapas de sumadores completos;
  • resta implementada como A + (~B) + 1;
  • puertas bit a bit en paralelo;
  • cableado fijo de desplazamiento de un bit; y
  • un multiplexor de salida seleccionado por alu_op.

Definiciones de las banderas

Define las banderas como parte de la ISA, incluido lo que hacen las instrucciones lógicas y de desplazamiento con C y V.

  • Z: 1 cuando todos los bits del resultado son cero.
  • N: bit 7 del resultado, interpretado como bit de signo solo en operaciones con signo.
  • C en ADD: acarreo de salida del bit 7.
  • C en SUB: elige y documenta «sin préstamo» o «préstamo». Esta guía usa sin préstamo, de acuerdo con la convención de acarreo de salida del sumador.
  • V en ADD: los operandos tienen el mismo signo y el resultado tiene el signo contrario.
  • V en SUB: los operandos tienen signos distintos y el signo del resultado difiere del de A.

La fórmula equivalente XOR entre acarreo de entrada y de salida se aplica a la suma y resta en complemento a 2 cuando la ruta de resta se implementa con el operando B invertido y un acarreo de entrada. Consulta Banderas de CPU para ver casos resueltos.

Esta ISA didáctica actualiza Z, N, C y V en ADD y SUB. Para operaciones lógicas y de desplazamiento, elige una política documentada —conservar C/V o definir nuevos significados— antes de escribir el microcódigo. «Lo que tengan los cables» no es una política válida.

Tabla de verificación de la ALU

Como mínimo, prueba:

CasoAspecto esperado
0x00 + 0x00Z
0xFF + 0x01acarreo sin signo y cero
0x7F + 0x01desbordamiento con signo y N
0x80 + 0x80acarreo más desbordamiento con signo
0x00 − 0x01convención de préstamo y N
0x80 − 0x01desbordamiento con signo
Identidades AND/OR/XORrutas bit a bit
desplazamientos de 0x81dirección, bit de relleno y política del bit desplazado

5. Construye el bus compartido

El bus didáctico tiene ocho cables con varias fuentes posibles. Se aplica un invariante central:

∑enabled bus drivers≤1\sum \text{enabled bus drivers} \le 1

Entre los controladores posibles están PC, MDR, R0–R3, A, B, un byte del IR y el resultado de la ALU. La lista exacta es más larga que «el número de registros», así que enumera cada fuente en la especificación de control.

En DigiSim, los buffers triestado pueden modelar un bus compartido:

  • habilita exactamente una fuente durante una transferencia;
  • activa una o más señales de carga de destino para el siguiente flanco activo; y
  • nunca captures el bus cuando ninguna fuente lo controla.

La contención es un error de diseño. Un conflicto físico de salidas puede causar una tensión incorrecta y un exceso de corriente; la consecuencia depende de la tecnología y la duración, así que «el chip siempre se quema» no es una regla útil.

Las arquitecturas FPGA modernas suelen implementar los buses compartidos internos como multiplexores, no como cableado triestado interno real. Los estilos ASIC también varían. El bus triestado sigue siendo un modelo didáctico claro de propiedad y transferencia.

Prueba de arbitraje del bus

Coloca valores conocidos en R0 y R1:

  • R0 = 0xAA
  • R1 = 0x55

Verifica R0 solo, R1 solo, ninguna fuente y ambas fuentes. Los dos últimos casos deben tratarse como estados inválidos del controlador. Después añade una aserción o un indicador visible para detectar inmediatamente errores posteriores del microcódigo.

6. Conecta la memoria de ancho de byte

Usa el componente RAM del simulador como modelo de memoria arquitectónica. No afirmes que la RAM física es literalmente una matriz de registros D: SRAM, DRAM, RAM de bloques y matrices de registros sintetizadas usan celdas de almacenamiento y temporizaciones distintas.

La interfaz de esta CPU es:

  • ADDR[7:0], controlada por MAR;
  • DATA[7:0], transferida mediante MDR o la interfaz de bus controlada;
  • mem_read;
  • mem_write; y
  • una ready opcional si el modelo admite latencia variable.

Este recorrido supone que una lectura puede solicitarse y capturarse en un único micro paso dedicado. Si la memoria elegida es síncrona o tiene una latencia visible, inserta un estado de espera y captura los datos solo cuando sean válidos.

Las instrucciones y los datos comparten los mismos 256 bytes. Un diseño Harvard con memorias separadas también es válido, pero sería una arquitectura y una secuencia de obtención diferentes.

Para distinguir entre las abstracciones de almacenamiento escribible y no volátil/constante, consulta RAM frente a ROM.

7. Construye el controlador microsecuenciado

El controlador convierte un opcode, un micro paso, las banderas y el estado de la máquina en:

  • habilitaciones de salida del bus;
  • habilitaciones de carga de registros;
  • operación de la ALU;
  • controles de lectura/escritura de memoria;
  • controles de incremento/carga del PC;
  • carga de banderas;
  • carga del byte alto/bajo del IR; y
  • control del siguiente micro paso o del fin de instrucción.

Una implementación usa una ROM de control:

control address = {opcode, microstep}
control word    = {oe signals, load signals, alu_op, memory controls, next control}

Las banderas pueden calificar acciones seleccionadas —por ejemplo, load_pc = microcode_load_pc AND Z para JZ— o participar en la dirección del almacén de control. La lógica explícita de siguiente estado suele ser más clara que suponer que el contador de micro pasos siempre se incrementa.

Este es un controlador microcodificado didáctico. Los procesadores reales usan muchas combinaciones de decodificación combinacional, almacenes de control programables, máquinas de estados y secuenciadores. No uses la etiqueta RISC/CISC como atajo para describir la implementación.

Valores predeterminados seguros

Cada entrada no utilizada de la ROM de control debe:

  • deshabilitar todos los controladores del bus;
  • deshabilitar las escrituras de memoria;
  • deshabilitar las escrituras de registros arquitectónicos; y
  • entrar en una ruta definida de instrucción ilegal o parada.

Una palabra de control sin inicializar nunca debe convertirse en «escribir datos arbitrarios en memoria arbitraria».

8. Obtén siempre dos bytes de instrucción

Como la RAM y la ruta de datos tienen 8 bits de ancho, mientras cada instrucción tiene 16 bits, la obtención requiere dos lecturas de byte. El byte alto se almacena primero por definición.

Suponiendo que un micro paso basta para la lectura de RAM seleccionada:

PasoTransferencia de registroControles importantes
F0MAR ← PCoe_pc, load_mar
F1MDR ← RAM[MAR]; PC ← PC + 1mem_read, load_mdr, inc_pc
F2IR[15:8] ← MDRoe_mdr, load_ir_hi
F3MAR ← PCoe_pc, load_mar
F4MDR ← RAM[MAR]; PC ← PC + 1mem_read, load_mdr, inc_pc
F5IR[7:0] ← MDRoe_mdr, load_ir_lo

Después de F5, el IR contiene una instrucción completa y el PC ya apunta a la siguiente instrucción. La decodificación selecciona la secuencia de ejecución.

Si la RAM necesita una espera entre dirección y datos, divide F1 y F4 en pasos de solicitud, espera y captura. Nunca ocultes una obtención de 16 bits dentro de una transferencia de ancho de byte.

9. Define las microoperaciones de ejecución

ADD RD, RA, RB

PasoTransferencia
E0A ← R[RA]
E1B ← R[RB]
E2R[RD] ← A + B; FLAGS ← ALU flags
E3Fin de instrucción; vuelve a F0

RA, RB y RD seleccionan uno de R0–R3 mediante habilitaciones decodificadas. Los códigos de registro inválidos deben entrar en la ruta de instrucción ilegal.

LDI RD, imm8

PasoTransferencia
E0R[RD] ← IR[7:0]
E1Fin de instrucción

LD RD, addr8

PasoTransferencia
E0MAR ← IR[7:0]
E1MDR ← RAM[MAR]
E2R[RD] ← MDR
E3Fin de instrucción

ST RD, addr8

PasoTransferencia
E0MAR ← IR[7:0]
E1MDR ← R[RD]
E2RAM[MAR] ← MDR
E3Fin de instrucción

JMP y JZ

  • JMP: PC ← IR[7:0] y después termina.
  • JZ: carga el PC solo cuando Z es 1 y después termina en cualquier caso.

HLT

Entra en un estado estable del controlador que no emita escrituras. Detener manualmente el reloj del simulador es cómodo, pero el estado arquitectónico debe seguir bien definido aunque continúen los flancos de reloj.

10. Ensambla a mano y carga el programa de prueba

El programa carga 5 y 3, los suma en R2, almacena el resultado en la dirección 0x10 y se detiene.

Dirección inicialInstrucciónPalabra de 16 bitsBytes en memoria
0x00LDI R0, #50x10050x10 0x05
0x02LDI R1, #30x11030x11 0x03
0x04ADD R2, R0, R10x40120x40 0x12
0x06ST R2, 0x100x32100x32 0x10
0x08HLT0x80000x80 0x00

Carga esos diez bytes en las direcciones 0x00 a 0x09 y después reinicia:

  • PC = 0x00
  • micro paso del controlador = F0
  • detenido = 0
  • todos los controladores del bus deshabilitados salvo cuando la palabra de control seleccione uno

Avanza paso a paso la primera obtención y confirma:

  1. después de F2, el byte alto del IR es 0x10;
  2. después de F5, IR es 0x1005;
  3. PC es 0x02;
  4. LDI coloca 0x05 en R0; y
  5. ningún otro registro o posición de memoria cambia.

Al detenerse:

  • R0 = 0x05
  • R1 = 0x03
  • R2 = 0x08
  • RAM[0x10] = 0x08
  • PC = 0x0A

Esos valores finales son necesarios, pero no suficientes. Una CPU defectuosa podría corromper otra posición y aun así producirlos.

11. Verifica invariantes, no solo la respuesta final

Añade comprobaciones para:

  • no más de una habilitación de salida del bus en cada momento;
  • escritura de memoria activa solo durante el paso de escritura ST;
  • bytes del IR cargados solo durante F2 y F5;
  • PC incrementado exactamente dos veces por obtención normal;
  • banderas escritas solo por las instrucciones definidas para actualizarlas;
  • un opcode o código de registro inválido no causa ninguna escritura no prevista;
  • el estado detenido no produce efectos secundarios arquitectónicos; y
  • el reinicio alcanza un estado conocido del controlador.

Para cada instrucción, registra una traza de:

clock, microstep, PC, IR, bus owner, bus value,
register loads, memory read/write, ALU op, flags

Cuando falle una prueba, encuentra el primer ciclo que diverge de la traza esperada. Depurar a partir del valor final incorrecto suele llevarte varios pasos demasiado tarde.

La guía de depuración con osciloscopio explica cómo inspeccionar formas de onda modeladas sin confundirlas con la validación temporal física.

12. Fallos habituales

SíntomaCausa probablePrimera comprobación
El IR contiene bytes intercambiadosEl orden de bytes difiere entre carga y obtenciónDirecciones PC y PC+1
PC avanza cuatroEl incremento ocurre dos veces por byte o se mezclaron las anchurasTraza F0–F5
El bus está en contenciónDos habilitaciones de salida comparten un bit de controlPalabra de control decodificada
El bus no está controlado al capturarloFalta habilitar la fuenteAserción de propietario del bus
ADD escribe el registro incorrectoDecodificación del campo RD u orden de códigos de registroIR[3:0] y líneas de carga
Store corrompe la memoria de instruccionesTemporización de dirección/inmediato o mem_writeMAR y ST E2
El salto usa una Z antiguaNo se escribieron las banderas o JZ muestrea el estado incorrectoADD/SUB E2 y JZ
El controlador nunca vuelve a obtenerFalta la entrada de fin/siguiente estadoTransición de micro paso
Un opcode ilegal ejecuta una operaciónLa ROM de control no utilizada no se inicializó de forma seguraPalabra de control predeterminada

13. Qué enseña y qué no enseña esta CPU

Este diseño enseña transferencia de registros, codificación de instrucciones, propiedad del bus, banderas de la ALU, secuenciación de memoria y estado del controlador.

No modela:

  • RAM o lógica a nivel de transistores;
  • jerarquía de caché o memoria virtual;
  • ejecución especulativa o fuera de orden;
  • comportamiento analógico del reloj o metastabilidad;
  • propagación, enrutamiento, potencia o límites térmicos físicos; ni
  • una arquitectura de seguridad o excepciones de producción.

Su único bus compartido está diseñado para ser fácil de observar, no es una afirmación sobre la topología interna de una CPU moderna.

Extensiones en un orden seguro

  1. Añade pruebas automatizadas de instrucciones. Verifica cada opcode y codificación ilegal antes de añadir funciones.
  2. Añade AND, OR, XOR y desplazamientos. Define primero el comportamiento de las banderas y después añade las secuencias de control.
  3. Añade un salto condicional mediante comparación directa. Esto muestra la diferencia entre las ISA basadas en banderas y las de comparar y saltar.
  4. Añade un puntero de pila y llamada/retorno. Especifica el crecimiento de la pila, el orden de bytes y el comportamiento ante desbordamiento.
  5. Añade una máquina de estados de interrupciones. Guarda una dirección de retorno y las banderas antes de cargar un vector; define la prioridad y el enmascaramiento.
  6. Separa la memoria de instrucciones y datos. Compara la nueva secuencia de obtención con el diseño de memoria unificada.
  7. Segmenta solo después de que pase la referencia multiciclo. Después define la validez de las etapas, las esperas, el forwarding, los vaciados y las excepciones.

Consulta el análisis detallado de obtención–decodificación–ejecución y la guía del registro de instrucciones y la decodificación antes de intentar la extensión segmentada.

Try It

Usa el ejecutor secuencial de instrucciones para orientarte, el sistema ALU de 8 bits para verificar la aritmética por separado y el registro de banderas de CPU para probar el comportamiento del estado. Mantén una arquitectura escrita y una tabla de microoperaciones junto al esquema.

El hito importante no es ver 8 una vez. Es poder explicar por qué se autorizó cada cambio de estado, reproducirlo en una traza y demostrar que ningún otro estado cambió.

Convierte la idea en un circuito funcional.