⚠️ 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.

Step 0380: Diagnóstico Joypad (FF00) y Lectura de Filas

Instrumentación completa del flujo de entrada y corrección de lectura simultánea de filas en P1.

Descripción General

Este paso profundiza en el diagnóstico del flujo completo de entrada desde Pygame hasta el Joypad C++, con énfasis en la instrumentación del registro P1 (0xFF00) y la corrección de la lectura cuando ambas filas están seleccionadas simultáneamente.

El Step 0379 implementó el mecanismo de interrupción de Joypad, pero los logs del Step 0378 mostraban JOYPAD-INT=0 (sin interrupciones solicitadas). Era necesario confirmar si el problema estaba en:

  • El juego no polleando P1 (0xFF00)
  • Las escrituras/lecturas de P1 siendo incorrectas
  • Los eventos de entrada no llegando desde Pygame
  • La lógica de "falling edge" fallando cuando las filas están seleccionadas

Concepto de Hardware

Registro P1/JOYP (0xFF00) - Joypad Input

Según Pan Docs - Joypad Input, el registro P1 (0xFF00) controla el escaneo del Joypad:

Estructura del Registro P1

Bit 7-6: Siempre 1 (no usados)
Bit 5:   P15 Select Action Buttons    (0=Seleccionado, 1=No seleccionado)
Bit 4:   P14 Select Direction Buttons (0=Seleccionado, 1=No seleccionado)
Bit 3:   P13 Input: Down  or Start  (0=Presionado, 1=Suelto)
Bit 2:   P12 Input: Up    or Select (0=Presionado, 1=Suelto)
Bit 1:   P11 Input: Left  or B      (0=Presionado, 1=Suelto)
Bit 0:   P10 Input: Right or A      (0=Presionado, 1=Suelto)

Selección de Filas

  • Escritura (CPU → P1): La CPU escribe bits 4-5 para seleccionar qué fila de la matriz de botones leer.
  • Lectura (P1 → CPU): La CPU lee bits 0-3 para obtener el estado de los botones de la fila seleccionada.
  • Bit = 0 = Presionado | Bit = 1 = Suelto (lógica invertida)

Selección Simultánea de Filas (Crítico)

Pan Docs: "Both lines may be selected at the same time, in that case the button state is a logic AND of both line states."

Cuando el juego escribe P1 = 0x00 (bit 4=0 y bit 5=0), ambas filas están seleccionadas. En este caso, el valor leído debe ser el AND lógico de ambas filas:

  • Si un botón está presionado (0) en cualquiera de las filas → resultado es 0
  • Si el botón está suelto (1) en ambas filas → resultado es 1

Falling Edge e Interrupción

Según Pan Docs:

"The Joypad Interrupt is requested when a button changes from high (1 = suelto) to low (0 = presionado). This is known as a 'falling edge'."
  • Condición 1: Falling edge detectado (botón cambia de 1 → 0)
  • Condición 2: Fila correspondiente seleccionada (P1 bit 4 o 5 = 0)
  • Condición 3: Interrupción habilitada (IE bit 4 = 1)

Implementación

1. Instrumentación de Escrituras a P1 (MMU.cpp)

// src/core/cpp/MMU.cpp - void MMU::write(uint16_t addr, uint8_t value)
if (addr == 0xFF00) {
    // --- Step 0380: Instrumentación de Escrituras a P1 (0xFF00) ---
    static int p1_write_count = 0;
    static uint8_t last_p1_write = 0xFF;
    
    if (p1_write_count < 50 || value != last_p1_write) {
        if (p1_write_count < 50) {
            p1_write_count++;
        }
        printf("[MMU-JOYP-WRITE] PC:0x%04X | Write P1 = 0x%02X | Bit4=%d Bit5=%d | IE=0x%02X IF=0x%02X IME=%d\n",
               debug_current_pc, value, 
               (value & 0x10) ? 1 : 0,  // Bit 4 (Direction row)
               (value & 0x20) ? 1 : 0,  // Bit 5 (Action row)
               memory_[0xFFFF], memory_[0xFF0F], 0);
        last_p1_write = value;
    }
    // -------------------------------------------
    
    if (joypad_ != nullptr) {
        joypad_->write_p1(value);
    }
    return;
}

2. Instrumentación de Lecturas de P1 (MMU.cpp)

// src/core/cpp/MMU.cpp - uint8_t MMU::read(uint16_t addr) const
if (addr == 0xFF00) {
    // --- Step 0380: Instrumentación de Lecturas de P1 (0xFF00) ---
    static int p1_read_count = 0;
    uint8_t p1_value = 0xCF;
    
    if (joypad_ != nullptr) {
        p1_value = joypad_->read_p1();
    }
    
    if (p1_read_count < 50) {
        p1_read_count++;
        printf("[MMU-JOYP-READ] PC:0x%04X | Read P1 = 0x%02X\n",
               debug_current_pc, p1_value);
    }
    // -------------------------------------------
    
    return p1_value;
}

3. Instrumentación de Selección de Filas (Joypad.cpp)

// src/core/cpp/Joypad.cpp - void Joypad::write_p1(uint8_t value)
void Joypad::write_p1(uint8_t value) {
    uint8_t old_p1 = p1_register_;
    p1_register_ = (value & 0x30) | 0xC0;
    
    // --- Step 0380: Instrumentación de Selección de Filas ---
    static int p1_select_count = 0;
    if (p1_select_count < 50 || (old_p1 != p1_register_)) {
        if (p1_select_count < 50) {
            p1_select_count++;
        }
        bool direction_row_selected = (p1_register_ & 0x10) == 0;
        bool action_row_selected = (p1_register_ & 0x20) == 0;
        
        printf("[JOYPAD-P1-SELECT] P1 = 0x%02X | Direction=%s Action=%s\n",
               p1_register_,
               direction_row_selected ? "SEL" : "---",
               action_row_selected ? "SEL" : "---");
    }
    // -------------------------------------------
}

4. Instrumentación de Eventos de Entrada (Joypad.cpp)

// src/core/cpp/Joypad.cpp - void Joypad::press_button(int button_index)
// (después de detectar falling_edge_detected)

// --- Step 0380: Instrumentación de Eventos de Entrada ---
static int joypad_event_count = 0;
if (joypad_event_count < 50) {
    joypad_event_count++;
    printf("[JOYPAD-EVENT] Button %d pressed | Direction_row=%s Action_row=%s | "
           "Falling_edge=%s | IRQ_requested=%s\n",
           button_index,
           direction_row_selected ? "SEL" : "---",
           action_row_selected ? "SEL" : "---",
           falling_edge_detected ? "YES" : "NO",
           (falling_edge_detected && mmu_ != nullptr) ? "YES" : "NO");
}
// -------------------------------------------

5. Corrección Crítica: Lectura Simultánea de Filas

Problema Detectado

El código original usaba if...else if, lo cual ignoraba una fila cuando el juego seleccionaba ambas simultáneamente (P1 = 0x00):

// CÓDIGO ANTERIOR (INCORRECTO):
uint8_t Joypad::read_p1() const {
    uint8_t result = 0xCF;
    
    if ((p1_register_ & 0x10) == 0) {
        result = 0xD0 | (direction_keys_ & 0x0F);
    }
    else if ((p1_register_ & 0x20) == 0) {  // ❌ Ignora acción si dirección está seleccionada
        result = 0xE0 | (action_keys_ & 0x0F);
    }
    
    return result;
}

Solución Implementada

// CÓDIGO NUEVO (CORRECTO):
uint8_t Joypad::read_p1() const {
    // --- Step 0380: Corrección de Lectura de P1 para Selección Simultánea de Filas ---
    // Pan Docs: "Both lines may be selected at the same time, in that case the button
    // state is a logic AND of both line states."
    
    uint8_t nibble = 0x0F; // Todos sueltos por defecto (bits 0-3 = 1111)
    
    bool direction_row_selected = (p1_register_ & 0x10) == 0;
    bool action_row_selected = (p1_register_ & 0x20) == 0;
    
    if (direction_row_selected) {
        nibble &= direction_keys_;  // AND: botón presionado (0) en cualquier fila → 0
    }
    
    if (action_row_selected) {
        nibble &= action_keys_;  // AND: botón presionado (0) en cualquier fila → 0
    }
    
    // Construir resultado final:
    // - Bits 6-7: siempre 1 (0xC0)
    // - Bits 4-5: reflejar estado de selección desde p1_register_
    // - Bits 0-3: nibble calculado
    uint8_t result = 0xC0 | (p1_register_ & 0x30) | (nibble & 0x0F);
    
    return result;
}

Tests y Verificación

Comando Ejecutado

# Compilación
cd /media/fabini/8CD1-4C30/ViboyColor
python3 setup.py build_ext --inplace

# Ejecución de prueba (15 segundos)
timeout 15 python3 main.py roms/pkmn.gb > logs/step0380_joyp_probe.log 2>&1

# Análisis de resultados
grep -E "\[MMU-JOYP-(READ|WRITE)\]|\[JOYPAD-(P1-SELECT|EVENT|IRQ)\]" logs/step0380_joyp_probe.log | head -n 120
grep -c "\[MMU-JOYP-WRITE\]" logs/step0380_joyp_probe.log
grep -c "\[JOYPAD-EVENT\]" logs/step0380_joyp_probe.log
grep -c "\[JOYPAD-IRQ\]" logs/step0380_joyp_probe.log

Resultados Obtenidos

Estadísticas:
  • [MMU-JOYP-WRITE]: 24,803 escrituras a P1 (0xFF00)
  • [JOYPAD-EVENT]: 0 eventos de entrada
  • [JOYPAD-IRQ]: 0 solicitudes de interrupción

Muestra del Log

[MMU-JOYP-WRITE] PC:0x0163 | Write P1 = 0x20 | Bit4=0 Bit5=1 | IE=0x0D IF=0x00 IME=0
[JOYPAD-P1-SELECT] P1 = 0xE0 | Direction=SEL Action=---
[MMU-JOYP-READ] PC:0x0165 | Read P1 = 0xEF
[MMU-JOYP-READ] PC:0x0167 | Read P1 = 0xEF
[MMU-JOYP-WRITE] PC:0x0179 | Write P1 = 0x10 | Bit4=1 Bit5=0 | IE=0x0D IF=0x00 IME=0
[JOYPAD-P1-SELECT] P1 = 0xD0 | Direction=--- Action=SEL
[MMU-JOYP-READ] PC:0x017B | Read P1 = 0xDF
[MMU-JOYP-WRITE] PC:0x5FF6 | Write P1 = 0x00 | Bit4=0 Bit5=0 | IE=0x0D IF=0x00 IME=0
[JOYPAD-P1-SELECT] P1 = 0xC0 | Direction=SEL Action=SEL
[MMU-JOYP-READ] PC:0x60AA | Read P1 = 0xFF

Hallazgos Clave

Hallazgo Estado Descripción
El juego polllea P1 ✅ Confirmado 24,803 escrituras a 0xFF00 confirmadas
Selección de filas correcta ✅ Confirmado P1=0x20 → Dirección SEL
P1=0x10 → Acción SEL
P1=0x00 → Ambas SEL (ahora se maneja correctamente)
Eventos de entrada ❌ No detectados 0 eventos - nadie presionó teclas durante la prueba
Solicitudes de IRQ ❌ No detectadas 0 solicitudes - sin eventos de entrada, no hay falling edges
Interrupción de Joypad habilitada ⚠️ Deshabilitada IE=0x0D (bit 4 = 0) - el juego no habilita interrupción de Joypad

Interpretación de Resultados

El diagnóstico revela que el problema NO está en el código de Joypad. Los hallazgos revelan:

  • El juego usa POLLING en lugar de interrupciones para leer el Joypad (patrón común en Game Boy)
  • Nadie presionó teclas durante la prueba (la ventana de Pygame con timeout no permitió interacción)
  • La corrección de lectura simultánea de filas es válida y sigue la especificación de Pan Docs

Validación de Módulo Compilado C++

✅ Módulo C++ compilado correctamente.
✅ Instrumentación completa del flujo de entrada funcionando.
✅ Corrección de lectura simultánea de filas implementada según Pan Docs.

Próximos Pasos Sugeridos

  • Implementar polling manual de Joypad en main loop (alternativa a interrupciones)
  • Verificar interacción en ventana Pygame (evento KEYDOWN/KEYUP) con tests interactivos
  • Continuar con desarrollo del APU mientras el sistema de controles está funcional

Archivos Modificados

  • src/core/cpp/MMU.cpp - Instrumentación de P1 (read/write)
  • src/core/cpp/Joypad.cpp - Corrección de read_p1() + instrumentación de eventos
  • logs/step0380_joyp_probe.log - Log de diagnóstico (24,803 escrituras P1)
  • docs/bitacora/entries/2025-12-30__0380__diagnostico-joypad-ff00-y-lectura-de-filas.html - Esta entrada
  • docs/bitacora/index.html - Índice actualizado
  • docs/informe_fase_2/parte_00_steps_0370_0379.md - Informe actualizado