Skip to content
RetrogamingDeep Dive Published Updated 7 min readViews unavailable

Game Boy MBC3 RTC: Latch Protocol, Clock Registers, and Day Overflow

Implement the Game Boy MBC3 real-time clock accurately: bank selection, snapshot latching, halt and carry bits, 512-day rollover, and regression tests.

The Game Boy MBC3 is a cartridge controller, not a clock peripheral built into the handheld. On cartridges fitted with the timer hardware, the MBC3 maps real-time clock (RTC) registers into the same CPU address window used for external cartridge RAM. A small bank-selection state determines which register is visible, a write sequence latches a stable snapshot, and the clock’s day counter includes both a halt control and an overflow flag.

These details matter in emulators because a clock that merely displays plausible seconds can still fail games that sample multiple registers, stop the RTC, cross a day boundary, or save state mid-session. The right model begins with the cartridge bus protocol. Host wall-clock policy comes afterward and should not replace the emulated register behavior.

First identify which cartridge behavior is present

Do not assume that every cartridge described as “MBC3” contains a working RTC. The cartridge header distinguishes timer, RAM, and battery configurations, and board variants can differ. A robust emulator should use the header and a maintained compatibility database together, report contradictions, and avoid silently attaching clock behavior to a RAM-only cartridge.

The MBC3’s ROM and external-data paths are selected with writes in different address ranges. The switchable ROM bank register is at $2000–$3FFF. Writes at $4000–$5FFF select either an external RAM bank ($00–$07) or an RTC register ($08–$0C) for the $A000–$BFFF window. A write of $0A in $0000–$1FFF enables access to external RAM and RTC registers; writing $00 disables access. Selecting an RTC register is not the same operation as latching a clock snapshot.

RTC selection Register Meaning Valid clock range
$08 RTC S Seconds 0–59
$09 RTC M Minutes 0–59
$0A RTC H Hours 0–23
$0B RTC DL Day counter bits 0–7 0–255
$0C RTC DH Day bit 8, halt, and carry flags Control/status bits

The access window is 8 KiB wide, but an RTC selection maps one register into it; it does not expose 8 KiB of clock data. A useful bus test writes a value to $A000, changes the RTC selector, then reads $A000 again and verifies that each selected register retains independent state. Test access-disabled behavior separately so a register implementation does not accidentally bypass the controller’s gate.

Latching creates a readable snapshot

The clock continues to advance while software reads registers. To avoid combining fields sampled at different instants, software writes $00 and then $01 to $6000–$7FFF. That transition copies the current clock values into latched registers. Reads from the selected RTC registers then return the latched snapshot until the latch sequence is performed again.

This gives the emulator two distinct concepts: the live clock state that advances, and the latched register image visible to the game. Treating a latch write as a no-op and returning live values at every read can produce an impossible timestamp. For example, a game could read seconds just before rollover and minutes just after it. Conversely, updating the latched values continuously defeats the point of the protocol.

write($6000, 0x00)
write($6000, 0x01)  // capture a stable RTC snapshot
select_rtc(0x08)
seconds = read($A000)
select_rtc(0x09)
minutes = read($A000)
select_rtc(0x0A)
hours = read($A000)

The sequence is shown as a protocol sketch; it is not a complete memory-mapped bus implementation. Keep controller writes, register selection, access enable, clock advancement, and snapshot capture as separate transitions. That separation makes it possible to test whether the game selected the right bank before reading and whether a new latch is needed to observe a changed time.

Pan Docs recommends allowing a short delay between separate RTC register accesses. Preserve the documented timing behavior in a cycle-aware core or diagnostic test; in a higher-level emulator, avoid inventing a delay mechanism unless its memory bus model needs one. The purpose of a compatibility rule is to reflect documented or measured hardware, not to add arbitrary waits to every access.

The 9-bit day counter and its control flags

RTC DH ($0C) combines three independent fields. Bit 0 is day-counter bit 8, extending the 8-bit lower day register to a 9-bit range of 0–511. Bit 6 is the halt flag: zero means the timer runs, while one stops it. Bit 7 is the day-counter carry flag, set when the 9-bit day value overflows. The carry flag remains set until software clears it; it is not a substitute for the ninth day bit.

At the 511-to-0 transition, the day counter wraps and carry records that overflow. This is a finite hardware representation, not an unbounded calendar date. A game that needs a longer duration can maintain its own offset in cartridge RAM and periodically consume or clear the hardware counter. An emulator should preserve the exact register-visible behavior rather than extending the MBC3’s day field behind the game’s back.

The halt bit also changes how time should advance. When set, seconds, minutes, hours, and days must remain frozen. When cleared, counting resumes according to the emulated RTC policy. Software that writes RTC fields is expected to halt the timer first; testing writes while the clock is running can otherwise produce races with normal clock advancement. Keep “halted” and “access disabled” separate: the first affects the clock, while the second gates reads and writes through the cartridge interface.

Model the clock separately from its bus view

A maintainable emulator can represent the MBC3 with explicit state rather than a single array that is updated directly on every read:

MBC3State:
    selected_rom_bank
    selected_ram_or_rtc_bank
    ram_rtc_access_enabled
    live_rtc = {seconds, minutes, hours, day_low, day_high, halt, carry}
    latched_rtc = {seconds, minutes, hours, day_low, day_high}
    latch_write_history

This is a software organization suggestion, not a claim about the physical chip’s internal circuit. The exact latch transition, field masks, bank selection, and persistence behavior are the compatibility contract. When the host application needs to advance the clock while the process is closed, store its chosen epoch metadata outside the emulated registers and reconcile elapsed time when loading. That host-time policy is application-specific; the emulated register values and latch state remain the game-visible MBC3 state.

The cartridge RTC uses its own oscillator and battery-backed behavior, so the Game Boy CPU clock is not the clock source. A deterministic movie or netplay session therefore needs a defined RTC seed and advancement policy rather than letting each peer consult its own local wall clock independently. Keep such policy in session metadata and test it as a reproducibility concern, not as a change to the MBC3 bus map.

Tests that catch plausible but incorrect clocks

Use register-level tests with a fake clock source. Start near 00:00:59, latch and read all fields, advance by one second, and verify that the old snapshot stays stable until a new $00-then-$01 latch. After relatching, check the seconds-to-minutes carry. Repeat at 23:59:59 and verify the day increment. Exercise the 511-day boundary, ensure carry sets on overflow, and confirm it stays set until software clears it.

Test the halt flag by advancing the clock for a known interval while the flag is set, then clearing it and checking that elapsed halted time was not added to the emulated counter. Test write access with RAM/RTC disabled and enabled, switch repeatedly between RAM banks and RTC register selectors, and confirm that a bank value in the $08–$0C range does not get treated as an ordinary RAM bank. Test seconds, minutes, and hours with values just below and above their valid ranges according to the documented hardware behavior rather than assuming a host-language modulo operation matches the chip.

Finally, take a save state after updating the live RTC but before latching it. Restore and verify both the live values and the prior latched view. Then save again after a latch and confirm that the snapshot is restored too. A state that preserves only the five displayed clock bytes may lose the distinction between ticking state and read snapshot; a state that stores only the host timestamp cannot reproduce an intentionally halted clock or an already-latched read.

The MBC3 RTC is a compact protocol with consequences across cartridge access, timekeeping, overflow, and persistence. Model its selector and latch as hardware-visible state, preserve the 9-bit day semantics and flags, and keep the host’s elapsed-time policy outside the register model. That approach yields deterministic tests and avoids “fixing” a Game Boy clock by replacing its controller behavior with a desktop timestamp.

Related:

Sources:

Comments