⚠️ Clean-Room / Educativo

Este proyecto es educativo y Open Source. No se copia código de otros emuladores. Implementación basada únicamente en documentación técnica y tests permitidas.

Implementación del Ciclo de Instrucción de la CPU

Fecha: 2025-12-16 Step ID: 0003 Estado: Verified

Resumen

Se implementó la clase CPU que unifica los Registros y la MMU para crear el ciclo de instrucción Fetch-Decode-Execute. Se implementaron los primeros 3 opcodes básicos: NOP (0x00), LD A,d8 (0x3E) y LD B,d8 (0x06). Se creó una suite completa de tests unitarios con 6 tests, todos pasando correctamente. La CPU ahora puede ejecutar instrucciones secuencialmente, marcando el primer "latido" funcional del emulador.

Concepto de Hardware

El Ciclo de Instrucción es el proceso fundamental que hace que una CPU funcione. Sin él, la CPU es solo una estructura de datos estática. Es el "latido" que convierte el hardware en una máquina ejecutable.

El Ciclo Fetch-Decode-Execute

Toda instrucción de la Game Boy sigue el mismo ciclo básico:

  1. Fetch (Buscar): Lee el byte en la dirección apuntada por el Program Counter (PC). Este byte es el opcode (código de operación).
  2. Increment (Incrementar): Avanza el PC para apuntar al siguiente byte. Esto es crítico porque el PC debe moverse secuencialmente.
  3. Decode (Decodificar): Identifica qué operación representa el opcode (ej: "cargar valor en registro A").
  4. Execute (Ejecutar): Realiza la operación identificada, posiblemente leyendo operandos adicionales de memoria o modificando registros.

Este ciclo se repite indefinidamente mientras la consola está encendida, ejecutando miles de instrucciones por segundo.

Opcodes e Instrucciones

Un opcode es un byte (0x00 a 0xFF) que identifica una operación específica. La Game Boy tiene aproximadamente 500 opcodes diferentes, aunque algunos se repiten con diferentes operandos.

Instrucciones implementadas en este paso:

  • 0x00 - NOP (No Operation): No hace nada, solo consume 1 ciclo de máquina. Útil para alineamiento de tiempo y relleno.
  • 0x3E - LD A, d8: Carga un valor inmediato de 8 bits (d8) en el registro A. Lee el siguiente byte de memoria y lo almacena en A.
  • 0x06 - LD B, d8: Similar a LD A, d8 pero carga el valor en el registro B.

M-Cycles vs T-Cycles (Ciclos de Máquina vs Ciclos de Reloj)

La Game Boy usa dos tipos de ciclos para medir el tiempo:

  • M-Cycle (Machine Cycle): Un ciclo de máquina corresponde a una operación de memoria (lectura o escritura). Es la unidad más útil para medir cuánto tarda una instrucción.
  • T-Cycle (Clock Cycle): Un ciclo de reloj es la unidad básica de tiempo del hardware. En la Game Boy, típicamente 1 M-Cycle = 4 T-Cycles.

Por ahora contamos M-Cycles porque es más simple y suficiente para validar que las instrucciones se ejecutan correctamente. Más adelante necesitaremos T-Cycles para sincronización precisa con otros componentes (PPU, APU, timers).

Program Counter (PC) y Ejecución Secuencial

El Program Counter (PC) es un registro de 16 bits que apunta a la siguiente instrucción a ejecutar. Después de cada instrucción, el PC avanza automáticamente. Esto permite ejecución secuencial de instrucciones en memoria.

Ejemplo: Si tenemos las instrucciones en memoria:

  • 0x0100: 0x3E (LD A, d8)
  • 0x0101: 0x42 (operando: valor a cargar)
  • 0x0102: 0x00 (NOP)

La ejecución es:

  1. PC = 0x0100, lee 0x3E, ejecuta LD A, d8 (lee 0x42 de 0x0101), PC = 0x0102
  2. PC = 0x0102, lee 0x00, ejecuta NOP, PC = 0x0103

Fuente: Pan Docs - CPU Instruction Set y arquitectura LR35902

Implementación

Se implementó la clase CPU que unifica los componentes anteriores (Registers y MMU) para crear un ciclo de instrucción funcional. La implementación es modular y extensible, preparada para añadir los 500 opcodes restantes.

Componentes creados/modificados

  • Clase CPU: Gestiona el ciclo Fetch-Decode-Execute y mantiene referencias a Registers y MMU
  • Método step(): Ejecuta una sola instrucción, devuelve los ciclos consumidos
  • Método fetch_byte(): Helper que lee un byte de memoria en la dirección PC y avanza PC automáticamente
  • Método _execute_opcode(): Dispatch de opcodes usando if/elif (compatible con Python 3.9+, preparado para match/case cuando se migre a 3.10+)
  • Opcodes implementados: NOP (0x00), LD A,d8 (0x3E), LD B,d8 (0x06)

Decisiones de diseño

  • Inyección de dependencias: La CPU recibe la MMU en el constructor, permitiendo tests con mocks y mejor modularidad.
  • Helper fetch_byte(): Facilita la lectura de operandos inmediatos sin repetir código de avance de PC.
  • Manejo de opcodes no implementados: Lanza NotImplementedError con información útil (opcode y PC) para facilitar debugging.
  • Logging: Se usa logging en lugar de print() para trazas de depuración (DEBUG level) que pueden activarse/desactivarse.
  • Compatibilidad Python: Se usa if/elif en lugar de match/case para mantener compatibilidad con Python 3.9 (el entorno actual usa 3.9.6). Se documentó un TODO para migrar a match/case cuando se actualice a Python 3.10+.

Estructura del código

La clase CPU sigue un patrón claro:

CPU
├── __init__(mmu)          # Inicializa registros y guarda referencia a MMU
├── step()                 # Ciclo principal: fetch → decode → execute
├── fetch_byte()           # Helper para leer operandos e incrementar PC
└── _execute_opcode()      # Dispatch de opcodes

Archivos Afectados

  • src/cpu/core.py - Nuevo archivo con la clase CPU e implementación del ciclo de instrucción (170 líneas)
  • src/cpu/__init__.py - Actualizado para exportar la clase CPU
  • tests/test_cpu_core.py - Nuevo archivo con suite completa de tests (6 tests, 204 líneas)

Tests y Verificación

Se creó una suite completa de tests unitarios que valida:

  • Test 1 (test_nop): Verifica que NOP avanza PC en 1 byte y consume 1 ciclo
  • Test 2 (test_ld_a_d8): Verifica que LD A, d8 carga el valor correcto, avanza PC en 2 bytes y consume 2 ciclos
  • Test 3 (test_ld_b_d8): Verifica que LD B, d8 funciona igual que LD A, d8 pero en el registro B
  • Test 4 (test_unimplemented_opcode_raises): Verifica que opcodes no implementados lanzan NotImplementedError
  • Test 5 (test_fetch_byte_helper): Verifica que fetch_byte() lee correctamente y avanza PC
  • Test 6 (test_multiple_instructions_sequential): Verifica ejecución secuencial de múltiples instrucciones

Resultado:6 tests pasando (ejecutado con pytest)

Validación adicional:

  • Verificación de que PC avanza correctamente después de cada instrucción
  • Verificación de que los registros se actualizan correctamente con valores inmediatos
  • Verificación de que los ciclos se cuentan correctamente
  • Sin errores de linting (verificado con read_lints)

Fuentes Consultadas

  • Pan Docs - CPU Instruction Set: Referencia para opcodes y ciclos de máquina
  • Pan Docs - LR35902 Architecture: Arquitectura general del procesador

Nota: Implementación clean-room basada únicamente en documentación técnica pública. No se consultó código de otros emuladores (mGBA, Gambatte, SameBoy, etc.).

Integridad Educativa

Lo que Entiendo Ahora

  • Ciclo Fetch-Decode-Execute: Es el bucle fundamental que hace funcionar una CPU. Sin este ciclo, los registros y la memoria son solo estructuras de datos estáticas.
  • Program Counter (PC): Debe avanzar automáticamente después de cada instrucción para permitir ejecución secuencial. El helper fetch_byte() facilita esto.
  • Opcodes: Son bytes que identifican operaciones. La mayoría de opcodes tienen operandos que siguen inmediatamente después en memoria.
  • M-Cycles: Por ahora contamos M-Cycles (ciclos de máquina) porque es más simple. Más adelante necesitaremos T-Cycles para sincronización precisa.
  • Modularidad: La CPU depende de MMU pero no viceversa. Esto permite tests independientes y mejor arquitectura.

Lo que Falta Confirmar

  • Timing preciso: Algunas instrucciones pueden tener variaciones en timing dependiendo de condiciones. Esto se validará con tests ROM cuando implementemos más opcodes.
  • Interrupciones: El ciclo de instrucción debe poder ser interrumpido. Esto se implementará más adelante.
  • Opcodes CB (prefijo): La Game Boy tiene un prefijo especial 0xCB que cambia el significado de los siguientes 256 opcodes. Esto se implementará más adelante.
  • Opcodes condicionales: Muchas instrucciones tienen versiones condicionales que dependen de flags. Necesitaremos lógica de branching.

Hipótesis y Suposiciones

Asumido (a validar con tests ROM):

  • El timing de M-Cycles es correcto según Pan Docs (NOP=1, LD r8,d8=2). Se validará con ejecución de programas reales.
  • El comportamiento de opcodes no implementados (NotImplementedError) es correcto para desarrollo. En un emulador completo, todos los opcodes deben estar implementados.

Próximos Pasos

  • [ ] Implementar más opcodes de carga (LD): LD C,d8, LD D,d8, LD E,d8, LD H,d8, LD L,d8
  • [ ] Implementar opcodes de carga entre registros (LD r8, r8)
  • [ ] Implementar opcodes de carga desde/hacia memoria (LD A,(HL), LD (HL),A, etc.)
  • [ ] Implementar opcodes aritméticos básicos (ADD, SUB, INC, DEC)
  • [ ] Implementar opcodes de salto (JP, JR) para cambiar el flujo de ejecución
  • [ ] Organizar los opcodes de forma escalable (tabla de dispatch, módulos separados)
  • [ ] Migrar a Python 3.10+ y usar match/case para dispatch de opcodes