gammpei's blog

RSS feed My GitHub

How to write a Game Boy emulator – Part 9: Interrupts

Posted on 2018-03-17

This post is part of a blog series about writing a Game Boy emulator.

I previously avoided talking about Blargg's CPU test ROM #2. I skipped it because it tests many non-trivial things. One of them is interrupts.

Blargg's CPU test ROM #2 (02-interrupts.gb) has this SHA-256 hash:

fb90b0d2b9501910c49709abda1d8e70f757dc12020ebf8409a7779bbfd12229

When I run it, I get this output:

It fails because I haven't implemented interrupts.

Interrupts

There are 5 sorts of interrupts in the Game Boy:

InterruptBit in IE and IFInterrupt handler address
V-Blank00x0040
LCD Status10x0048
Timer20x0050
Serial30x0058
Joypad40x0060

Interrupts disrupt the normal fetch-decode-execute cycle. After each such cycle, we need to check if there are interrupts to service. If it's the case, we have to call the appropriate interrupt handler.

We need to handle an interrupt if all these conditions are met:

When we have determined we need to handle an interrupt, we need to:

  1. Disable interrupts (set IME to false).
  2. Clear the corresponding bit in IF (acknowledge the interrupt).
  3. Push PC on the stack.
  4. Jump to the corresponding interrupt handler.

The V-Blank interrupt

The V-Blank interrupt is the most important interrupt to implement. In fact, I'm going to skip over the other interrupts in this post.

The V-Blank corresponds to the period where the scanline is between 144 and 153. When the scanline goes from 143 to 144, the V-Blank interrupt is automatically requested in IF (bit 0 is set to 1).

The code

You should add something like this in your main loop:

// V-Blank.
curScanline = getScanline(&st)
IF := st.readMem_u8(0xFF0F) // IF: Interrupt Flag
if prevScanline < 144 && curScanline >= 144 {
	// Request V-Blank interrupt.
	IF = setBit(IF, 0, true)
	st.writeMem_u8(0xFF0F, IF)
}

// Handle interrupts.
IE := st.readMem_u8(0xFFFF) // IE: Interrupt Enable
for i := uint(0); i <= 4; i++ {
	if getBit(IF, i) && getBit(IE, i) {
		if st.IME {
			// Disable interrupts.
			st.IME = false

			// Acknowledge interrupt.
			IF = setBit(IF, i, false)
			st.writeMem_u8(0xFF0F, IF)

			// Call interrupt handler.
			PUSH.f.(func(*state, r_u16))(&st, PC)
			interruptVector := [5]u16{0x0040, 0x0048, 0x0050, 0x0058, 0x0060}
			PC.set(&st, interruptVector[i])
		}
		break
	}
}

If you implemented everything correctly, you should get this output:

It still fails, but for another reason: we haven't implemented the timer.

See the code/commit on GitHub.