gammpei's blog

RSS feed My GitHub

How to write a Game Boy emulator – Part 4: The CPU instructions of the bios

Posted on 2017-07-01

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

This is the part where all Game Boy emulator tutorials get handwavy, and this one is no exception. We're going to add CPU instructions to our emulator until it can go through the bios, but it would be way too long for me to explain each and every instruction, so you'll have to refer to the code for the details.

I recommend you only implement the CPU instructions necessary to execute the bios. You run your emulator, you hit an unknown opcode, you implement it. You do this until the bios tries to write to memory location 0xFF50, which signals the end of it.

When you hit an unknown opcode, I suggest you look at GBZ80Opcodes.pdf to find the corresponding instruction. Then you look at um0080.pdf to find how to code that instruction.

Keep in mind that the latter documentation is for the Z80. While the Game Boy's CPU is very similar, it is not the same. Taken from the pandocs , here are the instructions that differ between the two:

I like to use GBZ80Opcodes.pdf because it cleary shows the patterns in the opcodes. I suggest you take advantage of these patterns to implement a bunch of opcodes at once. Personally I use some quick-and-dirty golang script to generate similar mappings at the same time. Ideally I would have used Rust macros, but I was losing too much time trying to understand how they worked.

Some notation

LD HL,0x0000 means load the value 0x0000 into HL.

On the other hand, LD (HL),0x00 means load the value 0x00 into memory location HL. The parentheses are important: they denote a memory location.

Integer overflow

Let's say A is equal to 0xFF. What happens if I increment its value? A is an unsigned 8-bit register, meaning that the maximum value it can hold is 0xFF, so what happens when you add 1 to it? Well, the value wraps around to 0x00.

Integer overflow is usually a bad thing. It's rare that you want your values to silently overflow in a normal program, but it is indeed what you want in your emulator. Everything should just overflow.

Carries and borrows

While reading about CPU operations, you'll encounter the concepts of carries, half-carries, borrows, and half-borrows. Here is how you can compute these values:

fn carry(x: u8, y: u8) -> bool {
	(x as i32) + (y as i32) > 0xFF
}

fn half_carry(x: u8, y: u8) -> bool {
	(x & 0x0F) + (y & 0x0F) > 0x0F
}

fn borrow(x: u8, y: u8) -> bool {
	x < y
}

fn half_borrow(x: u8, y: u8) -> bool {
	(x & 0x0F) < (y & 0x0F)
}

My emulator is stuck in an infinite loop!

There are 3 places where the bios can get stuck in an infinite loop:

  1. While reading memory locations 0x0104-0x0133. This address range is mapped to the cartridge. If there is no cartridge, these locations will return 0xFF. The problem is that the bios expects these locations to be strictly equal to the values at 0x00A8-0x00D7. So we're going to cheat and return the latter values when the former values are asked.
  2. While reading memory location 0xFF44. 0xFF44 is the LCDC Y-Coordinate, or LY. It cycles through the values 0 to 153 on a normal Game Boy. At one point, the bios will wait for that value to be exactly 144 before continuing. The easy fix is to always return 144. That's not what happens on a real Game Boy, but it's good enough for the moment.
  3. While reading memory locations 0x0134-0x014D. This address range is mapped to the cartridge. The bios expects that the sum of these values plus 0x19 be equal to 0x00. If it doesn't, the bios enters an infinite loop. So I just do this 0x0134...0x014D => if addr == 0x0134 { 0xE7 } else { 0x00 } which is good enough for the moment.

My emulator is too slow!

The bios takes about 5 seconds to execute on a real Game Boy. If your emulator takes more time than that, the first step is to disable all logging. Don't just turn off the printing, the problem is not necessarily I/O: string formatting in general can be really slow.

With logging on, gammaboy takes about 60s to execute. With logging off, it takes about 0.5s. Big difference.

If your emulator is still slow after turning logging off, I don't know what to tell you. You'll have to make it faster because we're not even drawing anything on the screen yet. Your emulator can only get slower, that's why it can't afford to be slow at this stage.

The code

You can compare your emulator's output to mine . If your trace differs, it doesn't necessarily mean that you've made a mistake. I have taken some shortcuts which I described earlier in this post, for example making memory location 0xFF44 always return 144. The biggest indicator that your emulator works is that the bios tries to write to memory location 0xFF50.

See the code/commit on GitHub.