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.
Joypad y Paleta por Defecto
Resumen
Se implementó el Joypad (control de botones y direcciones) de la Game Boy con lógica Active Low, y se corrigió la inicialización de la paleta BGP a 0xE4 por defecto. Los logs de diagnóstico revelaron que el juego estaba haciendo polling del Joypad (P1) y que la paleta estaba en 0x00 (todo blanco), haciendo que los gráficos fueran invisibles aunque se renderizaran correctamente. Con estas correcciones, el emulador puede responder a las pulsaciones de botones y mostrar gráficos correctamente coloreados.
Concepto de Hardware
El Joypad de la Game Boy usa un sistema de Active Low donde:
- 0 = Botón pulsado
- 1 = Botón soltado
El registro P1 (0xFF00) es de lectura/escritura:
- ESCRITURA (bits 4-5): El juego selecciona qué leer:
- Bit 4 = 0: Quiere leer Direcciones (Right, Left, Up, Down)
- Bit 5 = 0: Quiere leer Botones (A, B, Select, Start)
- LECTURA (bits 0-3): El juego lee el estado según el selector:
- Bit 0: Right / A
- Bit 1: Left / B
- Bit 2: Up / Select
- Bit 3: Down / Start
Cuando un botón pasa de Soltado (1) a Pulsado (0), se activa la interrupción Joypad (Bit 4 en IF, 0xFF0F).
La paleta BGP (Background Palette, 0xFF47) controla los colores del fondo. En una Game Boy real, la Boot ROM deja este registro configurado a 0xE4 (11100100 en binario), que mapea:
- Índice 0 → Blanco (0)
- Índice 1 → Gris claro (1)
- Índice 2 → Gris oscuro (2)
- Índice 3 → Negro (3)
Si BGP queda en 0x00 (todo blanco), aunque el juego renderice correctamente los tiles, todos los píxeles aparecerán blancos y la pantalla se verá completamente blanca.
Fuente: Pan Docs - Joypad Input, Background Palette Register (BGP)
Implementación
Se creó una clase Joypad que implementa la lógica Active Low y maneja el registro P1.
Se integró en la MMU para interceptar lecturas/escrituras de P1, y en Viboy para capturar eventos
de teclado de pygame y actualizar el estado del Joypad.
Componentes creados/modificados
src/io/joypad.py: Nueva claseJoypadcon métodospress(),release(),read(),write(). Implementa lógica Active Low y solicita interrupciones cuando un botón se pulsa.src/io/__init__.py: Nuevo módulo de I/O.src/memory/mmu.py:- Inicialización de BGP a 0xE4 en
__init__(). - Interceptación de lectura/escritura de P1 (0xFF00) delegando al Joypad.
- Método
set_joypad()para conectar el Joypad a la MMU.
- Inicialización de BGP a 0xE4 en
src/viboy.py:- Instanciación del Joypad y conexión a la MMU.
- Método
_handle_pygame_events()que captura eventos de teclado y actualiza el Joypad. - Mapeo de teclas: K_UP/DOWN/LEFT/RIGHT (direcciones), K_z (A), K_x (B), K_RETURN (Start), K_RSHIFT (Select).
tests/test_io_joypad.py: Suite completa de tests (14 tests) validando inicialización, lógica Active Low, selector de lectura, interrupciones y integración con MMU.
Decisiones de diseño
Lógica Active Low: Se implementó correctamente donde True = pulsado (bit 0 en hardware), False = soltado (bit 1 en hardware). Al leer P1, si un botón está pulsado, se hace clear del bit correspondiente.
Detección de transiciones: La interrupción solo se solicita cuando un botón pasa de soltado
a pulsado. Si se llama press() varias veces sin release() intermedio, solo la primera
pulsación activa la interrupción.
Selector de lectura: Se guarda el selector (bits 4-5) cuando el juego escribe en P1. Al leer, se devuelve el estado de los botones correspondientes según el selector.
Manejo de eventos: Se centralizó el manejo de eventos de pygame en _handle_pygame_events()
dentro de Viboy, en lugar de hacerlo en el Renderer, para mantener la separación de responsabilidades.
Archivos Afectados
src/io/joypad.py- Nueva clase Joypadsrc/io/__init__.py- Nuevo módulo I/Osrc/memory/mmu.py- Inicialización BGP=0xE4, interceptación P1, método set_joypad()src/viboy.py- Instanciación Joypad, método _handle_pygame_events()tests/test_io_joypad.py- Suite completa de tests (14 tests)
Tests y Verificación
Se ejecutaron tests unitarios con pytest validando:
Tests ejecutados
- Comando:
python3 -m pytest tests/test_io_joypad.py -v - Entorno: macOS, Python 3.9.6
- Resultado: 14 tests PASSED en 0.33s
Tests implementados
test_default_palette_init: Verifica que BGP se inicializa a 0xE4.test_joypad_initial_state: Verifica que todos los botones están soltados al inicio.test_joypad_read_default: Verifica lectura con selector por defecto.test_joypad_read_directions: Verifica lectura de direcciones cuando están soltadas.test_joypad_read_directions_pressed: Verifica lectura cuando Right está pulsado (bit 0 = 0).test_joypad_read_buttons: Verifica lectura de botones cuando están soltados.test_joypad_read_buttons_pressed: Verifica lectura cuando A está pulsado (bit 0 = 0).test_joypad_press_interrupt: Verifica que pulsar un botón activa el bit 4 de IF.test_joypad_release_no_interrupt: Verifica que soltar un botón NO activa interrupción.test_joypad_press_twice_no_double_interrupt: Verifica que pulsar dos veces sin soltar solo activa interrupción la primera vez.test_joypad_press_release_press_interrupt: Verifica que pulsar-soltar-pulsar activa interrupción ambas veces.test_joypad_mmu_integration: Verifica integración con MMU (leer/escribir P1 funciona correctamente).test_joypad_all_directions: Verifica lectura de todas las direcciones cuando están pulsadas.test_joypad_all_buttons: Verifica lectura de todos los botones cuando están pulsados.
Código del test (ejemplo: detección de interrupción)
def test_joypad_press_interrupt(self) -> None:
"""Test: Pulsar un botón debe activar la interrupción Joypad (bit 4 en IF)"""
mmu = MMU(None)
joypad = Joypad(mmu)
# Limpiar IF
mmu.write_byte(IO_IF, 0x00)
assert (mmu.read_byte(IO_IF) & 0x10) == 0
# Pulsar Start
joypad.press("start")
# Verificar que el bit 4 de IF está activo
if_val = mmu.read_byte(IO_IF)
assert (if_val & 0x10) != 0
Qué valida: Este test demuestra que el hardware del Joypad solicita una interrupción cuando se pulsa un botón, activando el bit 4 del registro IF (Interrupt Flag). Esto permite que la CPU responda a eventos de entrada del usuario.
Fuentes Consultadas
- Pan Docs: Joypad Input
- Pan Docs: Background Palette Register (BGP)
- Pan Docs: CPU Interrupts (Joypad interrupt, bit 4)
Integridad Educativa
Lo que Entiendo Ahora
- Active Low es fundamental: La lógica inversa (0 = pulsado, 1 = soltado) es una característica del hardware real de la Game Boy, no una elección arbitraria. Esto permite que el hardware use pull-up resistors y detecte pulsaciones conectando a tierra.
- Selector de lectura: El hecho de que el juego tenga que escribir en P1 para seleccionar qué leer (direcciones vs botones) es una limitación del hardware que reduce el número de pines necesarios. El juego debe alternar entre leer direcciones y botones.
- Interrupciones por transición: La interrupción solo se activa en la transición de soltado a pulsado, no mientras el botón permanece pulsado. Esto evita spam de interrupciones y permite al juego detectar "clicks" discretos.
- Importancia de valores por defecto: Aunque no implementamos la Boot ROM todavía, los valores que deja la Boot ROM (como BGP=0xE4) son críticos. Sin estos valores, muchos juegos no funcionan correctamente porque asumen que la Boot ROM los configuró.
Lo que Falta Confirmar
- Timing de interrupciones: No está completamente claro si hay algún retardo entre pulsar un botón y que se active el bit de IF, o si es instantáneo. Por ahora, asumimos que es instantáneo.
- Comportamiento de bits 4-5 en lectura: En hardware real, cuando se lee P1, los bits 4-5 reflejan el selector (lo que escribió el juego), pero podría haber detalles de comportamiento que no están completamente documentados. Por ahora, mantenemos los bits 4-5 como están en el selector.
Hipótesis y Suposiciones
Se asume que la solicitud de interrupción es instantánea cuando se pulsa un botón (sin retardo). Se asume que si el juego no ha escrito un selector válido (ambos bits 4-5 = 1), la lectura devuelve todos los bits a 1 (todos los botones "soltados" desde la perspectiva del hardware).
Próximos Pasos
- [ ] Probar el emulador con un juego real para verificar que el Joypad funciona correctamente
- [ ] Implementar Scroll (SCX/SCY) para que el juego pueda desplazar el fondo
- [ ] Implementar Window (WX/WY) para que el juego pueda mostrar una ventana superpuesta
- [ ] Implementar Sprites (OAM) para que el juego pueda mostrar objetos móviles
- [ ] Implementar el Timer completo (TIMA, TMA, TAC) para sincronización precisa