This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
The Heart of Time: Implementation of the Complete Timer (TIMA, TMA, TAC)
Summary
After the success of Step 0185, where the emulator successfully rendered the Nintendo logo, a critical diagnosis arose: although the emulator is stable and the registryL.Y.cycles correctly, the screen remains blank because the CPU never gets to the instruction that triggers the background rendering. The diagnostic pointed to the CPU being stuck in a time delay loop, waiting for a Timer interrupt that our C++ kernel couldn't yet generate.
This Step implements the complete Programmable Timer subsystem (TIMA, TMA, CT) in C++, completing the functionality that only the registry hadDIV. With this implementation, the emulator can execute precise timing routines, allowing games to complete their boot sequences and eventually give the command to draw the title screen.
LCDCis 0 and obediently refuses to draw the background layer. The problem is not our emulator; is that the game never gets to the instruction that changesLCDCof0x80to0x91because it is waiting for a Timer interruption that we could not generate yet.Hardware Concept: The Programmable Timer
In addition to registrationDIV(Divider, 0xFF04), the Game Boy has a game-configurable timer, controlled by 3 registers:
-
CT(Timer Control - 0xFF07):- Bit 2:Timer Enable (1 = ON, 0 = OFF).
- Bits 1-0:Input Clock Select. Select the frequency at which it increases
TIMA:00: 4096 Hz (1024 T-Cycles per increment)01: 262144 Hz (16 T-Cycles per increment)10: 65536 Hz (64 T-Cycles per increment)11: 16384 Hz (256 T-Cycles per increment)
-
TIMA(Timer Counter - 0xFF05):It is an 8-bit counter that increments at the frequency selected inCT. WhenTIMAoverflows (goes from0xFFto0x00), is automatically reloaded with the value ofTMAand a request is madeTimer interrupt (Bit 2 of the registerI.F.). -
TMA(Timer Module - 0xFF06):WhenTIMAoverflows, it is automatically reloaded with the value ofTMA.
The BIOS and games use this system to create precise delays. If not implemented, any game that sets the Timer and then usesHALTto wait for the interruption will be frozen forever.
Fountain:Pan Docs - Timer and Divider Register, Timer Control, Timer Counter, Timer Module.
Implementation
1. Expansion of the Timer Class in C++
Class expandedTimerexisting to include the recordsTIMA, TMAandCT, along with overflow and interrupt logic.
Modifications inTimer.hpp:
- Added pointer to
MMUto request interruptions whenTIMAoverflows - Added private members:
tima_counter_,tima_,tma_,tac_ - Added public methods:
read_tima(),write_tima(),read_tma(),write_tma(),read_tac(),write_tac() - Added private method:
get_tima_threshold()to calculate the number of T-Cycles based on the configured frequency - Modified the constructor to accept a pointer to
MMU
Modifications inTimer.cpp:
- Updated the method
step()to handle the increase inTIMAdepending on the configuration ofCT - Implemented overflow logic: when
TIMAgoes from0xFFto0x00, it is recharged withTMAand an interruption is requested bymmu_->request_interrupt(2) - Implemented
get_tima_threshold()which returns the number of T-Cycles needed according to bits 1-0 ofCT
2. Integration into the MMU
It was updatedMMU.cppto handle reads and writes of Timer registers:
0xFF05(TIMA): Read/write viaTimer::read_time()/Timer::write_time()0xFF06(TMA): Read/write viaTimer::read_tma()/Timer::write_tma()0xFF07(CT): Read/write viaTimer::read_tac()/Timer::write_tac()
3. Wrappers Cython Update
Updated Cython wrappers to expose the new functionality:
timer.pxd: Added declarations for new methods and modified the constructor to acceptMMU*timer.pyx: Implemented Python methods for reading/writingTIMA,TMAandCT, and modified the constructor to accept aPyMMUoptionalmmu.pyx: Added methodget_cpp_ptr()to maintain consistency with other wrappers
4. Main System Update
It was updatedviboy.pyto pass the instance ofMMUto the builderPyTimer, allowing the Timer to request interruptions correctly.
Design Decisions
Why does the Timer need a pointer to MMU?WhenTIMAoverflows, the hardware requests a Timer interrupt by writing to bit 2 of the registerI.F.(0xFF0F). Since the MMU owns the memory and manages the registerI.F., the Timer needs a pointer to the MMU in order to request interrupts.
Handling multiple increments:The methodstep()correctly handles cases where an instruction consumes many T-Cycles, using a loopwhileto process multiple increments ofTIMAif required.
Affected Files
src/core/cpp/Timer.hpp- Expansion to include TIMA, TMA, TACsrc/core/cpp/Timer.cpp- Implementation of overflow and interrupt logicsrc/core/cpp/MMU.cpp- Handling reads/writes of 0xFF05, 0xFF06, 0xFF07src/core/cython/timer.pxd- Updated Cython declarationssrc/core/cython/timer.pyx- Wrapper Python updated with new methodssrc/core/cython/mmu.pyx- Added get_cpp_ptr() methodsrc/viboy.py- Updated to pass MMU to PyTimer constructortests/test_core_timer.py- Complete tests for TIMA, TMA, TAC
Tests and Verification
Command executed:
python -m pytest tests/test_core_timer.py -v
Result: 16 tests passedin 0.07s
The tests validate:
- ✅ Reading/writing
TIMA,TMAandCT - ✅ Increase in
TIMAwhen the Timer is activated - ✅
TIMANO increments when Timer is disabled - ✅
TIMAincreases at the correct frequencies (4096Hz, 262144Hz, 65536Hz, 16384Hz) - ✅ When
TIMAoverflows, is reloaded with the value ofTMA - ✅ When
TIMAoverflows, a Timer interrupt is requested (IF bit 2) - ✅ Correct handling of multiple increments in a single
step()
Key Test Code:
def test_tima_overflow_requests_interrupt(self):
"""Verify that when TIMA overflows, a Timer interrupt is requested."""
mmu = PyMMU()
timer = PyTimer(mmu)
mmu.set_timer(timer) # Connect Timer to MMU for interrupts
# Set TIMA near overflow
timer.write_time(0xFF)
timer.write_tac(0x04) # Timer ON, frequency 4096 Hz
# Advance 1024 T-Cycles (should cause overflow)
timer.step(1024)
# Verify that a Timer interrupt was requested (bit 2 of the IF register)
if_reg = mmu.read(0xFF0F)
assert (if_reg & 0x04) != 0, "A Timer interrupt (IF bit 2) should have been requested"
Native Validation:Compiled C++ module validation with exhaustive unit tests.
Sources consulted
- Pan Docs: Timer and Divider Register (0xFF04)
- Pan Docs: Timer Counter (TIMA - 0xFF05)
- Pan Docs: Timer Module (TMA - 0xFF06)
- Pan Docs: Timer Control (TAC - 0xFF07)
- Pan Docs: Interrupts, Interrupt Flag Register (IF - 0xFF0F)
Educational Integrity
What I Understand Now
- The programmable timer:In addition to the DIV (which is a continuous counter), the Game Boy has a programmable Timer that allows games to create precise delays and generate timing interruptions.
- Timer frequencies:The Timer can operate at 4 different frequencies (4096Hz, 262144Hz, 65536Hz, 16384Hz), selected using TAC bits 1-0.
- Overflow and recharge:When TIMA overflows (0xFF → 0x00), it is automatically reloaded with the TMA value and a Timer interrupt is requested.
- The importance for the boot sequence:The BIOS and games use the Timer to create precise delays during initialization. Without this functionality, games would freeze waiting for interruptions that never arrive.
What remains to be confirmed
- Verification with real ROMs:The next step will be to run the emulator with a real ROM to verify that the Full Timer allows the games to complete their timing routines and finally draw the title screen.
- Synchronization with HALT:Verify that when the CPU executes HALT and the Timer generates an interrupt, the CPU wakes up correctly and processes the interrupt.
Hypotheses and Assumptions
There are no critical assumptions. The implementation is based directly on Pan Docs, which clearly specifies the behavior of each record and the relationship between TIMA, TMA and TAC.
Bottom line
After this implementation, the emulator now has:
- ✅ Full functional timer:All Timer registers (DIV, TIMA, TMA, TAC) are implemented and working correctly
- ✅ Timer Interruptions:When TIMA overflows, a Timer interrupt (IF bit 2) is successfully requested.
- ✅ Precise frequencies:Timer increments at correct frequencies based on TAC settings
- ✅ Comprehensive tests:16 unit tests validate all Timer functionality
Next Steps
- [ ] Run the emulator with a real ROM to verify that the Full Timer allows games to complete their timing routines
- [ ] Verify that Timer interrupts correctly wake up the CPU when it is in HALT state
- [ ] If everything works correctly, we should finally see the Nintendo logo on the screen