This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Step 0409: RTC MBC3 + Wait-Loop Diagnostic + Header Verification
Summary
Minimum functional RTC (Real Time Clock) implementation for MBC3 usingstd::chrono,
with latch mechanism (0x00→0x01) and registers 0x08-0x0C. Creation of generic detector
wait-loop with automatic MMIO analysis (interrupts, LCD, RTC) to identify conditions
missing. Added explicit ROM header logging (title, MBC type, CGB flag) to resolve
identification discrepancies. Tests confirmed: RTC works on Oro.gbc (3 latches),
pkmn.gb is MBC3 (not MBC1), and Pokémon does not load tiles for reasons NOT related to RTC.
Hardware Concept
MBC3 (Memory Bank Controller 3)
The MBC3 is a memory controller chip for Game Boy cartridges that supports up to128 ROM banks (2MB)and4 RAM banks (32KB). Its characteristic Distinctive is the optional support ofRTC (Real Time Clock), an integrated clock which allows games like Pokémon Gold/Silver/Crystal to track real time between sessions.
MBC3 Cartridge Types(header 0x0147):
0x0F: MBC3 + Timer (RTC) + Battery0x10: MBC3 + Timer (RTC) + RAM + Battery0x11: MBC30x12:MBC3+RAM0x13: MBC3 + RAM + Battery
RTC (Real Time Clock)
The RTC consists of5 8-bit registersthat track seconds, minutes, hours and days. Accessed through the normal range of external RAM (0xA000-0xBFFF) when selected the corresponding RTC register by writing 0x08-0x0C to 0x4000-0x5FFF.
RTC records
- 0x08: Seconds (0-59)
- 0x09: Minutes (0-59)
- 0x0A: Hours (0-23)
- 0x0B: Day Counter (bits 0-7)
- 0x0C: Day Counter bit 8 + Flags:
- Bit 0: Day bit 8 (9-bit counter = 0-511 days)
- Bit 6:HALT(0=active, 1=frozen)
- Bit 7:DAY_CARRY(overflow after 511 days)
Latch mechanism
The RTC is continually updated in real time. To read consistent values (without changing between reads), games must "capture" a snapshot using thelatch mechanism:
- Write
0x00to 0x6000-0x7FFF - Write
0x01to 0x6000-0x7FFF - RTC registers "freeze" their values until the next latch
Fountain: Pan Docs - "MBC3", "Real Time Clock"
Wait-Loop Diagnosis
Await-loop(wait loop) occurs when the CPU repeatedly executes the same PC waiting for an external condition to change (interrupt flag, LCD register, RTC, etc.). Detecting these loops automatically and analyzing which MMIO is being consulted allows for diagnosis What hardware component is missing or incorrectly configured.
Detection strategy:
- Trace current PC every instruction
- Count consecutive repetitions of the same PC
- Threshold:5000 iterations→ loop confirmed
- When detected: capture IE/IF status, LCDC/STAT/LY, and activate MMIO trace
- Report what condition the game is waiting for
Implementation
Task 1: Logging Header & MBC Detected
Archive: src/core/cpp/MMU.cpp, functionload_rom()
Modification of the methodload_rom()to extract and display complete information from the
ROM header before configuring MBC:
- ROM Title(0x0134-0x0143): Extraction with ASCII sanitization (printable 0x20-0x7E characters only)
- Cart Type(0x0147): Hexadecimal code of the cartridge type
- CGB Flag(0x0143): 0x80=CGB compatible, 0xC0=CGB only, others=DMG
- ROM/RAM Size Codes(0x0148, 0x0149)
- MBC Type: Readable String (ROM_ONLY, MBC1, MBC2, MBC3, MBC5)
Output format:
[MBC] ========== ROM HEADER INFO ==========
[MBC] Title: "POKEMON_GLDAAUS."
[MBC] Cart Type: 0x10
[MBC] CGB Flag: 0x80 (CGB)
[MBC] ROM Size Code: 0x06
[MBC] RAM Size Code: 0x03
[MBC] Detected MBC: MBC3
[MBC] ROM Banks: 128 (2097152 bytes total)
[MBC] ========================================
Task 2: Implement Minimum RTC (MBC3)
Files: src/core/cpp/MMU.hpp, src/core/cpp/MMU.cpp
Changes to MMU.hpp
- Includes: Added
#include <chrono>for real time management - RTC fields(marked
mutablefor modification in const context):mutable uint8_t rtc_seconds_; // 0x08 mutable uint8_t rtc_minutes_; // 0x09 mutable uint8_t rtc_hours_; // 0x0A mutable uint8_t rtc_day_low_; // 0x0B mutable uint8_t rtc_day_high_; // 0x0C mutable std::chrono::steady_clock::time_point rtc_start_time_; uint8_t mbc3_latch_value_; // Last value written to 0x6000-0x7FFF - Helper methods:
void rtc_update() const;- Update records based on elapsed time (const because it is cache)void rtc_latch();- Snapshot capture after latch sequence 0x00→0x01
Implementation rtc_update() const
void MMU::rtc_update() const {
if (rtc_day_high_ & 0x40) return; // HALT active
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - rtc_start_time_);
int64_t total_seconds = elapsed.count();
rtc_seconds_ = (total_seconds % 60);
rtc_minutes_ = ((total_seconds / 60) % 60);
rtc_hours_ = ((total_seconds / 3600) % 24);
int days = (total_seconds / 86400);
if (days > 511) {
days = 511;
rtc_day_high_ |= 0x80; // Activate DAY_CARRY
}
rtc_day_low_ = (days & 0xFF);
rtc_day_high_ = (rtc_day_high_ & 0xFE) | ((days >> 8) & 0x01);
}
RTC Read/Write
Modifications inread(uint16_t addr)andwrite(uint16_t addr, uint8_t value)to handle accesses to 0xA000-0xBFFF whenmbc3_rtc_reg_is between 0x08-0x0C:
- Reading: Calls
rtc_update()before returning corresponding record - Writing: Set register, if HALT is activated (bit 6 of 0x0C) restart
rtc_start_time_
Latch Mechanism (0x6000-0x7FFF)
if (mbc3_latch_value_ == 0x00 && value == 0x01) {
rtc_latch();
printf("[RTC] Latch triggered: %02d:%02d:%02d Day=%d\n",
rtc_hours_, rtc_minutes_, rtc_seconds_,
rtc_day_low_ | ((rtc_day_high_ & 0x01)<< 8));
}
mbc3_latch_value_ = value;
Task 3: Generic Wait-Loop Diagnostics
Files: src/core/cpp/CPU.hpp, src/core/cpp/CPU.cpp
Improved Detector (CPU::step())
The existing detector (Step 0391) was enhanced with detailed MMIO analysis. When detecting loop (>5000 iterations of the same PC), automatic report is generated:
printf("[WAITLOOP] === Condition Analysis ===\n");
// Interrupt analysis
bool interrupts_pending = ime && (ie & if_reg);
if (interrupts_pending) {
if (ie & if_reg & 0x01) printf("[WAITLOOP] - VBlank pending\n");
if (ie & if_reg & 0x02) printf("[WAITLOOP] - LCD STAT pending\n");
if (ie & if_reg & 0x04) printf("[WAITLOOP] - Timer pending\n");
// etc.
}
// LCD analysis
printf("[WAITLOOP] LCD: LCDC=0x%02X, STAT=0x%02X, LY=%d\n", lcdc, stat, ly);
printf("[WAITLOOP] - LCD %s\n", (lcdc & 0x80) ? "ON" : "OFF");
printf("[WAITLOOP] - STAT Mode=%d\n", stat & 0x03);
// RTC warning
printf("[WAITLOOP] ⚠️ If this game uses MBC3+RTC, it might be waiting for RTC\n");
Affected Files
src/core/cpp/MMU.hpp- +18 lines: RTC fields, methods, include chronosrc/core/cpp/MMU.cpp- +80 lines: rtc_update, rtc_latch, read/write RTC, logging headersrc/core/cpp/CPU.hpp- +4 lines: wait-loop detector fieldssrc/core/cpp/CPU.cpp- +20 lines: MMIO analysis in wait-loopdocs/report_phase_2/part_00_steps_0370_0402.md- Added entry Step 0409docs/report_phase_2/index.md- Updated Part 0 range (370-409)
Tests and Verification
Compilation
$python3 setup.py build_ext --inplace
✅ Compilation successful (0 errors, minor warnings ignored)
Controlled Tests with Timeout
$ timeout 45s python3 main.py roms/Oro.gbc > logs/step0409_oro_rtc_waitloop.log 2>&1
$ timeout 45s python3 main.py roms/pkmn.gb > logs/step0409_pkmn_waitloop.log 2>&1
$ timeout 30s python3 main.py roms/tetris_dx.gbc > logs/step0409_tetris_dx_baseline.log 2>&1
Results
✅ Oro.gbc (Pokémon Gold - MBC3+RTC)
[MBC] Title: "POKEMON_GLDAAUS."
[MBC] Cart Type: 0x10
[MBC] Detected MBC: MBC3
[MBC] ROM Banks: 128 (2097152 bytes total)
[RTC] Latch triggered: 00:00:03 Day=0 (×3 latches detected)
[VRAM-REGIONS] Frame 1200 | tiledata_effective=0.0% | gameplay_state=NO
Conclusion: ✅ RTC works properly(latches detected, consistent values), butDoes NOT load TileData(non-RTC problem)
✅ pkmn.gb (Pokémon Red - MBC3 without RTC)
[MBC] Title: "POKEMON RED."
[MBC] Cart Type: 0x13
[MBC] Detected MBC: MBC3 (IT WAS MBC3, NOT MBC1!)
[MBC] ROM Banks: 64 (1048576 bytes total)
(No RTC activity - correct, Red has no RTC)
[VRAM-REGIONS] Frame 1200 | tiledata_effective=0.0% | gameplay_state=NO
Conclusion: ✅ MBC confirmed asMBC3(without RTC). Discrepancy resolved. It also doesn't load tools.
✅ tetris_dx.gbc (Baseline - MBC1)
[MBC] Title: "TETRIS DX."
[MBC] Cart Type: 0x03
[MBC] Detected MBC: MBC1
[WAITLOOP] Loop detected! PC:0x0283 Bank:1 repeated 5000 times
[WAITLOOP] Interrupts: NONE (IME=1, IE=0x09, IF=0x00)
[WAITLOOP] LCD: LCDC=0xC3, STAT=0x02, LY=107
[VRAM-REGIONS] Frame 1200 | tiledata_effective=56.6% | gameplay_state=YES
Conclusion: ✅ No regression, it works correctly. Wait-loop detected is normal polling (waiting for interruptions).
Native Validation
✅ All tests run compiled C++ modules (Cython extension validationviboy_core.so)
Sources consulted
- Bread Docs - MBC3: https://gbdev.io/pandocs/MBC3.html
- Pan Docs - Real Time Clock: Section within MBC3, latch mechanism and RTC registers
- Pan Docs - Cartridge Header: https://gbdev.io/pandocs/The_Cartridge_Header.html
- C++ Reference - std::chrono: https://en.cppreference.com/w/cpp/chrono
Educational Integrity
What I Understand Now
- RTC MBC3: The RTC is a separate component mapped through registers 0x08-0x0C. The latch mechanism (0x00→0x01) is essential for consistent readings because the clock is continually updated.
- Latch Sequence: The sequence of two writes (0x00, then 0x01) is a handshake deliberate to avoid accidental readings. The game must explicitly "ask for it."
- std::chrono: Use of
steady_clock(monotonic, not affected by system time changes) instead ofsystem_clockto measure elapsed time deterministically. - mutable in C++: Marked RTC fields
mutableallow modification from methodsconstbecause they represent real-time temporary cache (conceptually "read-only" from a logical perspective, but technically changing). - Wait-Loop Detection: Same PC iteration counter is more efficient than opcode analysis because it detects loops of any length (not just single-instruction loops like JR -2).
What remains to be confirmed
- RTC persistence: This implementation does NOT persist RTC state between sessions. Real RTC on MBC3 cartridge has battery to keep the clock running without power. Complete implementation would require saving shutdown timestamp and calculating offset on load.
- Pokémon Doesn't Load Tiles: Tests confirmed that Pokémon (Red and Gold) DO NOT load tiles to VRAM. RTC works correctly, so the problem is another: do they expect specific Boot ROM? HDMA misconfigured? Different init sequence?
- Wait-Loop in Tetris DX: Detector reported loop on PC:0x0283 waiting for interruptions They do NOT occur (IE=0x09, IF=0x00). However, Tetris DX works perfectly (gameplay_state=YES). This suggests that the loop is normal polling (waiting for VBlank) and the interrupt arrives eventually. Does not affect functionality.
Critical Findings
1. RTC MBC3 works properly: Pokémon Gold detects 3 latches with consistent values (00:00:03, Day=0). The Pokémon initialization problem is NOT RTC.
2. pkmn.gb is MBC3 (not MBC1): Header confirmed cart_type=0x13 (MBC3+RAM+Battery). The previous discrepancy was due to lack of explicit logging.
3. Pokémon (Red and Gold) DO NOT load TileData: Both games have tiledata_effective=0.0% after 1200 frames (~20 seconds). The problem is NOT RTC, MBC, or CGB palettes. Possible causes: Boot ROM expected, HDMA/DMA incorrectly configured, or specific hardware condition not met.
Next Steps
- [ ] Research Pokémon initialization: Compare Tetris DX init sequence (works) vs Pokémon (doesn't work). Implement changes in LCDC, BGP, IE, HDMA records.
- [ ] Instrument DMA/HDMA: Add detailed logging of DMA transfers to detect if Pokémon tries to load tiles via DMA and fails.
- [ ] Boot ROM Stub: Implement minimal Boot ROM stub to check if Pokémon waits for specific Boot ROM initialization before loading tiles.
- [ ] RTC persistence: Implement RTC state saving (shutdown timestamp) in .sav file to maintain clock between sessions.
- [ ] Tests with more MBC3+RTC ROMs: Test with Pokémon Crystal, Legend of Zelda: Oracle of Seasons/Ages to validate RTC implementation.