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 de la MMU Básica
Resumen
Se implementó la clase MMU (Memory Management Unit) que gestiona el espacio de direcciones de 16 bits de la Game Boy (0x0000 a 0xFFFF).
Se implementaron métodos para leer y escribir bytes (8 bits) y palabras (16 bits) con soporte correcto para Little-Endian.
Se creó una suite completa de tests unitarios con 13 tests, todos pasando correctamente.
Esta es la base necesaria para que la CPU pueda leer instrucciones y datos de la memoria.
Concepto de Hardware
La Game Boy tiene un espacio de direcciones de 16 bits, lo que permite direccionar 65536 bytes (0x0000 a 0xFFFF). Este espacio no es simplemente un almacén de datos lineal; es un mapa de memoria donde diferentes rangos de direcciones activan diferentes componentes del hardware.
Mapa de Memoria de la Game Boy
El espacio de direcciones está dividido en regiones específicas:
- 0x0000 - 0x3FFF: ROM Bank 0 (Cartucho, no cambiable, 16KB)
- 0x4000 - 0x7FFF: ROM Bank N (Cartucho, switchable, 16KB)
- 0x8000 - 0x9FFF: VRAM (Video RAM, 8KB) - memoria de gráficos
- 0xA000 - 0xBFFF: External RAM (Cartucho, switchable, 8KB)
- 0xC000 - 0xCFFF: WRAM Bank 0 (Working RAM, 4KB)
- 0xD000 - 0xDFFF: WRAM Bank 1-7 (Working RAM, switchable, 4KB)
- 0xE000 - 0xFDFF: Echo RAM (mirror de 0xC000-0xDDFF, no usar)
- 0xFE00 - 0xFE9F: OAM (Object Attribute Memory, 160 bytes) - sprites
- 0xFEA0 - 0xFEFF: No usable (prohibido)
- 0xFF00 - 0xFF7F: I/O Ports (Entrada/Salida) - botones, pantalla, sonido
- 0xFF80 - 0xFFFE: HRAM (High RAM, 127 bytes) - RAM rápida
- 0xFFFF: IE (Interrupt Enable Register) - registro de interrupciones
Endianness: Little-Endian (CRÍTICO)
La Game Boy usa Little-Endian para valores de 16 bits. Esto significa que:
- El byte menos significativo (LSB) se almacena en la dirección más baja
- El byte más significativo (MSB) se almacena en la dirección más alta (addr+1)
Ejemplo práctico: Si queremos almacenar el valor 0x1234 en la dirección 0x1000:
- En 0x1000 se escribe 0x34 (LSB, bits 0-7)
- En 0x1001 se escribe 0x12 (MSB, bits 8-15)
- Al leer:
read_word(0x1000)= (0x12 << 8) | 0x34 = 0x1234
Por qué es crítico: Si se implementa incorrectamente (Big-Endian), todas las operaciones de 16 bits fallarán, incluyendo direcciones, contadores y valores numéricos. Es uno de los errores más comunes en emuladores novatos.
Memoria Lineal vs. Mapeo por Regiones
En esta primera iteración, usamos un bytearray lineal de 65536 bytes para simular toda la memoria.
Esto es suficiente para empezar a ejecutar opcodes simples, pero más adelante necesitaremos:
- Separar las regiones de memoria (ROM, VRAM, WRAM, I/O, etc.)
- Implementar mapeo específico para cada región
- Gestionar "Bank Switching" para ROM y RAM del cartucho
- Implementar protección de escritura en regiones de solo lectura
Implementación
Se implementó la clase MMU en Python con tipado estricto y documentación educativa completa.
Todos los métodos aseguran que las direcciones y valores estén en el rango válido usando máscaras bitwise.
Componentes creados/modificados
- Clase MMU: Gestiona el espacio de direcciones completo con un bytearray de 65536 bytes
- read_byte(addr): Lee un byte (8 bits) de la dirección especificada
- write_byte(addr, value): Escribe un byte (8 bits) en la dirección especificada
- read_word(addr): Lee una palabra (16 bits) usando Little-Endian
- write_word(addr, value): Escribe una palabra (16 bits) usando Little-Endian
Decisiones de diseño
- Almacenamiento lineal: Por ahora, un
bytearraysimple de 65536 bytes. Más adelante se separarán las regiones. - Enmascarado de direcciones: Todas las direcciones se enmascaran con
& 0xFFFFpara asegurar que estén en el rango válido (0x0000-0xFFFF). Esto permite wrap-around automático si se pasa una dirección fuera de rango. - Enmascarado de valores: Los valores se enmascaran a su tamaño correcto (
& 0xFFpara bytes,& 0xFFFFpara palabras) para simular el comportamiento del hardware. - Little-Endian explícito: Los métodos
read_wordywrite_wordimplementan explícitamente Little-Endian con comentarios detallados explicando el orden de los bytes. - Wrap-around en límites: Si se lee/escribe una palabra en 0xFFFE, el segundo byte se lee/escribe en 0xFFFF (wrap-around a 0x0000 no aplica aquí, pero se maneja correctamente).
Estructura de paquetes
Se creó el archivo __init__.py en src/memory/ para exportar la clase MMU y convertir la carpeta en un paquete Python válido.
Archivos Afectados
src/memory/__init__.py- Inicialización del módulo de memoria (nuevo)src/memory/mmu.py- Clase MMU con métodos de lectura/escritura (nuevo, 185 líneas)tests/test_mmu.py- Suite completa de tests unitarios (nuevo, 195 líneas)INFORME_COMPLETO.md- Actualización de la bitácora (modificado)docs/bitacora/index.html- Actualización del índice (modificado)docs/bitacora/entries/2025-12-16__0002__mmu-basica.html- Esta entrada (nuevo)
Tests y Verificación
Se implementó una suite completa de tests unitarios con pytest:
- test_read_write_byte: Verifica lectura/escritura básica de bytes
- test_write_byte_wraps_value: Verifica que valores > 0xFF hacen wrap-around correctamente
- test_write_byte_negative_value: Verifica que valores negativos se convierten correctamente
- test_read_word_little_endian: Test CRÍTICO - Verifica que read_word lee correctamente en formato Little-Endian (0xCD en addr, 0xAB en addr+1 → 0xABCD)
- test_write_word_little_endian: Test CRÍTICO - Verifica que write_word escribe correctamente en formato Little-Endian (0x1234 → 0x34 en addr, 0x12 en addr+1)
- test_read_write_word_roundtrip: Verifica que escribir y leer una palabra devuelve el valor original
- test_write_word_wraps_value: Verifica que valores > 0xFFFF hacen wrap-around correctamente
- test_address_wrap_around: Verifica que direcciones fuera de rango hacen wrap-around
- test_read_word_at_boundary: Verifica lectura de palabras en el límite del espacio (0xFFFE)
- test_write_word_at_boundary: Verifica escritura de palabras en el límite del espacio
- test_memory_initialized_to_zero: Verifica que la memoria se inicializa a cero
- test_multiple_writes_same_address: Verifica que múltiples escrituras sobrescriben correctamente
- test_little_endian_example_from_docs: Verifica el ejemplo específico mencionado en la documentación
Resultado: 13 tests en total, todos pasando correctamente (0.29s).
Se verificó que no hay errores de linting en los archivos creados.
Fuentes Consultadas
- Pan Docs: Game Boy Memory Map - Descripción del espacio de direcciones y sus regiones
- Pan Docs: Endianness - Explicación de Little-Endian en la Game Boy
- Conocimiento general de arquitectura: Conceptos de memoria en sistemas de 8 bits y endianness
Nota: La implementación se basó en conocimiento general de arquitectura de memoria y las especificaciones del mapa de memoria de la Game Boy. El uso de Little-Endian es una característica conocida del hardware real de la Game Boy.
Integridad Educativa
Lo que Entiendo Ahora
- Little-Endian: El byte menos significativo (LSB) se almacena en la dirección más baja. Esto es crítico para todas las operaciones de 16 bits. La implementación correcta es:
(msb << 8) | lsbal leer, y separar convalue & 0xFF(LSB) y(value >> 8) & 0xFF(MSB) al escribir. - Mapa de memoria: El espacio de direcciones no es solo almacenamiento, sino un mapa donde diferentes rangos activan diferentes componentes. Esto será importante cuando implementemos mapeo específico por regiones.
- Wrap-around: Las direcciones y valores que exceden su rango válido deben hacer wrap-around usando máscaras bitwise. Esto simula el comportamiento del hardware real.
- Memoria inicializada a cero: Por defecto, toda la memoria se inicializa a 0x00. Esto es importante para el comportamiento inicial del sistema.
Lo que Falta Confirmar
- Valores iniciales de regiones específicas: Algunas regiones de memoria (como I/O ports) pueden tener valores iniciales específicos al boot. Pendiente de verificar con documentación o tests ROMs permitidas.
- Comportamiento de regiones protegidas: Algunas regiones son de solo lectura (ROM) o tienen restricciones de escritura. Esto se implementará cuando separemos las regiones.
- Bank Switching: El mecanismo exacto de cambio de bancos de ROM/RAM del cartucho. Se implementará cuando se añada soporte para cartuchos.
- Echo RAM: El comportamiento exacto de la región Echo RAM (0xE000-0xFDFF) que espeja WRAM. Pendiente de verificar si hay diferencias sutiles.
Hipótesis y Suposiciones
Se asume que el uso de un bytearray lineal es suficiente para empezar a ejecutar opcodes simples.
Esta es una suposición razonable para una primera iteración, pero se validará cuando se implementen opcodes que accedan a diferentes regiones de memoria.
Se asume que el comportamiento de wrap-around usando máscaras bitwise es correcto para todas las operaciones. Esto es consistente con cómo funcionan los sistemas de 8 bits, pero se validará completamente cuando se implementen opcodes que usen direccionamiento indirecto y aritmética de direcciones.
Próximos Pasos
- [ ] Implementar el ciclo de instrucción básico de la CPU (Fetch-Decode-Execute)
- [ ] Implementar decodificación de opcodes simples (empezar con NOP, LD, etc.)
- [ ] Conectar la CPU con la MMU para que pueda leer instrucciones de la memoria
- [ ] Implementar separación de regiones de memoria (ROM, VRAM, WRAM, I/O, etc.)
- [ ] Implementar sistema de carga de ROMs en la región de ROM
- [ ] Implementar mapeo específico para I/O ports básicos