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

LCDC Cheat and Timer Fix

Date:2025-12-18 StepID:0049 State: draft

Summary

A was implementeddiagnostic trapin the MMU to monitor all write attempts in the LCDC register (0xFF40), allowing you to identify if the game tries to turn on the screen but fails, or if It never gets to that part of the code. Additionally, a critical bug in the Timer overflow logic was corrected. (TIMA) which prevented the Timer interrupt from being generated correctly when TIMA went from 0xFF to 0x00. This fix is ​​vital because many games (including Pokémon) depend on the Timer to advance their initialization.

Hardware Concept

LCDC Register (LCD Control, 0xFF40)

The LCDC register is the main controller of the LCD screen. Bit 7 (0x80) controls whether the LCD is on (1) or off (0). When the LCD is off, the screen shows white/blue and the log LY (Line Y) freezes at 0. Many games turn off the LCD during initialization to load data into VRAM without interference, and then turn it on by writing a value with bit 7 active (typically 0x80, 0x91, etc.).

If a game turns off the LCD at startup and never tries to turn it back on, it indicates that the code got stuck in a loop waiting for a condition that is never met (for example, a Timer interruption or a change in some register).

TIMA Overflow Timer

The Timer Counter (TIMA, 0xFF05) is an 8-bit counter that increments according to the frequency configured in TAC. When TIMA overflows (goes from 0xFF to 0x00), the real hardware does the following:

  1. Detect overflow: TIMA is incremented from 0xFF to 0x00
  2. Recharge TIMA: TIMA is immediately reloaded with the value of TMA (Timer Modulo, 0xFF06)
  3. Request interruption: Bit 2 of the IF register (0xFF0F) is set to indicate a pending Timer interrupt

The Timer interrupt is critical for many games that use it to measure time, generate periodic events, or just advance the initialization code. If this interrupt is not generated correctly, the game may stay waiting in an infinite loop.

Fountain:Pan Docs - LCD Control Register, Timer and Divider Registers

Implementation

LCDC trap in MMU

Added a CRITICAL log in the methodwrite_byteof the MMU that fires every time the game attempts to write to the LCDC register (0xFF40). The log shows the previous value and the new value, allowing Track all LCD state changes:

# CRITICAL: Diagnostic Trap for LCDC (0xFF40)
# Monitors attempts to turn the screen on/off
if addr == IO_LCDC:
    old_value = self.read_byte(IO_LCDC)
    logging.critical(f"[TRAP LCDC] LCDC CHANGE ATTEMPT: {old_value:02X} -> {value:02X}")

This log will always be displayed in the console (CRITICAL level), regardless of the logging configuration, allowing you to diagnose if the game tries to turn on the screen or if it never gets to that part of the code.

Timer Overflow Fix

Fixed overflow detection logic in methodtickof the Timer. The previous implementation checked to see if TIMA was 0xFF before incrementing, but then reloaded directly without doing the increment that caused the overflow. The correct logic should be:

  1. Increase first: TIMA increases from 0xFF to 0x00 (overflow)
  2. Detect overflow: If after the TIMA increment it is 0x00, it means that there was overflow
  3. Reload and request interruption: TIMA is reloaded with TMA and interruption is requested

Corrected code:

# Increase TIMA (this may cause overflow from 0xFF to 0x00)
self._tima = (self._tima + 1) & 0xFF

# If TIMA overflowed (went from 0xFF to 0x00), reload and request interruption
if self._tima == 0x00:
    # OVERFLOW detected: Reload TIMA with TMA
    self._tima = self._tma & 0xFF
    # Request Interrupt Timer (IF Bit 2, 0xFF0F)
    self._request_timer_interrupt()

This fix is critical because without it, the Timer interrupt was never generated, preventing games from that depend on it advance in their initialization.

Affected Files

  • src/memory/mmu.py- Added diagnostic trap for LCDC writes (0xFF40)
  • src/io/timer.py- Corrected TIMA overflow detection logic to generate interrupts correctly
  • main.py- Tweaked logging settings to ensure CRITICAL messages are displayed

Tests and Verification

Timer Verification

The correctness of the Timer was validated by reviewing the implemented logic according to the hardware specification:

  • Overflow logic:The code now increments TIMA first and then detects if the result is 0x00, which is correct hardware behavior
  • TMA Recharge:When overflow is detected, TIMA is immediately reloaded with the TMA value
  • Interrupt request:The method_request_timer_interrupt()correctly activates bit 2 of the IF register (0xFF0F)

Diagnosis with pkmn.gb

ROM:pkmn.gb (user-contributed ROM, not distributed)

Execution mode:Graphical UI, with active LCDC trap

Success Criterion:The console logs should show if the game attempts to write to LCDC (change from 0x00 to a value with bit 7 active, such as 0x80 or 0x91)

Expected observation:

  • If we see[LCDC TRAP] LCDC CHANGE ATTEMPT: 00 -> 91: The game tries to turn on the screen, but there is a problem with our LCDC or PPU implementation
  • If we see[LCDC TRAP] LCDC CHANGE ATTEMPT: 00 -> 00and then nothing more: The game turns off the LCD but never tries to turn it back on, indicating that it is hanging waiting for an interrupt or condition
  • If we don't see any LCDC log: The game never tries to change the LCD state, possibly hanging before reaching that part of the code

Result: Pending verification- It is required to run the game and analyze the console logs to determine the diagnosis.

Legal notes:The pkmn.gb ROM is provided by the user for local testing. It is not distributed or linked in this project.

Sources consulted

Educational Integrity

What I Understand Now

  • Timer Overflow:TIMA overflow should be detected AFTER the increment, not before. The hardware increments first and then detects whether the result is 0x00, indicating that there was an overflow from 0xFF.
  • LCDC Diagnosis:Monitoring writes to critical hardware registers allows you to identify exactly where the game is lagging. If the game tries to turn on the LCD but fails, the problem is with our PPU/LCDC implementation. If you never try to turn it on, the problem is before (CPU, interruptions, timer).
  • Timer Interruptions:Many games critically depend on Timer interruptions to progress. If the Timer does not generate interrupts correctly, the game can stay in infinite loops waiting for events that never occur.

What remains to be confirmed

  • Exact behavior of the Timer in hardware:According to the documentation, writing to TIMA during the overflow cycle has special behavior. For now, we implement the basic logic. This may need adjustments if we encounter edge cases.
  • Diagnosis result:We need to run the game and analyze the logs to determine if the problem is:
    • The game never tries to turn on the LCD (CPU/interrupts/timer issue)
    • The game tries to turn on the LCD but fails (PPU/LCDC issue)
    • The game turns on the LCD but the screen is still blue (rendering/palette issue)

Hypotheses and Assumptions

Main hypothesis:The game is hanging waiting for a Timer interruption that never arrives due to the bug in the overflow logic that we just fixed. With this fix, we hope that the Timer generates interrupts correctly and the game can progress to the point of turning on the LCD.

Assumption about diagnosis:If after correcting the Timer the game still does not try to turn on the LCD, the problem may be:

  • Interrupts are not being processed correctly (IME, interrupt dispatcher)
  • The game is waiting for another condition (joypad, STAT, etc.)
  • There is another bug in the CPU that prevents the code from moving forward

Next Steps

  • [ ] Run pkmn.gb and analyze the console logs to see if the LCDC trap message appears
  • [ ] If the message appears with a change from 0x00 to a value with bit 7 active, verify that the PPU reacts correctly to the LCDC change
  • [ ] If no LCDC message appears, add more diagnostic traps (IF, IE, TIMA, TAC) to track interrupt and timer status
  • [ ] Verify that existing Timer tests still pass after the fix
  • [ ] If necessary, create specific tests to validate the generation of Timer interruptions in overflow