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:
- Source - the hardware peripheral must be configured to fire an interrupt (for example
reg_dispstat.enable_irq_vblank) reg_ie- the Interrupt Enable register must have the corresponding bit setreg_ime- the Interrupt Master Enable must betrue
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
| Field | Source |
|---|---|
.vblank | Vertical blank |
.hblank | Horizontal blank |
.vcounter | V-counter match |
.timer0 | Timer 0 overflow |
.timer1 | Timer 1 overflow |
.timer2 | Timer 2 overflow |
.timer3 | Timer 3 overflow |
.serial | Serial communication |
.dma0-.dma3 | DMA channel completion |
.keypad | Keypad interrupt |
.gamepak | Game Pak interrupt |
tonclib comparison
| stdgba | tonclib |
|---|---|
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); |