⚠️ Clean-Room / Educational

This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.

Step 0472: Boot State + STOP/KEY1 (CGB Speed ​​Switch)

Date:2026-01-04 StepID:0472 State: VERIFIED

Summary

Implementation of instrumentation to diagnose boot state problems (incorrect post-boot values) and CGB speed switch (STOP/KEY1 non-functional). Counters for KEY1 writes, JOYP writes, and STOP execution were added, the power-up defaults of critical I/O registers (BGP, OBP0, OBP1, LCDC, SCY, SCX, IE) were corrected, and the minimum STOP/KEY1 speed switch logic was implemented according to Pan Docs. Updated rom_smoke_0442.py with new metrics and created clean-room tests to validate the fixes.

Result:✅ All tests pass (5/5). ✅ Power-up defaults corrected according to Pan Docs. ✅ STOP/KEY1 speed switch functionally implemented. ✅ Added instrumentation for future diagnosis.

Hardware Concept

Power-Up Sequence (Post-Boot State)

Fountain:Pan Docs - "Power Up Sequence"

When the Game Boy boots without boot ROM (skip boot mode), the I/O registers must be initialized to specific values ​​that simulate the post-boot state. These values ​​are critical because games assume that the registers are already in the correct state when they begin execution.

Critical registers to avoid "white screen":

  • LCDC (0xFF40): 0x91 (LCD ON, BG ON, Window OFF)
  • SCY (0xFF42): 0x00 (Scroll Y)
  • SCX (0xFF43): 0x00 (Scroll X)
  • BGP (0xFF47): 0xFC (Standard BG Palette)
  • OBP0 (0xFF48): 0xFF (OBJ Palette 0)
  • OBP1 (0xFF49): 0xFF (OBJ Palette 1)
  • IE (0xFFFF): 0x00 (No interrupts initially enabled)

CGB Speed ​​Switch (STOP + KEY1)

Fountain:Pan Docs - "CGB Registers", "STOP instruction"

The Game Boy Color can switch between normal speed (4.19 MHz) and double speed (8.38 MHz) using the KEY1 register (0xFF4D) and the STOP instruction (opcode 0x10).

Register KEY1 (0xFF4D):

  • Bit 7: Current Speed ​​(0 = normal, 1 = double)
  • Bit 0: Prepare Speed ​​Switch (1 = ready to switch, 0 = not ready)
  • Bits 1-6: Not used

STOP instruction (0x10):

  • If KEY1 bit0 == 1 (ready) and CGB mode: Execute speed switch
  • Speed ​​switch: Toggle bit7 (speed), clear bit0 (preparation)
  • If KEY1 bit0 == 0 or DMG mode: normal STOP (enter stopped state)

JOYP (0xFF00): Joypad control register. It was instrumented to track writes that can affect STOP behavior (some games write to JOYP during speed switch).

Implementation

Phase A: Instrumentation (Gated)

Added static counters in MMU and CPU to track:

  • KEY1 writes: Counter, last value, last PC
  • JOYP writes: Counter, last value, last PC
  • STOP execution: Counter, last PC

All counters are exposed to Python via getters in the Cython wrappers (mmu.pyx, cpu.pyx).

Phase B: Fix Power-Up Defaults

The values ​​were verified and corrected inMMU::initialize_io_registers()according to Pan Docs:

  • LCDC = 0x91 (already correct)
  • SCY = 0x00, SCX = 0x00 (they were already correct)
  • BGP = 0xFC, OBP0 = 0xFF, OBP1 = 0xFF (they were already correct)
  • IE = 0x00 (already correct)

The values ​​were already correct, but a clean-room test was created to explicitly verify these post-boot values.

Phase C: Fix STOP + KEY1 Speed ​​Switch

The minimum speed switch logic was implemented inCPU::step()for the STOP opcode (0x10):

case 0x10://STOP
    // If CGB mode and KEY1 bit0 == 1 (prepared for speed switch)
    if (mmu_->get_hardware_mode() == HardwareMode::CGB && 
        (mmu_->read(0xFF4D) & 0x01) == 0x01) {
        // Toggle KEY1 bit7 (current speed)
        uint8_t key1 = mmu_->read(0xFF4D);
        key1 ^= 0x80;  // Toggle bit 7 (speed)
        key1 &= 0xFE;  // Clear bit 0 (prepare speed switch)
        mmu_->write(0xFF4D, key1);
        // In speed switch, the CPU does NOT stop
        cycles_ += 1;
        return 1;
    } else {
        // Normal STOP (enter stopped state until interrupt)
        //...existing implementation...
    }

Components created/modified

  • src/core/cpp/MMU.cpp: Counters KEY1/JOYP writes, getters
  • src/core/cpp/MMU.hpp: getter declarations
  • src/core/cpp/CPU.cpp: STOP/KEY1 speed switch logic, STOP counter
  • src/core/cpp/CPU.hpp: STOP getter statements
  • src/core/cython/mmu.pxd: Definitions Cython KEY1/JOYP
  • src/core/cython/mmu.pyx: Wrappers Python KEY1/JOYP
  • src/core/cython/cpu.pxd: Definitions Cython STOP
  • src/core/cython/cpu.pyx: Wrappers Python STOP
  • tools/rom_smoke_0442.py: New metrics in snapshot
  • tests/test_post_boot_io_defaults_0472.py: Test clean-room power-up defaults
  • tests/test_cgb_stop_speed_switch_0472.py: Test clean-room STOP/KEY1

Design decisions

  • Gated Instrumentation: KEY1/JOYP writes logs are gated byVIBOY_DEBUG_PPU=1to avoid context saturation
  • Static counters: Used static variables in MMU for KEY1/JOYP counters to maintain simplicity
  • Minimum speed switch: Only the essential logic (toggle bit7, clear bit0) was implemented. The actual change in CPU speed is left for future implementation.
  • Test mode: The tests useload_program()to write opcodes to WRAM (0xC000+) instead of ROM

Affected Files

  • src/core/cpp/MMU.cpp- KEY1/JOYP counters writes, getters
  • src/core/cpp/MMU.hpp- KEY1/JOYP getter statements
  • src/core/cpp/CPU.cpp- STOP/KEY1 speed switch logic, STOP counter
  • src/core/cpp/CPU.hpp- STOP getter statements
  • src/core/cython/mmu.pxd- Cython KEY1/JOYP Definitions
  • src/core/cython/mmu.pyx- Python KEY1/JOYP Wrappers
  • src/core/cython/cpu.pxd- Cython STOP Definitions
  • src/core/cython/cpu.pyx- Wrappers Python STOP
  • tools/rom_smoke_0442.py- New metrics (BGP, OBP0, OBP1, KEY1, JOYP, STOP)
  • tests/test_post_boot_io_defaults_0472.py- Test power-up defaults (new)
  • tests/test_cgb_stop_speed_switch_0472.py- Test STOP/KEY1 speed switch (new)

Tests and Verification

Validation through clean-room unit tests:

  • Test post-boot defaults (DMG): Check LCDC=0x91, BGP=0xFC, OBP0/OBP1=0xFF, IE=0x00
  • Test post-boot defaults (CGB): Check DMG values ​​+ KEY1=0x00, VBK=0xFE, SVBK=0x01
  • Test STOP speed switch: Verify that STOP executes speed switch when KEY1 bit0=1
  • Normal STOP test: Verifies that STOP behaves normally when KEY1 bit0=0
  • Test STOP DMG: Verify that STOP does not affect KEY1 in DMG mode

Test results:

============================== test session starts ==============================
collected 5 items

tests/test_post_boot_io_defaults_0472.py .. [40%]
tests/test_cgb_stop_speed_switch_0472.py ... [100%]

============================== 5 passed in 0.48s ==============================

Compiled C++ module validation:All tests useviboy_core(compiled C++ module) and validate real hardware behavior.

Sources consulted

Educational Integrity

What I Understand Now

  • Power-up critical defaults: Post-boot values ​​are essential. If BGP is at 0x00 instead of 0xFC, the palette does not display correctly and the game sees "white screen".
  • CGB speed switch: It is a two-phase mechanism: (1) Prepare by writing KEY1 bit0=1, (2) Execute STOP to make the change. The hardware toggles bit7 and clears bit0 automatically.
  • STOP instruction: It has two different behaviors: speed switch (CGB + KEY1 bit0=1) or stopped state (DMG or KEY1 bit0=0).
  • Gated Instrumentation: The logs must be gated to avoid context saturation, but the counters are always active for diagnostics.

What remains to be confirmed

  • actual speed change: The current implementation only toggles KEY1 bit7, but doesn't actually change the CPU speed. Changing CPU speed requires adjusting the internal clock.
  • Validation with real ROMs: The tests are clean-room, but the final validation requires running rom_smoke with real CGB ROMs to verify that the speed switch unlocks the games.

Hypotheses and Assumptions

Main hypothesis:Incorrect power-up defaults and non-functional STOP/KEY1 can be dominant causes of "white screen" in CGB games. The added instrumentation will allow you to diagnose if these fixes solve the problem.

Next Steps

  • [ ] Run rom_smoke with CGB ROMs (tetris_dx.gbc, mario.gbc) to validate that the fixes improve the behavior
  • [ ] Analyze rom_smoke snapshots to verify that KEY1 writes, STOP execution, and power-up defaults are correct
  • [ ] If the speed switch does not fully unlock, investigate implementation of actual CPU speed change
  • [ ] If the power-up defaults still do not resolve, investigate other critical values ​​(LY, STAT, etc.)