Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Interrupts

The GBA uses interrupts to notify the CPU about hardware events: VBlank, HBlank, timer overflow, DMA completion, serial communication, and keypad input.

For the raw register bitfields, see Interrupt Peripheral Reference.

Setting up interrupts

Before any BIOS wait function will work, you must install an IRQ handler. The normal stdgba path is the high-level dispatcher exposed as gba::irq_handler:

#include <gba/bios>
#include <gba/interrupt>
#include <gba/peripherals>

// Install the default dispatcher / empty stdgba IRQ stub
gba::irq_handler = {};

// Enable specific interrupt sources
gba::reg_dispstat = { .enable_irq_vblank = true };
gba::reg_ie = { .vblank = true };
gba::reg_ime = true;

// Now VBlankIntrWait() works
gba::VBlankIntrWait();

The three switches

Interrupts require three things to be enabled:

  1. Source - the hardware peripheral must be configured to fire an interrupt (for example reg_dispstat.enable_irq_vblank)
  2. reg_ie - the Interrupt Enable register must have the corresponding bit set
  3. reg_ime - the Interrupt Master Enable must be true

All three must be set for the interrupt to reach the handler.

High-level custom handlers

You can provide a callable (lambda, function pointer, etc.) to gba::irq_handler:

volatile int vblank_count = 0;

gba::irq_handler = [](gba::irq irq) {
    if (irq.vblank) {
        ++vblank_count;
    }
};

The handler receives a gba::irq bitfield with named boolean fields for each interrupt source. stdgba’s internal IRQ wrapper acknowledges REG_IF and the BIOS IRQ flag for you before calling the handler, so BIOS wait functions continue to work.

Multiple interrupt sources

Because the handler receives the full gba::irq bitfield, a single callable can dispatch to different logic based on which flags are set:

volatile int vblank_count = 0;
volatile int timer2_count = 0;

gba::irq_handler = [](gba::irq irq) {
    if (irq.vblank) ++vblank_count;
    if (irq.timer2) ++timer2_count;
};

gba::reg_dispstat = { .enable_irq_vblank = true };
gba::reg_ie       = { .vblank = true, .timer2 = true };
gba::reg_ime      = true;

Querying the current handler

// bool conversion -- true when a handler is installed
if (gba::irq_handler) { /* handler is set */ }

// has_value() is equivalent
if (gba::irq_handler.has_value()) { /* handler is set */ }

// Retrieve a const reference to the stored callable
const gba::handler<gba::irq>& h = gba::irq_handler.value();

Swapping handlers

swap exchanges the stored callable with a local gba::handler<gba::irq>, useful for temporarily replacing a handler and then restoring it:

gba::handler<gba::irq> my_handler = [](gba::irq irq) {
    if (irq.timer0) { /* ... */ }
};

// Swap in; old handler is now in my_handler
gba::irq_handler.swap(my_handler);

// ... do work ...

// Restore the original
gba::irq_handler.swap(my_handler);

Uninstalling the dispatcher

To uninstall the stdgba user handler and restore the built-in empty acknowledgement stub, use either of these:

gba::irq_handler = gba::nullisr;
// or
gba::irq_handler.reset();
// or
gba::irq_handler = {};

This removes the current callable, but still leaves a valid low-level IRQ stub installed so BIOS wait functions remain usable.

What a raw handler must do itself

If you install a low-level handler directly, you are responsible for the work normally done by stdgba’s internal wrapper:

  • acknowledge REG_IF
  • acknowledge the BIOS IRQ flag (0x03FFFFF8)
  • preserve the registers and CPU state your handler clobbers
  • restore any IRQ masking state you change
  • keep BIOS wait functions (VBlankIntrWait(), IntrWait()) working correctly

If you skip the acknowledgements, the interrupt may immediately retrigger or BIOS wait functions may stop working.

Uninstalling a low-level custom handler

If you want to remove a raw handler and go back to stdgba’s safe empty stub, use:

gba::irq_handler.reset();

If instead you want to return to the normal high-level dispatcher path, assign a callable again:

gba::irq_handler = [](gba::irq irq) {
    if (irq.vblank) {
        // ...
    }
};

Important note about irq_handler state queries

gba::irq_handler.has_value() reports whether the low-level vector currently points at something other than stdgba’s empty handler. That means it will also report true for a raw handler installed directly.

However, gba::irq_handler.value() only returns your callable when the vector points at stdgba’s own dispatcher wrapper. If you install a raw handler directly, value() behaves as if no user callable is installed.

Available interrupt sources

FieldSource
.vblankVertical blank
.hblankHorizontal blank
.vcounterV-counter match
.timer0Timer 0 overflow
.timer1Timer 1 overflow
.timer2Timer 2 overflow
.timer3Timer 3 overflow
.serialSerial communication
.dma0-.dma3DMA channel completion
.keypadKeypad interrupt
.gamepakGame Pak interrupt

tonclib comparison

stdgbatonclib
gba::irq_handler = {};irq_init(NULL);
gba::irq_handler = my_fn;irq_set(II_VBLANK, my_fn);
gba::irq_handler = gba::nullisr;(no direct equivalent)
gba::irq_handler.reset();(no direct equivalent)
gba::registral<void(*)()>{0x3007FFC} = my_raw_irq;direct IRQ vector write
gba::reg_ie = { .vblank = true };irq_enable(II_VBLANK);