How to write a Game Boy emulator - Part 4: The instructions of the bios
This is the part where all Game Boy emulator tutorials get handwavy. 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 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 and then um0080.pdf to implement it.
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 opcodes that differ between the two:
Moved, Removed, and Added Opcodes
Opcode Z80 GMB
---------------------------------------
08 EX AF,AF LD (nn),SP
10 DJNZ PC+dd STOP
22 LD (nn),HL LDI (HL),A
2A LD HL,(nn) LDI A,(HL)
32 LD (nn),A LDD (HL),A
3A LD A,(nn) LDD A,(HL)
D3 OUT (n),A -
D9 EXX RETI
DB IN A,(n) -
DD <IX> -
E0 RET PO LD (FF00+n),A
E2 JP PO,nn LD (FF00+C),A
E3 EX (SP),HL -
E4 CALL P0,nn -
E8 RET PE ADD SP,dd
EA JP PE,nn LD (nn),A
EB EX DE,HL -
EC CALL PE,nn -
ED <pref> -
F0 RET P LD A,(FF00+n)
F2 JP P,nn LD A,(FF00+C)
F4 CALL P,nn -
F8 RET M LD HL,SP+dd
FA JP M,nn LD A,(nn)
FC CALL M,nn -
FD <IY> -
CB3X SLL r/(HL) SWAP r/(HL)
Note: The unused (-) opcodes will lock-up the gameboy CPU when used.
The only Game Boy specific instructions in the bios are LDI and LDD. They notably don’t work the same as LDI and LDD in the Z80. Here’s how to implement them taken from the pandocs:
ldi (HL),A 22 8 ---- (HL)=A, HL=HL+1
ldi A,(HL) 2A 8 ---- A=(HL), HL=HL+1
ldd (HL),A 32 8 ---- (HL)=A, HL=HL-1
ldd A,(HL) 3A 8 ---- A=(HL), HL=HL-1
They’re equivalent to a LD followed by an increment/decrement of HL. No flags are affected.
I recommend you take advantage of the patterns in the opcodes to implement similar instructions at the same time. Personally I use a Go script to generate the case
statements in my opcode-decoding switch
. It’s easier and less error prone than copy-pasting the same code around and slightly modifying it.
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:
bool carry(u8 x, u8 y) {
return x + y > 0xFF;
}
bool half_carry(u8 x, u8 y) {
return (x & 0x0F) + (y & 0x0F) > 0x0F;
}
bool borrow(u8 x, u8 y) {
return x < y;
}
bool half_borrow(u8 x, u8 y) {
return (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:
- 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.
- 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.
- 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 equals 0x00. If it doesn’t, the bios enters an infinite loop. I return 0xFF for these addresses except for 0x014D where I return 0x00. This takes care of the problem.
My emulator is 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 turned on, gammaboy takes about 350ms to execute. With logging turned off, it takes about 3ms: that’s a hundredfold improvement!
$ time ./gammaboy roms/DMG_ROM.gb --log
[...]
Unimplemented memory write at 0xFF50.
real 0m0.354s
user 0m0.033s
sys 0m0.122s
$ time ./gammaboy roms/DMG_ROM.gb --log > /dev/null
Unimplemented memory write at 0xFF50.
real 0m0.030s
user 0m0.028s
sys 0m0.001s
$ time ./gammaboy roms/DMG_ROM.gb
Unimplemented memory write at 0xFF50.
real 0m0.003s
user 0m0.002s
sys 0m0.001s
If your emulator is still slow after turning logging off, you’ll have to make it faster because it can only get slower. Modern hardware should run circles around the Game Boy.
The code
You can compare your emulator’s output to mine. If your output differs, it doesn’t mean you’ve made a mistake. I have taken some shortcuts which I described earlier in this post. The biggest indicator that your emulator works is that it tries to write to memory location 0xFF50.
Here is a very useful disassembly of the bios: https://gist.github.com/drhelius/6063288.