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:
For the purposes of this short guide we 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
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
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
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
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 is used by the CPU to begin an interrupt; basically, if any of
the bits in
rIE match, the corresponding inturrupt is
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
globally enables interrupts. This is not the same as
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:
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
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
.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.
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!