gammpei's blog

RSS feed My GitHub

How to write a Game Boy emulator – Part 8: Blargg's CPU test ROMs #1,3,4,5,7,8,9,10,11

Posted on 2018-03-04

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

In the previous part, we emulated Blargg's CPU test ROM #6. In this part, we'll emulate test roms #1,3,4,5,7,8,9,10,11.

The goal is to get these outputs:

Test rom SHA-256 hash of the rom & screenshot
01-special.gb fe61349cbaee10cc384b50f356e541c90d1bc380185716706b5d8c465a03cf89
03-op sp,hl.gb ca553e606d9b9c86fbd318f1b916c6f0b9df0cf1774825d4361a3fdff2e5a136
04-op r,imm.gb 7686aa7a39ef3d2520ec1037371b5f94dc283fbbfd0f5051d1f64d987bdd6671
05-op rp.gb d504adfa0a4c4793436a154f14492f044d38b3c6db9efc44138f3c9ad138b775
07-jr,jp,call,ret,rst.gb ab31d3daaaa3a98bdbd9395b64f48c1bdaa889aba5b19dd5aaff4ec2a7d228a3
08-misc instrs.gb 974a71fe4c67f70f5cc6e98d4dc8c096057ff8a028b7bfa9f7a4330038cf8b7e
09-op r,r.gb b28e1be5cd95f22bd1ecacdd33c6f03e607d68870e31a47b15a0229033d5ba2a
10-bit ops.gb 7f5b8e488c6988b5aaba8c2a74529b7c180c55a58449d5ee89d606a07c53514a
11-op a,(hl).gb 0ec0cf9fda3f00becaefa476df6fb526c434abd9d4a4beac237c2c2692dac5d3

You'll have to implement missing CPU instructions. It's not the most fun work, but it's necessary work. There are a couple of gotchas along the way.

POP AF

The first time I ran test rom #8, I got this output:

The output is a bit cryptic, but it means opcode 0xF1 is wrong. 0xF1 corresponds to the instruction POP AF. As you know, F is the flag register. The bits 7,6,5,4 are the flags Z,N,H,C but what about the other bits? Turns out they always return 0. I fixed that and the test passed.

Differences with the Z80

The Game Boy CPU is similar but different than the Z80.

Opcodes 0xE8 and 0xF8 are Game Boy specific instructions. The way they set the carry flags is a bit weird: you have to use the lowest byte of the 16-bit value, and use the immediate signed 8-bit value as unsigned. Here's how I implemented the two instructions:

// pandocs.htm
// add  SP,dd     E8          16 00hc SP = SP +/- dd ;dd is 8bit signed number
// http://forums.nesdev.com/viewtopic.php?p=42143#p42143
var ADD_E8 = operation{"ADD", func(st *st, x rw_u16, y r_i8) {
	v1 := x.get(st)
	v2 := y.get(st)
	b1 := u8(v1)
	b2 := u8(v2)

	result := u16(int(v1) + int(v2))
	x.set(st, result)

	F.Z.set(st, false)
	F.N.set(st, false)
	F.H.set(st, halfCarry_u8(b1, b2, false))
	F.C.set(st, carry_u8(b1, b2, false))
}}

// pandocs.htm
// ld   HL,SP+dd  F8          12 00hc HL = SP +/- dd ;dd is 8bit signed number
// http://forums.nesdev.com/viewtopic.php?p=42143#p42143
var LD_F8 = operation{"LD", func(st *st, x w_u16, y SP_imm_i8) {
	v1 := y.v1().get(st)
	v2 := y.v2().get(st)
	b1 := u8(v1)
	b2 := u8(v2)

	result := u16(int(v1) + int(v2))
	x.set(st, result)

	F.Z.set(st, false)
	F.N.set(st, false)
	F.H.set(st, halfCarry_u8(b1, b2, false))
	F.C.set(st, carry_u8(b1, b2, false))
}}

These instructions have subtle differences in how they set their flags: CCF, RLA, RLCA, RRA, RRCA.

The DAA instruction also works differently. I had to lookup how to implement it:

// http://forums.nesdev.com/viewtopic.php?f=20&t=15944#p196282
var DAA = operation{"DAA", func(st *st) {
	a := A.get(st)
	if !F.N.get(st) {
		if F.C.get(st) || a > 0x99 {
			a += 0x60
			F.C.set(st, true)
		}
		if F.H.get(st) || a&0x0F > 0x09 {
			a += 0x06
		}
	} else {
		if F.C.get(st) {
			a -= 0x60
		}
		if F.H.get(st) {
			a -= 0x06
		}
	}

	A.set(st, a)

	F.Z.set(st, a == 0x00)
	F.H.set(st, false)
}}

Carries and borrows

When an instruction fails a test, it's usually the flag setting that's wrong. Here are the helper functions I use to set the carry and borrow flags:

func carry_u8(x, y u8, z bool) bool {
	result := int(x) + int(y) + int(u8FromBool(z))
	return result > 0xFF
}

func halfCarry_u8(x, y u8, z bool) bool {
	result := (x & 0x0F) + (y & 0x0F) + u8FromBool(z)
	return result > 0x0F
}

func carry_u16(x, y u16) bool {
	result := int(x) + int(y)
	return result > 0xFFFF
}

func halfCarry_u16(x, y u16) bool {
	result := (x & 0x0FFF) + (y & 0x0FFF)
	return result > 0x0FFF
}

func borrow(x, y u8, z bool) bool {
	result := int(x) - int(y) - int(u8FromBool(z))
	return result < 0
}

func halfBorrow(x, y u8, z bool) bool {
	result := int(x&0x0F) - int(y&0x0F) - int(u8FromBool(z))
	return result < 0
}

See the code/commit on GitHub.