Skip to content
RetrogamingDeep Dive Published Updated 7 min readViews unavailable

Game Boy Advance Timers: Prescalers, Cascade Overflow, and Reload State

Trace GBA timer reload registers, prescalers, count-up cascades, overflow IRQs, and audio links with an event-driven, testable clock model.

The Game Boy Advance provides four 16-bit timer channels, numbered 0 through 3. Each channel has a counter/reload register and a control register that selects a prescaler, enables count-up operation for timers 1-3, optionally requests an interrupt on overflow, and starts or stops the timer. These fields make a compact clock peripheral, but their update boundaries are easy to emulate incorrectly. In particular, a write to the low counter register programs a reload value; it does not necessarily replace the currently running counter immediately.

Timer behavior feeds much more than delays. Games use overflows for periodic work and frame timing. Timers 0 or 1 can provide the sample cadence used by Direct Sound FIFO playback. Timers 1, 2, and 3 can count overflows from a preceding timer instead of using a CPU-clock prescaler. That cascade forms a guest-visible chain whose phase, pending overflow, interrupt, and audio consequences must survive a save state.

Separate reload, live count, and phase

Treat the programmed reload value and the live counter as separate state. A timer start transition loads the reload value into the counter, and an overflow reloads it for the next interval. A write to the low half while a timer is already running changes the reload value for a future reload rather than acting like a direct live-count assignment. Reads expose the current counter behavior defined by the register, not a host-maintained deadline converted back to an approximate value.

Prescalers divide the system clock by a selected factor. Store the fractional progress toward the next increment as an integer phase accumulator. If the timer is advanced in chunks, retain the remainder instead of rounding each chunk into a number of increments. Otherwise, two calls advancing 100 cycles can produce a different result from one call advancing 200 cycles, even though the guest machine experienced the same time.

Starting, stopping, and changing control bits are edges with hardware semantics. A 0-to-1 start transition loads the counter and begins counting. A stop transition must freeze or otherwise update the current count exactly as documented. A write that changes prescaler or cascade selection while running can affect phase; do not reset the entire timer unless the hardware or tests establish that behavior. Log the old and new control word together with the timer phase.

Overflow is a sequence of events

When the 16-bit count advances beyond its terminal value, the timer reloads from its programmed reload value and creates an overflow event. If the IRQ enable bit is set, the overflow requests the timer’s interrupt. If the next timer is configured to count up, the preceding overflow is also the event that increments the next stage. Audio FIFO clocks can be tied to selected timers, so a single overflow may have more than one observable consumer.

Do not reduce overflow handling to counter = reload in a host loop. Keep an explicit event path: advance the source timer, detect overflow at the guest cycle, reload the counter, request the timer IRQ if enabled, notify the cascaded timer if configured, and signal any sound-clock consumer connected to that timer. Each side effect must be ordered consistently relative to CPU register reads and DMA service.

Timer 0 cannot count up from a preceding timer; the count-up option belongs to later channels. When a timer is cascaded, the prescaler selection no longer represents an independent periodic source in the same way. Keep the control decode visible so a later timer does not accidentally receive both CPU ticks and predecessor overflow ticks. Test each channel in standalone and cascaded modes.

Cascades preserve long intervals without host drift

With count-up enabled, timer 1 increments on timer 0 overflow; timer 2 increments on timer 1 overflow; timer 3 increments on timer 2 overflow. This makes a cascade useful for longer durations and sample accounting. A cascade must advance at the same event boundary as the previous timer’s reload and IRQ. If the emulator processes timer events by polling once per video frame, it can lose multiple overflows during a long frame, a stalled CPU, or a fast DMA/audio workload.

Represent each overflow as a discrete event with a monotonically increasing sequence number or guest timestamp. A downstream timer consumes exactly one tick for each source overflow, even if several events occur before the CPU next reads the registers. This preserves count and interrupt edges while allowing the host scheduler to coalesce unrelated work.

Tests should include a short reload value to force rapid cascades, then a large reload value near wrap. Record each timer’s counter and overflow count before and after the chain. Compare a timer advanced in small cycle steps with one advanced in a large step; results and event sequence must agree. Use a second test that disables or stops an upstream timer while a downstream cascade is active and verify that no new downstream ticks are invented.

Interrupts are latched outcomes, not callbacks

An overflow with its IRQ enable set requests the corresponding timer interrupt through the GBA interrupt controller. The timer device should assert the source; it should not invoke game code directly or assume the CPU will immediately run the handler. The interrupt controller’s master enable, per-source enable, and acknowledgement state remain separate. A disabled interrupt can still be a timer overflow that reloads and cascades.

This distinction is essential for accurate tests. Disable the CPU interrupt master while leaving the timer’s IRQ request enabled and verify that the timer continues to overflow, reload, and tick any downstream cascade. Then re-enable interrupts and confirm the pending source is visible according to interrupt-controller state. A single boolean called timer_irq_fired cannot represent timer enable, overflow occurrence, interrupt request, and CPU service.

Save states should include the live counter, reload value, prescaler phase, running state, cascade configuration, pending interrupt source, and any queued overflow event. Restore and advance one cycle at a time around the next expected overflow. The resumed chain must produce the same count and interrupt sequence as an uninterrupted run.

Direct Sound shares timer events, not timer state

The GBA’s two Direct Sound FIFO streams use timer-selected sample cadence. Timer overflow consumes data from a FIFO and may trigger DMA when the FIFO reaches the documented low-water condition. The timer is still a timer: sound FIFO contents, DMA request, channel selection, and mixer state belong to other devices. Connect them through an overflow event instead of having audio code read the timer’s counter or run its own duplicate clock.

A stream test should log the selected timer, each overflow cycle, the sample byte removed, FIFO occupancy, DMA request transition, DMA completion, and output sample. Repeat with the alternate timer selection and with two streams configured independently. A wrong frequency can come from reload arithmetic, prescaler phase, wrong timer selection, or a FIFO/DMA threshold, not only from the sound mixer.

Keep the source clock’s integer period. If the host audio rate is 48 kHz and the guest timer produces a nearby rational frequency, the resampler can interpolate the resulting guest samples. Do not alter the GBA timer period to match a convenient host rate; that changes the game’s observable machine time and can desynchronize sound from video and gameplay.

A testable overflow reducer

This reference helper makes the reload and cascade action visible for one already-detected overflow. It omits prescaler phase, hardware write timing, and interrupt-controller behavior; production code must schedule those parts in guest time.

def apply_overflow(reload_value, count_up, cascaded_count):
    if not 0 <= reload_value <= 0xFFFF:
        raise ValueError("reload must fit in 16 bits")
    if not 0 <= cascaded_count <= 0xFFFF:
        raise ValueError("cascade count must fit in 16 bits")
    next_count = (cascaded_count + 1) & 0xFFFF if count_up else cascaded_count
    return reload_value, next_count


assert apply_overflow(0xFFF0, True, 0x0007) == (0xFFF0, 0x0008)
assert apply_overflow(0x4000, False, 0x0007) == (0x4000, 0x0007)

Test the reducer alongside a cycle scheduler, not instead of one. Boundary fixtures should test overflow at the exact final source-clock edge, writing the reload register before and after that edge, toggling start, and cascading through two downstream stages.

Acceptance criteria

A dependable GBA timer model preserves reload and live-count state, prescaler phase, start/stop transitions, 16-bit overflow, IRQ requests, and count-up cascades. Timer-derived audio and downstream timers receive the same timestamped overflow without duplicating the clock. A trace can explain each counter value and interrupt from the preceding guest clock events.

Related:

Sources:

Comments