This guide assumes you have a short project to test code on. ISSOtm's "Hello World!" tutorial should work fine as a base.
Interrupts are used to call a given function when certain conditions are met. On the Game Boy, these conditions are:
- The beginning of the VBlank period
- A Joypad input
- A serial data transfer from the link cable
- The tick of a configurable timer
- A configurable LCD Status interrupt
For the purposes of this short guide, I will focus on the most prevalent interrupt - VBlank
VBlank refers to the period of time that the screen spends returning to the upper-left hand corner, and during this time the PPU gives the CPU access to VRAM. Because of this, most VRAM access during normal gameplay must occur during VBlank.
But how do you know when VBlank has started? If you have read ISSOtm's gb-asm-tutorial you likely know how to use a loop for this, which checks the value of rLY
to see if it is past the screen area. However, because this loop must continually run to check for VBlank, you may miss VBlank entirely or enter your VBlank code too late. Additionally, loops like that keep the CPU running, wasting power on real hardware. This isn't an issue for turning off the screen, but when you have time-sensitive code and a loop running game logic, it's important to make sure everything is run at predictable intervals.
This is where the VBlank interrupt comes in. When the VBlank period starts a flag will be set that tells the CPU to stop what it's doing, and call the address $0040
.
Before we even enable interrupts, let's write some VBlank code...
; Define a new section and hard-code it to be at $0040.
SECTION "VBlank Interrupt", ROM0[$0040]
VBlankInterrupt:
; This instruction is equivalent to `ret` and `ei`
reti
That handler doesn't do anything yet, but without it our VBlank interrupt would jump to $0040
and begin running random code, either the header or a portion of your ROM. Additionally, when an interrupt is fired it automatically runs di
, which disables interrupts so that nothing else conflicts with the handler. reti
is the same as an ei
followed by a ret
, so the interrupted code can continue executing and VBlank can be fired the next time it's needed.
Make sure you have a copy of 'hardware.inc', we're going to use it quite a bit here.
To enable the VBlank interrupt we need to write to the Interrupt Enable register, rIE
. If you take a look at rIE
on the Pandocs, you can see what interrupt each bit corresonds to, but we're going to focus on VBlank's bit, bit 0. To enable the VBlank interrupt, all we need to do is set bit 0 of rIE
to 1, so let's do that!
SECTION "Init", ROM0
Init:
; Place the following somewhere in your initiallization code
; hardware.inc defines a handy flag that we can use.
ld a, IEF_VBLANK
ldh [rIE], a
; ...
Additionally, we should clear another register while we're at it, rIF
. rIF
is used by the CPU to begin an interrupt; basically, if any of the bits in rIF
and rIE
match, the corresponding inturrupt is called. However, rIF
may have leftover values that would accidentally set off an interrupt at the wrong time, so we need to manually clear it.
xor a, a ; This is equivalent to `ld a, 0`!
ldh [rIF], a
Finally! Now the last step is running the instuction ei
, which globally enables interrupts. This is not the same as rIE
.
ei
If you run your rom now... you'll notice no difference. That's because our VBlank code doesn't do anything yet. But we can make it do something with just one instruction: halt
.
Your program likely has a loop somewhere which either does nothing, or continually runs some logic.
.endlessLoop
jr .endlessLoop
But this keeps the CPU running forever! Instead, we should give it a rest using the halt
instruction. Just place halt
at the end of that loop...
.endlessLoop
halt
jr .endlessLoop
... and run your program! If you're using BGB or Emulicious, you can check the Game Boy's CPU usage in their debuggers. Open it up and compare the meter with and without halt. You should see nearly 0% usage when halt is being used, because only three instructions are run per frame now.
Okay, saving power is great, but how does this help you write a Game Boy game? Well since the interrupt occurs as soon as VBlank starts, it gives us the perfect opportunity to access VRAM and graphics-related registers. We can start by playing with palettes. First, define a variable in HRAM; we'll use this as a frame counter.
SECTION "Frame Counter", HRAM
hFrameCounter:
db
Now, go back to that loop from earlier. Right before halt
, add some code to increment hFrameCounter
.
.endlessLoop
; Make sure to use `ldh` for HRAM and registers, and not a regular `ld`
ldh a, [hFrameCounter]
inc a
ldh [hFrameCounter], a
halt
jr .endlessLoop
Now, right before the loop halts, it'll increment a little timer which we can use for delays. We're going to use that timer to flicker the palettes back and forth in a short cycle during VBlank.
However, there is one issue to take care of: we only have 8 bytes of space in the VBlank interrupt handler! This is fine though, just jump outiside of the handler and continue execution. And while we're at it, we're going to push
every register, including the flags, to the stack. This is because there's a good chance VBlank will occur before the loop gets back to halt
in a real game, and we don't want to ruin any registers that the game was relying on.
SECTION "VBlank Interrupt", ROM0[$0040]
VBlankInterrupt:
push af
push bc
push de
push hl
jp VBlankHandler
SECTION "VBlank Handler", ROM0
VBlankHandler:
; Now we just have to `pop` those registers and return!
pop hl
pop de
pop bc
pop af
reti
Perfect! Now let's write some code. I'll heavily comment this to help you follow:
SECTION "VBlank Handler", ROM0
VBlankHandler:
; Begin by loading the frame counter
ldh a, [hFrameCounter]
; Now check the 5th bit, causing it to set the zero flag for 32 frames,
; every 32 frames. (about half a second on and off)
bit 5, a
; Now we're going to load a standard palette into `a`, but if the zero
; flag is set we'll complement it, inverting every color.
ld a, %11100100
jr z, .skipCpl
cpl ; ComPleMent `a`. Flips every bit in `a`
.skipCpl
; Finally, load `a` into `rBGP`, the Game Boy's Background Palette register.
ldh [rBGP], a
; Now we just have to `pop` those registers and return!
pop hl
pop de
pop bc
pop af
reti
Now you should see the colors invert every 32 frames. Cool!
If you're looking for something else to try on your own, try writing to a different tile each VBlank to slowly fill up the screen with a custom tile. Or load different graphics during VBlank to animate an existing tile!