Compare commits

...

13 Commits

Author SHA1 Message Date
Michael Smith
b860999dc8 Implement timer, some IO operations and more CPU instructions 2025-09-05 11:15:41 +02:00
Michael Smith
7678fda9e7 Add more instructions. Fix JR bug. Start implementation of IO R/W 2025-09-03 09:11:16 +02:00
Michael Smith
7c494acc7e Implement more CPU instructions 2025-08-30 13:19:07 +02:00
Michael Smith
772ff893af Implement CB instructions 2025-08-29 17:42:30 +02:00
Michael Smith
c18615c629 Implement more CPU instructions 2025-08-29 17:04:46 +02:00
Michael Smith
82d3898216 Add some RAM and a stack 2025-08-29 17:04:30 +02:00
Michael Smith
1bae615b39 Update README 2025-08-29 13:42:03 +02:00
Michael Smith
6e3149d093 More work on CPU 2025-08-21 19:53:41 +02:00
Michael Smith
b72667947f WIP: CPU 2025-08-13 17:54:55 +02:00
Michael Smith
3cb7e3a5c9 Make escape key quit application 2025-08-13 16:03:10 +02:00
Michael Smith
c3d17459c6 Calculate and verify ROM checksum 2025-08-13 16:03:10 +02:00
Michael Smith
ba4e098ba5 Update test suite to use example roms 2025-08-13 13:48:12 +02:00
Michael Smith
5387152a21 Animating a nice backbuffer. Thank you Casey Muratori of Handmade Hero fame (https://www.youtube.com/watch?v=hNKU8Jiza2g) 2025-08-07 17:56:59 +02:00
33 changed files with 1545 additions and 455 deletions

View File

@@ -1,3 +1,5 @@
# gb-player
Fun with Go and GameBoy cartridges
Learning the Go Programming Language by making a Game Boy emulator.
https://hacktheplanet.be/2025/08/29/game-boy-emulator-in-go/

View File

@@ -1,301 +0,0 @@
package main
import (
"bytes"
"encoding/binary"
"io"
"log"
"os"
)
// See https://gbdev.io/pandocs/The_Cartridge_Header.html#0104-0133--nintendo-logo
var expectedLogo = [48]byte{
0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B,
0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D,
0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E,
0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99,
0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC,
0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E,
}
// Cartridge types
// See https://gbdev.io/pandocs/The_Cartridge_Header.html#0147--cartridge-type
var cartridgeTypes = map[byte]string{
0x00: "ROM ONLY",
0x01: "MBC1",
0x02: "MBC1+RAM",
0x03: "MBC1+RAM+BATTERY",
0x05: "MBC2",
0x06: "MBC2+BATTERY",
0x08: "ROM+RAM 9",
0x09: "ROM+RAM+BATTERY 9",
0x0B: "MMM01",
0x0C: "MMM01+RAM",
0x0D: "MMM01+RAM+BATTERY",
0x0F: "MBC3+TIMER+BATTERY",
0x10: "MBC3+TIMER+RAM+BATTERY 10",
0x11: "MBC3",
0x12: "MBC3+RAM 10",
0x13: "MBC3+RAM+BATTERY 10",
0x19: "MBC5",
0x1A: "MBC5+RAM",
0x1B: "MBC5+RAM+BATTERY",
0x1C: "MBC5+RUMBLE",
0x1D: "MBC5+RUMBLE+RAM",
0x1E: "MBC5+RUMBLE+RAM+BATTERY",
0x20: "MBC6",
0x22: "MBC7+SENSOR+RUMBLE+RAM+BATTERY",
0xFC: "POCKET CAMERA",
0xFD: "BANDAI TAMA5",
0xFE: "HuC3",
0xFF: "HuC1+RAM+BATTERY",
}
// RAM sizes
// https://gbdev.io/pandocs/The_Cartridge_Header.html#0149--ram-size
var ramSizes = map[byte]string{
0x00: "0 - No RAM",
0x01: "UNUSED VALUE",
0x02: "8 KiB - 1 bank",
0x03: "32 KiB - 4 banks of 8 KiB each",
0x04: "128 KiB 16 banks of 8 KiB each",
0x05: "64 KiB 8 banks of 8 KiB each",
}
// Old licensees
// https://gbdev.io/pandocs/The_Cartridge_Header.html#014b--old-licensee-code
var oldLicensees = map[byte]string{
0x00: "None",
0x01: "Nintendo",
0x08: "Capcom",
0x09: "HOT-B",
0x0A: "Jaleco",
0x0B: "Coconuts Japan",
0x0C: "Elite Systems",
0x13: "EA (Electronic Arts)",
0x18: "Hudson Soft",
0x19: "ITC Entertainment",
0x1A: "Yanoman",
0x1D: "Japan Clary",
0x1F: "Virgin Games Ltd.3",
0x24: "PCM Complete",
0x25: "San-X",
0x28: "Kemco",
0x29: "SETA Corporation",
0x30: "Infogrames5",
0x31: "Nintendo",
0x32: "Bandai",
0x33: "Indicates that the New licensee code should be used instead.",
0x34: "Konami",
0x35: "HectorSoft",
0x38: "Capcom",
0x39: "Banpresto",
0x3C: "Entertainment Interactive (stub)",
0x3E: "Gremlin",
0x41: "Ubi Soft1",
0x42: "Atlus",
0x44: "Malibu Interactive",
0x46: "Angel",
0x47: "Spectrum HoloByte",
0x49: "Irem",
0x4A: "Virgin Games Ltd.3",
0x4D: "Malibu Interactive",
0x4F: "U.S. Gold",
0x50: "Absolute",
0x51: "Acclaim Entertainment",
0x52: "Activision",
0x53: "Sammy USA Corporation",
0x54: "GameTek",
0x55: "Park Place13",
0x56: "LJN",
0x57: "Matchbox",
0x59: "Milton Bradley Company",
0x5A: "Mindscape",
0x5B: "Romstar",
0x5C: "Naxat Soft14",
0x5D: "Tradewest",
0x60: "Titus Interactive",
0x61: "Virgin Games Ltd.3",
0x67: "Ocean Software",
0x69: "EA (Electronic Arts)",
0x6E: "Elite Systems",
0x6F: "Electro Brain",
0x70: "Infogrames5",
0x71: "Interplay Entertainment",
0x72: "Broderbund",
0x73: "Sculptured Software6",
0x75: "The Sales Curve Limited7",
0x78: "THQ",
0x79: "Accolade15",
0x7A: "Triffix Entertainment",
0x7C: "MicroProse",
0x7F: "Kemco",
0x80: "Misawa Entertainment",
0x83: "LOZC G.",
0x86: "Tokuma Shoten",
0x8B: "Bullet-Proof Software2",
0x8C: "Vic Tokai Corp.16",
0x8E: "Ape Inc.17",
0x8F: "I'Max18",
0x91: "Chunsoft Co.8",
0x92: "Video System",
0x93: "Tsubaraya Productions",
0x95: "Varie",
0x96: "Yonezawa19/S'Pal",
0x97: "Kemco",
0x99: "Arc",
0x9A: "Nihon Bussan",
0x9B: "Tecmo",
0x9C: "Imagineer",
0x9D: "Banpresto",
0x9F: "Nova",
0xA1: "Hori Electric",
0xA2: "Bandai",
0xA4: "Konami",
0xA6: "Kawada",
0xA7: "Takara",
0xA9: "Technos Japan",
0xAA: "Broderbund",
0xAC: "Toei Animation",
0xAD: "Toho",
0xAF: "Namco",
0xB0: "Acclaim Entertainment",
0xB1: "ASCII Corporation or Nexsoft",
0xB2: "Bandai",
0xB4: "Square Enix",
0xB6: "HAL Laboratory",
0xB7: "SNK",
0xB9: "Pony Canyon",
0xBA: "Culture Brain",
0xBB: "Sunsoft",
0xBD: "Sony Imagesoft",
0xBF: "Sammy Corporation",
0xC0: "Taito",
0xC2: "Kemco",
0xC3: "Square",
0xC4: "Tokuma Shoten",
0xC5: "Data East",
0xC6: "Tonkin House",
0xC8: "Koei",
0xC9: "UFL",
0xCA: "Ultra Games",
0xCB: "VAP, Inc.",
0xCC: "Use Corporation",
0xCD: "Meldac",
0xCE: "Pony Canyon",
0xCF: "Angel",
0xD0: "Taito",
0xD1: "SOFEL (Software Engineering Lab)",
0xD2: "Quest",
0xD3: "Sigma Enterprises",
0xD4: "ASK Kodansha Co.",
0xD6: "Naxat Soft14",
0xD7: "Copya System",
0xD9: "Banpresto",
0xDA: "Tomy",
0xDB: "LJN",
0xDD: "Nippon Computer Systems",
0xDE: "Human Ent.",
0xDF: "Altron",
0xE0: "Jaleco",
0xE1: "Towa Chiki",
0xE2: "Yutaka # Needs more info",
0xE3: "Varie",
0xE5: "Epoch",
0xE7: "Athena",
0xE8: "Asmik Ace Entertainment",
0xE9: "Natsume",
0xEA: "King Records",
0xEB: "Atlus",
0xEC: "Epic/Sony Records",
0xEE: "IGS",
0xF0: "A Wave",
0xF3: "Extreme Entertainment",
0xFF: "LJN",
}
type ROMHeader struct {
EntryPoint [4]byte
Logo [48]byte
// NOTE(m): Assuming "old" cartridges here. This may cause problems with newer cartridges.
// See https://gbdev.io/pandocs/The_Cartridge_Header.html#0134-0143--title
Title [16]byte
NewLicenseeCode [2]byte
SGBFlag byte
CartridgeType byte
ROMSize byte
RAMSize byte
DestinationCode byte
OldLicenseeCode byte
MaskROMVersionNumber byte
HeaderChecksum byte
GlobalChecksum [2]byte
}
type Cartridge struct {
Filename string
Mapper string
Title string
Licensee string
SGBSupport bool
ROMSize int
RAMSize string
Destination string
Version int
}
func Insert(filename string) Cartridge {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
defer file.Close()
cartridge := Cartridge{Filename: file.Name()}
// Jump to start of header
_, err = file.Seek(0x0100, io.SeekStart)
if err != nil {
log.Fatal(err)
}
// Read header
var header ROMHeader
err = binary.Read(file, binary.LittleEndian, &header)
if err != nil {
log.Fatal(err)
}
// Validate the ROM by checking presence of the Nintendo logo
if header.Logo != expectedLogo {
log.Fatal("Invalid ROM file: No valid logo found!")
}
// Convert some header values
cartridge.Title = string(bytes.Trim(header.Title[:], "\x00"))
cartridge.Mapper = cartridgeTypes[header.CartridgeType]
if header.OldLicenseeCode == 0x33 {
// FIXME(m): Support new licensee codes
cartridge.Licensee = "Indicates that the New licensee code should be used instead."
} else {
cartridge.Licensee = oldLicensees[header.OldLicenseeCode]
}
cartridge.SGBSupport = (header.SGBFlag == 0x03)
cartridge.ROMSize = 32 * (1 << header.ROMSize)
cartridge.RAMSize = ramSizes[header.RAMSize]
switch header.DestinationCode {
case 0x00:
cartridge.Destination = "Japan (and possibly overseas)"
case 0x01:
cartridge.Destination = "Overseas only"
default:
cartridge.Destination = "UNKNOWN"
}
cartridge.Version = int(header.MaskROMVersionNumber)
// TODO(m): Verify header checksum
// NOTE(m): Ignoring global checksum which is not used, except by one emulator.
// See https://gbdev.io/pandocs/The_Cartridge_Header.html#014e-014f--global-checksum
return cartridge
}

View File

@@ -1,21 +0,0 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestInsertCartridge(t *testing.T) {
cartridge := Insert("rom.gb")
assert := assert.New(t)
assert.Equal(cartridge.Title, "SEIKEN DENSETSU")
assert.Equal(cartridge.Mapper, "MBC2+BATTERY")
assert.Equal(cartridge.Licensee, "Square")
assert.False(cartridge.SGBSupport, "SGB support should be false")
assert.Equal(cartridge.ROMSize, 256)
assert.Equal(cartridge.RAMSize, "0 - No RAM")
assert.Equal(cartridge.Destination, "Overseas only")
assert.Equal(cartridge.Version, 0)
}

21
cpu.go
View File

@@ -1,21 +0,0 @@
package main
type Cpu struct {
A uint8
Flags uint8
BC uint16
DE uint16
HL uint16
SP uint16
PC uint16
}
func Reset() Cpu {
cpu := Cpu{}
return cpu
}
func Tick(cpu *Cpu) {
cpu.PC++
}

92
gb/bus.go Normal file
View File

@@ -0,0 +1,92 @@
package gb
import (
"fmt"
)
// 0x0000 - 0x3FFF : ROM Bank 0
// 0x4000 - 0x7FFF : ROM Bank 1 - Switchable
// 0x8000 - 0x97FF : CHR RAM
// 0x9800 - 0x9BFF : BG Map 1
// 0x9C00 - 0x9FFF : BG Map 2
// 0xA000 - 0xBFFF : Cartridge RAM
// 0xC000 - 0xCFFF : RAM Bank 0
// 0xD000 - 0xDFFF : RAM Bank 1-7 - switchable - Color only
// 0xE000 - 0xFDFF : Reserved - Echo RAM
// 0xFE00 - 0xFE9F : Object Attribute Memory
// 0xFEA0 - 0xFEFF : Reserved - Unusable
// 0xFF00 - 0xFF7F : I/O Registers
// 0xFF80 - 0xFFFE : Zero Page
type Bus struct {
Cart *Cartridge
RAM *RAM
}
func NewBus(cart *Cartridge) *Bus {
bus := Bus{Cart: cart, RAM: NewRAM()}
return &bus
}
func (bus *Bus) Read(address uint16) byte {
if address < 0x8000 {
// ROM data
value, err := bus.Cart.Read(address)
if err != nil {
fmt.Printf("Error reading from bus address %X: %s\n", address, err)
return 0
}
return value
} else if address < 0xE000 {
//WRAM (Working RAM)
return bus.RAM.WRAMRead(address)
} else if address < 0xFF80 {
//IO Registers
return IORead(address)
} else {
fmt.Printf("Reading from bus address %X not implemented!\n", address)
return 0
}
}
func (bus *Bus) Read16(address uint16) uint16 {
lo := bus.Read(address)
hi := bus.Read(address + 1)
return uint16(lo) | uint16(hi)<<8
}
func (bus *Bus) Write(address uint16, value byte) error {
if address < 0x8000 {
// ROM data
err := bus.Cart.Write(address, value)
if err != nil {
return fmt.Errorf("Error writing to bus address %X: %s", address, err)
}
return nil
} else if address < 0xE000 {
//WRAM (Working RAM)
bus.RAM.WRAMWrite(address, value)
return nil
} else if address < 0xFF80 {
//IO Registers
IOWrite(address, value)
return nil
} else {
return fmt.Errorf("Writing to bus address %X not implemented!", address)
}
}
func (bus *Bus) Write16(address uint16, value uint16) error {
err := bus.Write(address+1, byte((value>>8)&0xFF))
if err != nil {
return err
}
err = bus.Write(address, byte(value&0xFF))
if err != nil {
return err
}
return nil
}

View File

@@ -3,22 +3,11 @@ package gb
import (
"bytes"
"encoding/binary"
"io"
"log"
"fmt"
"os"
)
// See https://gbdev.io/pandocs/The_Cartridge_Header.html#0104-0133--nintendo-logo
var expectedLogo = [48]byte{
0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B,
0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D,
0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E,
0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99,
0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC,
0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E,
}
// Cartridge types
// Cartridge types aka Mappers
// See https://gbdev.io/pandocs/The_Cartridge_Header.html#0147--cartridge-type
var cartridgeTypes = map[byte]string{
0x00: "ROM ONLY",
@@ -215,6 +204,7 @@ var oldLicensees = map[byte]string{
}
type ROMHeader struct {
_ [256]byte
EntryPoint [4]byte
Logo [48]byte
// NOTE(m): Assuming "old" cartridges here. This may cause problems with newer cartridges.
@@ -228,11 +218,13 @@ type ROMHeader struct {
DestinationCode byte
OldLicenseeCode byte
MaskROMVersionNumber byte
HeaderChecksum byte
Checksum byte
GlobalChecksum [2]byte
}
type Cartridge struct {
Header *ROMHeader
Data []byte
Filename string
Mapper string
Title string
@@ -242,60 +234,67 @@ type Cartridge struct {
RAMSize string
Destination string
Version int
Checksum byte
}
func InsertCartridge(path string) *Cartridge {
file, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
defer file.Close()
cartridge := Cartridge{Filename: file.Name()}
func InsertCartridge(filename string) (*Cartridge, error) {
cart := Cartridge{Filename: filename}
// Jump to start of header
_, err = file.Seek(0x0100, io.SeekStart)
data, err := os.ReadFile(filename)
if err != nil {
log.Fatal(err)
return &cart, err
}
cart.Data = data
// Read header
var header ROMHeader
err = binary.Read(file, binary.LittleEndian, &header)
buffer := bytes.NewReader(cart.Data)
err = binary.Read(buffer, binary.LittleEndian, &header)
if err != nil {
log.Fatal(err)
}
// Validate the ROM by checking presence of the Nintendo logo
if header.Logo != expectedLogo {
log.Fatal("Invalid ROM file: No valid logo found!")
return &cart, nil
}
cart.Header = &header
// Convert some header values
cartridge.Title = string(bytes.Trim(header.Title[:], "\x00"))
cartridge.Mapper = cartridgeTypes[header.CartridgeType]
cart.Title = string(bytes.Trim(header.Title[:], "\x00"))
cart.Mapper = cartridgeTypes[header.CartridgeType]
if header.OldLicenseeCode == 0x33 {
// FIXME(m): Support new licensee codes
cartridge.Licensee = "Indicates that the New licensee code should be used instead."
cart.Licensee = "Indicates that the New licensee code should be used instead."
} else {
cartridge.Licensee = oldLicensees[header.OldLicenseeCode]
cart.Licensee = oldLicensees[header.OldLicenseeCode]
}
cartridge.SGBSupport = (header.SGBFlag == 0x03)
cartridge.ROMSize = 32 * (1 << header.ROMSize)
cartridge.RAMSize = ramSizes[header.RAMSize]
cart.SGBSupport = (header.SGBFlag == 0x03)
cart.ROMSize = 32 * (1 << header.ROMSize)
cart.RAMSize = ramSizes[header.RAMSize]
switch header.DestinationCode {
case 0x00:
cartridge.Destination = "Japan (and possibly overseas)"
cart.Destination = "Japan (and possibly overseas)"
case 0x01:
cartridge.Destination = "Overseas only"
cart.Destination = "Overseas only"
default:
cartridge.Destination = "UNKNOWN"
cart.Destination = "UNKNOWN"
}
cartridge.Version = int(header.MaskROMVersionNumber)
cart.Version = int(header.MaskROMVersionNumber)
// TODO(m): Verify header checksum
// Calculate and verify checksum
for address := uint16(0x0134); address <= uint16(0x014C); address++ {
cart.Checksum = cart.Checksum - cart.Data[address] - 1
}
if cart.Checksum != header.Checksum {
return &cart, fmt.Errorf("ROM checksum failed: %X does not equal %X", cart.Checksum, header.Checksum)
}
// NOTE(m): Ignoring global checksum which is not used, except by one emulator.
// See https://gbdev.io/pandocs/The_Cartridge_Header.html#014e-014f--global-checksum
return &cartridge
return &cart, nil
}
func (cart *Cartridge) Read(address uint16) (byte, error) {
return cart.Data[address], nil
}
func (cart *Cartridge) Write(address uint16, value byte) error {
return fmt.Errorf("Writing to cartridge address %X not implemented!", address)
}

31
gb/cartridge_test.go Normal file
View File

@@ -0,0 +1,31 @@
package gb
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestInsert(t *testing.T) {
cartridge, err := InsertCartridge("../roms/dmg-acid2.gb")
assert := assert.New(t)
assert.Nil(err)
assert.Equal(cartridge.Filename, "../roms/dmg-acid2.gb")
assert.Equal(cartridge.Title, "DMG-ACID2")
assert.Equal(cartridge.Mapper, "ROM ONLY")
assert.Equal(cartridge.Licensee, "None")
assert.False(cartridge.SGBSupport, "SGB support should be false")
assert.Equal(cartridge.ROMSize, 32)
assert.Equal(cartridge.RAMSize, "0 - No RAM")
assert.Equal(cartridge.Destination, "Japan (and possibly overseas)")
assert.Equal(cartridge.Version, 0)
assert.Equal(cartridge.Checksum, byte(0x9F))
}
func TestFailedChecksum(t *testing.T) {
_, err := InsertCartridge("../roms/failed-checksum.gb")
assert.EqualError(t, err, "ROM checksum failed: 9F does not equal 41")
}

View File

@@ -1,17 +1,72 @@
package gb
import (
"fmt"
"image"
"image/color"
)
const (
ConsoleWidth = 160
ConsoleHeight = 144
)
type Console struct {
Cartridge *Cartridge
Bus *Bus
CPU *CPU
front *image.RGBA
Ticks uint64
}
func NewConsole(path string) (*Console, error) {
cartridge := InsertCartridge(path)
cart, err := InsertCartridge(path)
if err != nil {
return &Console{}, err
}
console := Console{cartridge}
fmt.Println("Cartridge loaded:")
fmt.Printf("\t Title : %s\n", cart.Title)
fmt.Printf("\t Type : %02X (%s)\n", cart.Header.CartridgeType, cart.Mapper)
fmt.Printf("\t ROM Size : %d KB\n", cart.ROMSize)
fmt.Printf("\t RAM Size : %02X (%s)\n", cart.Header.RAMSize, cart.RAMSize)
fmt.Printf("\t LIC Code : %02X (%s)\n", cart.Header.OldLicenseeCode, cart.Licensee)
fmt.Printf("\t ROM Vers : %02X\n", cart.Header.MaskROMVersionNumber)
fmt.Printf("\t Checksum : %02X ", cart.Checksum)
if cart.Checksum == cart.Header.Checksum {
fmt.Printf("(PASSED)\n")
} else {
fmt.Printf("(FAILED)\n")
}
buffer := image.NewRGBA(image.Rect(0, 0, ConsoleWidth, ConsoleHeight))
bus := NewBus(cart)
console := Console{Bus: bus, CPU: NewCPU(bus), front: buffer}
return &console, nil
}
func (console *Console) Update(dt uint64) {
console.StepSeconds(dt)
func (console *Console) StepMilliSeconds(ms uint64) {
speed := int(ms / 3)
for y := range ConsoleHeight {
for x := range ConsoleWidth {
console.front.Set(x, y, color.RGBA{0, uint8(y + speed), uint8(x + speed), 255})
}
}
}
func (console *Console) Buffer() *image.RGBA {
return console.front
}
func (console *Console) Cycle(cycles int) {
for range cycles {
// Cycles are given in M-cycles (machine cycles) instead of T-states (system clock ticks)
// One machine cycle takes 4 system clock ticks to complete
for range 4 {
console.Ticks++
timer.Tick()
}
}
}

382
gb/cpu.go Normal file
View File

@@ -0,0 +1,382 @@
package gb
import (
"fmt"
"os"
)
const CPUFrequency = 4194304
type CPUFlags byte
const (
C CPUFlags = 1 << 4 // Carry Flag
H CPUFlags = 1 << 5 // Half Carry Flag
N CPUFlags = 1 << 6 // Subtract Flag
Z CPUFlags = 1 << 7 // Zero Flag
)
type Registers struct {
A byte
F CPUFlags
B byte
C byte
D byte
E byte
H byte
L byte
PC uint16
SP uint16
}
type CPU struct {
Bus *Bus
Regs Registers
Halted bool
Stepping bool
InterruptMasterEnable bool
InterruptFlags byte
}
func NewCPU(bus *Bus) *CPU {
cpu := CPU{}
cpu.Bus = bus
// NOTE(m): PC is usually set to 0x100 by the boot rom
// TODO(m): SP is usually set programmatically by the cartridge code.
// Remove this hardcoded value later!
cpu.Regs = Registers{SP: 0xDFFF}
cpu.Stepping = true
return &cpu
}
func (cpu *CPU) Step() {
if !cpu.Halted {
opcode := cpu.Bus.Read(cpu.Regs.PC)
fmt.Printf("%04X: (%02X %02X %02X) A: %02X B: %02X C: %02X D: %02X E: %02X H: %02X L: %02X\n", cpu.Regs.PC,
opcode, cpu.Bus.Read(cpu.Regs.PC+1), cpu.Bus.Read(cpu.Regs.PC+2), cpu.Regs.A, cpu.Regs.B, cpu.Regs.C,
cpu.Regs.D, cpu.Regs.E, cpu.Regs.H, cpu.Regs.L)
cpu.Regs.PC++
switch opcode {
case 0x00:
// NOP
case 0x0D:
// DEC C
cpu.Regs.C--
// Set appropriate flags
if cpu.Regs.C == 0 {
cpu.SetFlag(Z)
} else {
cpu.ClearFlag(Z)
}
cpu.SetFlag(N)
if (cpu.Regs.C & 0x0F) == 0x0F {
cpu.SetFlag(H)
} else {
cpu.ClearFlag(H)
}
case 0x0E:
// LD C, n8
val := cpu.Bus.Read(cpu.Regs.PC)
// emu_cycles(1);
cpu.Regs.C = val
cpu.Regs.PC++
case 0x10:
// STOP n8
cpu.Halted = true
case 0x11:
// LD DE, n16
cpu.Regs.E = cpu.Bus.Read(cpu.Regs.PC)
// emu_cycles(1);
cpu.Regs.D = cpu.Bus.Read(cpu.Regs.PC + 1)
// emu_cycles(1);
cpu.Regs.PC += 2
case 0x12:
// LD [DE], A
// Get 16-bit address from DE
address := uint16(cpu.Regs.D)<<8 | uint16(cpu.Regs.E)
cpu.Bus.Write(address, cpu.Regs.A)
case 0x14:
// INC D
cpu.Regs.D++
// Set appropriate flags
if cpu.Regs.D == 0 {
cpu.SetFlag(Z)
} else {
cpu.ClearFlag(Z)
}
cpu.ClearFlag(N)
if (cpu.Regs.D & 0x0F) == 0 {
cpu.SetFlag(H)
} else {
cpu.ClearFlag(H)
}
case 0x18:
// JR e8
// Jump relative to 8-bit signed offset
// emu_cycles(3);
offset := int8(cpu.Bus.Read(cpu.Regs.PC))
cpu.Regs.PC++
cpu.Regs.PC = uint16(int(cpu.Regs.PC) + int(offset))
case 0x1C:
// INC E
cpu.Regs.E++
// Set appropriate flags
if cpu.Regs.E == 0 {
cpu.SetFlag(Z)
} else {
cpu.ClearFlag(Z)
}
cpu.ClearFlag(N)
if (cpu.Regs.E & 0x0F) == 0 {
cpu.SetFlag(H)
} else {
cpu.ClearFlag(H)
}
case 0x20:
// JR NZ, e8
if cpu.IsFlagSet(Z) {
// Z is set, don't execute
// emu_cycles(2);
cpu.Regs.PC++
} else {
// Z is not set, execute
// Jump relative to 8-bit signed offset
// emu_cycles(3);
offset := int8(cpu.Bus.Read(cpu.Regs.PC))
cpu.Regs.PC++
cpu.Regs.PC = uint16(int(cpu.Regs.PC) + int(offset))
}
case 0x21:
// LD HL, n16
cpu.Regs.L = cpu.Bus.Read(cpu.Regs.PC)
// emu_cycles(1);
cpu.Regs.H = cpu.Bus.Read(cpu.Regs.PC + 1)
// emu_cycles(1);
cpu.Regs.PC += 2
case 0x2A:
// LD A, [HL+] or LD A, [HLI]
// Get 16-bit address from HL
address := uint16(cpu.Regs.H)<<8 | uint16(cpu.Regs.L)
// Read byte at address and assign value to A register
cpu.Regs.A = cpu.Bus.Read(address)
// emu_cycles(1);
// Increment HL
address++
cpu.Regs.H = byte(address >> 8)
cpu.Regs.L = byte(address)
// emu_cycles(1);
case 0x31:
// LD SP, n16
lo := cpu.Bus.Read(cpu.Regs.PC)
// emu_cycles(1);
hi := cpu.Bus.Read(cpu.Regs.PC + 1)
// emu_cycles(1);
cpu.Regs.SP = uint16(lo) | uint16(hi)<<8
cpu.Regs.PC += 2
case 0x3C:
// INC A
cpu.Regs.A++
// Set appropriate flags
if cpu.Regs.A == 0 {
cpu.SetFlag(Z)
} else {
cpu.ClearFlag(Z)
}
cpu.ClearFlag(N)
if (cpu.Regs.A & 0x0F) == 0 {
cpu.SetFlag(H)
} else {
cpu.ClearFlag(H)
}
case 0x3E:
// LD A, n8
val := cpu.Bus.Read(cpu.Regs.PC)
// emu_cycles(1);
cpu.Regs.A = val
cpu.Regs.PC++
case 0x47:
// LD B, A
cpu.Regs.B = cpu.Regs.A
case 0x78:
// LD A, B
cpu.Regs.A = cpu.Regs.B
case 0x7C:
// LD A, H
cpu.Regs.A = cpu.Regs.H
case 0x7D:
// LD A, L
cpu.Regs.A = cpu.Regs.L
case 0xCB:
// Prefix byte instructions
cbOpcode := cpu.Bus.Read(cpu.Regs.PC)
fmt.Printf("%04X: (%02X %02X %02X) A: %02X B: %02X C: %02X\n", cpu.Regs.PC,
cbOpcode, cpu.Bus.Read(cpu.Regs.PC+1), cpu.Bus.Read(cpu.Regs.PC+2), cpu.Regs.A, cpu.Regs.B, cpu.Regs.C)
cpu.Regs.PC++
switch cbOpcode {
// case 0x7E:
// // BIT 7, [HL]
// // Read byte pointed to by address HL
// address := uint16(cpu.Regs.H)<<8 | uint16(cpu.Regs.L)
// val := cpu.Bus.Read(address)
// // Check if bit 7 is set
// if (val & 0x80) == 0 {
// // Set zero flag if bit is not set
// cpu.SetFlag(Z)
// }
// cpu.ClearFlag(N)
// cpu.SetFlag(H)
default:
fmt.Printf("\nINVALID INSTRUCTION! Unknown CB opcode: %02X\n", cbOpcode)
os.Exit(1)
}
case 0xC3:
// JP a16
lo := cpu.Bus.Read(cpu.Regs.PC)
// emu_cycles(1);
hi := cpu.Bus.Read(cpu.Regs.PC + 1)
// emu_cycles(1);
cpu.Regs.PC = uint16(lo) | uint16(hi)<<8
case 0xC9:
// RET
// emu_cycles(4);
cpu.Regs.PC = cpu.StackPop16()
case 0xCD:
// CALL a16
cpu.StackPush16(cpu.Regs.PC + 2)
lo := cpu.Bus.Read(cpu.Regs.PC)
// emu_cycles(1);
hi := cpu.Bus.Read(cpu.Regs.PC + 1)
// emu_cycles(1);
cpu.Regs.PC = uint16(lo) | uint16(hi)<<8
case 0xE0:
// LDH [a8], A
offset := cpu.Bus.Read(cpu.Regs.PC)
address := 0xFF00 | uint16(offset)
cpu.Bus.Write(address, cpu.Regs.A)
cpu.Regs.PC++
case 0xE5:
// PUSH HL
// emu_cycles(4);
cpu.StackPush(cpu.Regs.H)
cpu.StackPush(cpu.Regs.L)
case 0xE9:
// JP HL
val := uint16(cpu.Regs.H)<<8 | uint16(cpu.Regs.L)
cpu.Regs.PC = val
case 0xEA:
// LD [a16], A
lo := cpu.Bus.Read(cpu.Regs.PC)
cpu.Regs.PC++
hi := cpu.Bus.Read(cpu.Regs.PC)
cpu.Regs.PC++
address := uint16(lo) | uint16(hi)<<8
cpu.Bus.Write(address, cpu.Regs.A)
case 0xF3:
// DI
cpu.InterruptMasterEnable = false
default:
fmt.Printf("\nINVALID INSTRUCTION! Unknown opcode: %02X\n", opcode)
os.Exit(1)
}
}
}
func (cpu *CPU) SetFlag(flag CPUFlags) {
cpu.Regs.F |= flag
}
func (cpu *CPU) ClearFlag(flag CPUFlags) {
cpu.Regs.F &^= flag
}
func (cpu *CPU) ToggleFlag(flag CPUFlags) {
cpu.Regs.F ^= flag
}
func (cpu *CPU) IsFlagSet(flag CPUFlags) bool {
return cpu.Regs.F&flag != 0
}
func (cpu *CPU) StackPush(data byte) {
cpu.Regs.SP--
cpu.Bus.Write(cpu.Regs.SP, data)
}
func (cpu *CPU) StackPush16(data uint16) {
cpu.StackPush(byte((data >> 8) & 0xFF))
cpu.StackPush(byte(data & 0xFF))
}
func (cpu *CPU) StackPop() byte {
val := cpu.Bus.Read(cpu.Regs.SP)
cpu.Regs.SP++
return val
}
func (cpu *CPU) StackPop16() uint16 {
lo := cpu.StackPop()
hi := cpu.StackPop()
return uint16(hi)<<8 | uint16(lo)
}
func (cpu *CPU) GetInterruptFlags() byte {
return cpu.InterruptFlags
}
func (cpu *CPU) SetInterruptFlags(value byte) {
cpu.InterruptFlags = value
}

520
gb/cpu_test.go Normal file
View File

@@ -0,0 +1,520 @@
package gb
import (
"testing"
"github.com/stretchr/testify/assert"
)
func createCPU(data []byte) *CPU {
cart := Cartridge{Filename: "test"}
cart.Data = data
cpu := NewCPU(NewBus(&cart))
cpu.Regs.PC = 0
return cpu
}
func TestSetFlag(t *testing.T) {
cartridge, _ := InsertCartridge("../roms/dmg-acid2.gb")
bus := NewBus(cartridge)
cpu := NewCPU(bus)
cpu.SetFlag(C)
assert.True(t, cpu.IsFlagSet(C))
}
func TestClearFlag(t *testing.T) {
cartridge, _ := InsertCartridge("../roms/dmg-acid2.gb")
bus := NewBus(cartridge)
cpu := NewCPU(bus)
cpu.SetFlag(C)
assert.True(t, cpu.IsFlagSet(C))
cpu.ClearFlag(C)
assert.False(t, cpu.IsFlagSet(C))
}
func TestToggleFlag(t *testing.T) {
cartridge, _ := InsertCartridge("../roms/dmg-acid2.gb")
bus := NewBus(cartridge)
cpu := NewCPU(bus)
cpu.ToggleFlag(C)
assert.True(t, cpu.IsFlagSet(C))
cpu.ToggleFlag(C)
assert.False(t, cpu.IsFlagSet(C))
}
func TestInstruction00(t *testing.T) {
cpu := createCPU([]byte{0x00, 0x00, 0x00})
cpu.Step()
assert.Equal(t, uint16(0x01), cpu.Regs.PC)
}
func TestInstruction3C(t *testing.T) {
// Should increment A register
cpu := createCPU([]byte{0x3C, 0x00, 0x00})
cpu.Step()
assert.Equal(t, byte(0x01), cpu.Regs.A)
// Should clear Zero Flag
cpu = createCPU([]byte{0x3C, 0x00, 0x00})
cpu.SetFlag(Z)
cpu.Step()
assert.False(t, cpu.IsFlagSet(Z))
// Should set Zero Flag
cpu = createCPU([]byte{0x3C, 0x00, 0x00})
cpu.Regs.A = 0xFF
cpu.Step()
assert.True(t, cpu.IsFlagSet(Z))
// Should clear Subtract Flag
cpu = createCPU([]byte{0x3C, 0x00, 0x00})
cpu.SetFlag(N)
cpu.Step()
assert.False(t, cpu.IsFlagSet(N))
// Should set Half Carry Flag if we overflow from bit 3
cpu = createCPU([]byte{0x3C, 0x00, 0x00})
cpu.Regs.A = 0x0F
cpu.Step()
assert.True(t, cpu.IsFlagSet(H))
// Should clear Half Carry Flag if we don't overflow from bit 3
cpu = createCPU([]byte{0x3C, 0x00, 0x00})
cpu.SetFlag(H)
cpu.Step()
assert.False(t, cpu.IsFlagSet(H))
}
func TestInstructionC3(t *testing.T) {
cpu := createCPU([]byte{0xC3, 0x50, 0x01})
cpu.Step()
assert.Equal(t, uint16(0x150), cpu.Regs.PC)
}
func TestInstructionE9(t *testing.T) {
cpu := createCPU([]byte{0xE9, 0x00, 0x00})
cpu.Regs.H = 0x12
cpu.Regs.L = 0x34
cpu.Step()
assert.Equal(t, uint16(0x1234), cpu.Regs.PC)
}
func TestInstructionF3(t *testing.T) {
cpu := createCPU([]byte{0xF3, 0x00, 0x00})
cpu.InterruptMasterEnable = true
cpu.Step()
assert.False(t, cpu.InterruptMasterEnable)
}
func TestInstruction31(t *testing.T) {
cpu := createCPU([]byte{0x31, 0xFF, 0xCF})
cpu.Step()
// Should load SP with n16 value
assert.Equal(t, uint16(0xCFFF), cpu.Regs.SP)
// Should step over the 16 bit value onto the next instruction, i.e. increase the program counter with 3.
assert.Equal(t, uint16(0x0003), cpu.Regs.PC)
}
func TestInstructionCD(t *testing.T) {
cpu := createCPU([]byte{0xCD, 0x4C, 0x48, 0x41})
cpu.Step()
// Should push the address of the next instruction onto the stack
// In this case 0x41 which is at address 0x03 (i.e. the 4th instruction in the row above creating the CPU instance)
assert.Equal(t, uint16(0x03), cpu.Bus.Read16(cpu.Regs.SP))
// Should jump to n16 value
assert.Equal(t, uint16(0x484C), cpu.Regs.PC)
}
func TestInstruction21(t *testing.T) {
cpu := createCPU([]byte{0x21, 0x34, 0x12})
cpu.Step()
// Should load the 16-bit immediate value into the combined HL register
assert.Equal(t, byte(0x12), cpu.Regs.H)
assert.Equal(t, byte(0x34), cpu.Regs.L)
// Should increase the stack pointer
assert.Equal(t, uint16(0x3), cpu.Regs.PC)
}
// func TestInstructionCB7E(t *testing.T) {
// cpu := createCPU([]byte{0xCB, 0x7E, 0x00, 0x00})
// cpu.Regs.H = 0xFF
// cpu.Regs.L = 0x40
// err := cpu.Bus.Write(0xFF40, 0xFF)
// assert.Equal(t, err, nil)
// // FIXME(m): This needs bus access to IO, in particular the LCD
// // at 0xFF40.
// // assert.Equal(t, 0xFF, cpu.Bus.Read(0xFF40))
// // cpu.Step()
// // // Should set the zero flag
// // assert.True(t, cpu.IsFlagSet(Z))
// }
func TestInstruction47(t *testing.T) {
cpu := createCPU([]byte{0x47, 0x00, 0x00})
cpu.Regs.A = 0xDE
cpu.Step()
// Should load value in register A into register B
assert.Equal(t, byte(0xDE), cpu.Regs.B)
// Should increase the stack pointer
assert.Equal(t, uint16(0x1), cpu.Regs.PC)
}
func TestInstruction11(t *testing.T) {
cpu := createCPU([]byte{0x11, 0x34, 0x12})
cpu.Step()
// Should load the 16-bit immediate value into the combined DE register
assert.Equal(t, byte(0x12), cpu.Regs.D)
assert.Equal(t, byte(0x34), cpu.Regs.E)
// Should increase the stack pointer
assert.Equal(t, uint16(0x3), cpu.Regs.PC)
}
func TestInstruction0E(t *testing.T) {
cpu := createCPU([]byte{0x0E, 0xDE, 0x00})
cpu.Step()
// Should load the 8-bit immediate value into register C
assert.Equal(t, byte(0xDE), cpu.Regs.C)
// Should increase the stack pointer
assert.Equal(t, uint16(0x2), cpu.Regs.PC)
}
func TestInstruction2A(t *testing.T) {
cpu := createCPU([]byte{0x2A, 0x00, 0x00})
cpu.Regs.H = 0xD1
cpu.Regs.L = 0x23
cpu.Bus.Write(0xD123, 0xDE)
cpu.Step()
// Should load the byte pointed to by HL into register A
assert.Equal(t, byte(0xDE), cpu.Regs.A)
// Should increment HL
hl := uint16(cpu.Regs.H)<<8 | uint16(cpu.Regs.L)
assert.Equal(t, uint16(0xD124), hl)
// Should increase the stack pointer
assert.Equal(t, uint16(0x1), cpu.Regs.PC)
}
func TestInstruction12(t *testing.T) {
cpu := createCPU([]byte{0x12, 0x00, 0x00})
cpu.Regs.D = 0xD1
cpu.Regs.E = 0x23
cpu.Regs.A = 0xDE
cpu.Step()
// Should copy the value in register A into the byte pointed to by DE
address := uint16(cpu.Regs.D)<<8 | uint16(cpu.Regs.E)
assert.Equal(t, byte(0xDE), cpu.Bus.Read(address))
// Should increase the stack pointer
assert.Equal(t, uint16(0x1), cpu.Regs.PC)
}
func TestInstruction1C(t *testing.T) {
// Should increment E register
cpu := createCPU([]byte{0x1C, 0x00, 0x00})
cpu.Step()
assert.Equal(t, byte(0x01), cpu.Regs.E)
// Should clear Zero Flag
cpu = createCPU([]byte{0x1C, 0x00, 0x00})
cpu.SetFlag(Z)
cpu.Step()
assert.False(t, cpu.IsFlagSet(Z))
// Should set Zero Flag
cpu = createCPU([]byte{0x1C, 0x00, 0x00})
cpu.Regs.E = 0xFF
cpu.Step()
assert.True(t, cpu.IsFlagSet(Z))
// Should clear Subtract Flag
cpu = createCPU([]byte{0x1C, 0x00, 0x00})
cpu.SetFlag(N)
cpu.Step()
assert.False(t, cpu.IsFlagSet(N))
// Should set Half Carry Flag if we overflow from bit 3
cpu = createCPU([]byte{0x1C, 0x00, 0x00})
cpu.Regs.E = 0x0F
cpu.Step()
assert.True(t, cpu.IsFlagSet(H))
// Should clear Half Carry Flag if we don't overflow from bit 3
cpu = createCPU([]byte{0x1C, 0x00, 0x00})
cpu.SetFlag(H)
cpu.Step()
assert.False(t, cpu.IsFlagSet(H))
}
func TestInstruction20(t *testing.T) {
// Should not jump if Z is set
cpu := createCPU([]byte{0x20, 0xFB, 0x00})
cpu.SetFlag(Z)
cpu.Step()
assert.Equal(t, uint16(0x02), cpu.Regs.PC)
// Should jump to positive offset if Z is not set
cpu = createCPU([]byte{0x20, 0x0A, 0x00})
cpu.Step()
assert.Equal(t, uint16(0x0C), cpu.Regs.PC)
// Should jump to negative offset if Z is not set
cpu = createCPU([]byte{0x20, 0xFB, 0x00})
cpu.Step()
assert.Equal(t, uint16(0xFFFD), cpu.Regs.PC)
}
func TestInstruction10(t *testing.T) {
cpu := createCPU([]byte{0x10, 0x2A, 0x12})
cpu.Step()
assert.True(t, cpu.Halted)
}
func TestInstruction14(t *testing.T) {
// Should increment D register
cpu := createCPU([]byte{0x14, 0x00, 0x00})
cpu.Step()
assert.Equal(t, byte(0x01), cpu.Regs.D)
// Should clear Zero Flag
cpu = createCPU([]byte{0x14, 0x00, 0x00})
cpu.SetFlag(Z)
cpu.Step()
assert.False(t, cpu.IsFlagSet(Z))
// Should set Zero Flag
cpu = createCPU([]byte{0x14, 0x00, 0x00})
cpu.Regs.D = 0xFF
cpu.Step()
assert.True(t, cpu.IsFlagSet(Z))
// Should clear Subtract Flag
cpu = createCPU([]byte{0x14, 0x00, 0x00})
cpu.SetFlag(N)
cpu.Step()
assert.False(t, cpu.IsFlagSet(N))
// Should set Half Carry Flag if we overflow from bit 3
cpu = createCPU([]byte{0x14, 0x00, 0x00})
cpu.Regs.D = 0x0F
cpu.Step()
assert.True(t, cpu.IsFlagSet(H))
// Should clear Half Carry Flag if we don't overflow from bit 3
cpu = createCPU([]byte{0x14, 0x00, 0x00})
cpu.SetFlag(H)
cpu.Step()
assert.False(t, cpu.IsFlagSet(H))
}
func TestInstruction0D(t *testing.T) {
// Should decrement C register
cpu := createCPU([]byte{0x0D, 0x00, 0x00})
cpu.Regs.C = 0x01
cpu.Step()
assert.Equal(t, byte(0x00), cpu.Regs.C)
// Should clear Zero Flag
cpu = createCPU([]byte{0x0D, 0x00, 0x00})
cpu.SetFlag(Z)
cpu.Step()
assert.False(t, cpu.IsFlagSet(Z))
// Should set Zero Flag
cpu = createCPU([]byte{0x0D, 0x00, 0x00})
cpu.Regs.C = 0x01
cpu.Step()
assert.True(t, cpu.IsFlagSet(Z))
// Should set Subtract Flag
cpu = createCPU([]byte{0x0D, 0x00, 0x00})
cpu.SetFlag(N)
cpu.Step()
assert.True(t, cpu.IsFlagSet(N))
// Should set Half Carry Flag if we overflow from bit 3
cpu = createCPU([]byte{0x0D, 0x00, 0x00})
cpu.Regs.C = 0x10
cpu.Step()
assert.True(t, cpu.IsFlagSet(H))
// Should clear Half Carry Flag if we don't overflow from bit 3
cpu = createCPU([]byte{0x0D, 0x00, 0x00})
cpu.Regs.C = 0x01
cpu.SetFlag(H)
cpu.Step()
assert.False(t, cpu.IsFlagSet(H))
}
func TestInstruction78(t *testing.T) {
cpu := createCPU([]byte{0x78, 0x00, 0x00})
cpu.Regs.B = 0xDE
cpu.Step()
// Should load value in register B into register A
assert.Equal(t, byte(0xDE), cpu.Regs.A)
// Should increase the stack pointer
assert.Equal(t, uint16(0x1), cpu.Regs.PC)
}
func TestInstructionEA(t *testing.T) {
cpu := createCPU([]byte{0xEA, 0x23, 0xD1})
cpu.Regs.A = 0x42
cpu.Step()
// Should load byte in register A into the memory location pointed to by the 16-bit immediate value
val := cpu.Bus.Read(0xD123)
assert.Equal(t, byte(0x42), val)
// Should increase the stack pointer
assert.Equal(t, uint16(0x3), cpu.Regs.PC)
}
func TestInstruction3E(t *testing.T) {
cpu := createCPU([]byte{0x3E, 0xDE, 0x00})
cpu.Step()
// Should load the 8-bit immediate value into register A
assert.Equal(t, byte(0xDE), cpu.Regs.A)
// Should increase the stack pointer
assert.Equal(t, uint16(0x2), cpu.Regs.PC)
}
func TestInstructionE0(t *testing.T) {
cpu := createCPU([]byte{0xE0, 0x07, 0x00})
cpu.Regs.A = 0x42
cpu.Step()
// Should copy the value in register A into the byte at address 0xFF00 + 0x07
val := cpu.Bus.Read(0xFF07)
assert.Equal(t, byte(0x42), val)
// Should increase the stack pointer
assert.Equal(t, uint16(0x2), cpu.Regs.PC)
}
func TestInstruction7D(t *testing.T) {
cpu := createCPU([]byte{0x7D, 0x00, 0x00})
cpu.Regs.L = 0xDE
cpu.Step()
// Should load into register A the value in register L
assert.Equal(t, byte(0xDE), cpu.Regs.A)
// Should increase the stack pointer
assert.Equal(t, uint16(0x1), cpu.Regs.PC)
}
func TestInstruction7C(t *testing.T) {
cpu := createCPU([]byte{0x7C, 0x00, 0x00})
cpu.Regs.H = 0xDE
cpu.Step()
// Should load into register A the value in register H
assert.Equal(t, byte(0xDE), cpu.Regs.A)
// Should increase the stack pointer
assert.Equal(t, uint16(0x1), cpu.Regs.PC)
}
func TestInstruction18(t *testing.T) {
// Should jump to positive offset
cpu := createCPU([]byte{0x18, 0x0A, 0x00})
cpu.Step()
assert.Equal(t, uint16(0x0C), cpu.Regs.PC)
// Should jump to negative offset
cpu = createCPU([]byte{0x18, 0xFB, 0x00})
cpu.Step()
assert.Equal(t, uint16(0xFFFD), cpu.Regs.PC)
}

45
gb/io.go Normal file
View File

@@ -0,0 +1,45 @@
package gb
import (
"fmt"
"os"
)
var SerialData [2]byte
var InterruptFlags byte
func IORead(address uint16) byte {
if address == 0xFF01 {
return SerialData[0]
} else if address == 0xFF02 {
return SerialData[1]
} else if (address >= 0xFF04) && (address <= 0xFF07) {
return timer.Read(address)
} else if address == 0xFF0F {
return InterruptFlags
} else if (address >= 0xFF10) && (address <= 0xFF3F) {
// Ignore sound
return 0
} else {
fmt.Printf("Reading from IO: invalid address %X\n", address)
os.Exit(1)
}
return 0
}
func IOWrite(address uint16, value byte) {
if address == 0xFF01 {
SerialData[0] = value
} else if address == 0xFF02 {
SerialData[1] = value
} else if (address >= 0xFF04) && (address <= 0xFF07) {
timer.Write(address, value)
} else if address == 0xFF0F {
InterruptFlags = value
} else if (address >= 0xFF10) && (address <= 0xFF3F) {
// Ignore sound
} else {
fmt.Printf("Writing to IO: invalid address %X\n", address)
os.Exit(1)
}
}

55
gb/ram.go Normal file
View File

@@ -0,0 +1,55 @@
package gb
import (
"fmt"
"os"
)
type RAM struct {
WRAM [0x2000]byte
HRAM [0x80]byte
}
func NewRAM() *RAM {
ram := RAM{}
return &ram
}
func (ram *RAM) WRAMRead(address uint16) byte {
// TODO(m): Understand this line
address -= 0xC000
if address >= 0x2000 {
fmt.Printf("Reading from WRAM: invalid address %X\n", address+0xC000)
os.Exit(1)
}
return ram.WRAM[address]
}
func (ram *RAM) WRAMWrite(address uint16, value byte) {
// TODO(m): Understand this line
address -= 0xC000
if address >= 0x2000 {
fmt.Printf("Writing to WRAM: invalid address %X\n", address)
os.Exit(1)
}
ram.WRAM[address] = value
}
func (ram *RAM) HRAMRead(address uint16) byte {
// TODO(m): Understand this line
address -= 0xFF80
return ram.HRAM[address]
}
func (ram *RAM) HRAMWrite(address uint16, value byte) {
// TODO(m): Understand this line
address -= 0xFF80
ram.HRAM[address] = value
}

59
gb/stack_test.go Normal file
View File

@@ -0,0 +1,59 @@
package gb
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestStackPush(t *testing.T) {
cpu := createCPU([]byte{0x00, 0x00, 0x00})
cpu.StackPush(0xDE)
// Should decrement the stack pointer
assert.Equal(t, cpu.Regs.SP, uint16(0xDFFE))
// Should write the value to the stack
assert.Equal(t, cpu.Bus.Read(cpu.Regs.SP), byte(0xDE))
}
func TestStackPush16(t *testing.T) {
cpu := createCPU([]byte{0x00, 0x00, 0x00})
cpu.StackPush16(0xDEAD)
// Should decrement the stack pointer twice
assert.Equal(t, cpu.Regs.SP, uint16(0xDFFD))
// Should write the value to the stack
assert.Equal(t, cpu.Bus.Read16(cpu.Regs.SP), uint16(0xDEAD))
}
func TestStackPop(t *testing.T) {
cpu := createCPU([]byte{0x00, 0x00, 0x00})
cpu.StackPush(0xDE)
val := cpu.StackPop()
// Should increment the stack pointer
assert.Equal(t, cpu.Regs.SP, uint16(0xDFFF))
// Should return the byte value
assert.Equal(t, val, byte(0xDE))
}
func TestStackPop16(t *testing.T) {
cpu := createCPU([]byte{0x00, 0x00, 0x00})
cpu.StackPush16(0xBEEF)
val := cpu.StackPop16()
// Should increment the stack pointer
assert.Equal(t, cpu.Regs.SP, uint16(0xDFFF))
// Should return the 16 bit value
assert.Equal(t, val, uint16(0xBEEF))
}

125
gb/timer.go Normal file
View File

@@ -0,0 +1,125 @@
package gb
import (
"fmt"
"os"
)
var timer = Timer{DIV: 0xAC00}
type Timer struct {
DIV uint16
TIMA byte
TMA byte
TAC byte
}
func (timer *Timer) Tick() {
previousDIV := timer.DIV
timer.DIV++
timerNeedsUpdate := false
// Determine clock mode
// TAC (Timer control register)
// Bits 0-1 determines clock frequency
// 00: 4096 Hz
// 01: 262144 Hz
// 10: 65536 Hz
// 11: 16384 Hz
// Bit 2 determines whether timer is enabled or not
clockMode := timer.TAC & (0b11)
switch clockMode {
case 0b00:
// 4096 Hz
// Detect a falling edge by comparing bit 9 in the previous DIV value
// with bit 9 in the current DIV value
previouslyEnabled := previousDIV&(1<<9) != 0
currentlyDisabled := timer.DIV&(1<<9) == 0
timerNeedsUpdate = previouslyEnabled && currentlyDisabled
case 0b01:
// 262144 Hz
// Detect a falling edge by comparing bit 3 in the previous DIV value
// with bit 3 in the current DIV value
previouslyEnabled := previousDIV&(1<<3) != 0
currentlyDisabled := timer.DIV&(1<<3) == 0
timerNeedsUpdate = previouslyEnabled && currentlyDisabled
case 0b10:
// 65536 Hz
// Detect a falling edge by comparing bit 5 in the previous DIV value
// with bit 5 in the current DIV value
previouslyEnabled := previousDIV&(1<<5) != 0
currentlyDisabled := timer.DIV&(1<<5) == 0
timerNeedsUpdate = previouslyEnabled && currentlyDisabled
case 0b11:
// 16384 Hz
// Detect a falling edge by comparing bit 7 in the previous DIV value
// with bit 7 in the current DIV value
previouslyEnabled := previousDIV&(1<<7) != 0
currentlyDisabled := timer.DIV&(1<<7) == 0
timerNeedsUpdate = previouslyEnabled && currentlyDisabled
}
timerIsEnabled := timer.TAC&(1<<2) != 0
// If the timer needs to be updated based on the determined clock mode and the timer is enabled, increment the timer
if timerNeedsUpdate && timerIsEnabled {
timer.TIMA++
// Check if TIMA is going to wrap and trigger an interrupt if necessary
if timer.TIMA == 0xFF {
timer.TIMA = timer.TMA
fmt.Println("TODO: cpu_request_interrupt(IT_TIMER")
}
}
}
func (timer *Timer) Read(address uint16) byte {
switch address {
case 0xFF04:
return byte(timer.DIV >> 8)
case 0xFF05:
return timer.TIMA
case 0xFF06:
return timer.TMA
case 0xFF07:
return timer.TAC
default:
fmt.Printf("Reading from Timer: invalid address %X\n", address)
os.Exit(1)
}
return 0
}
func (timer *Timer) Write(address uint16, value byte) {
switch address {
case 0xFF04:
//DIV
timer.DIV = 0
case 0xFF05:
//TIMA
timer.TIMA = value
case 0xFF06:
//TMA
timer.TMA = value
case 0xFF07:
//TAC
timer.TAC = value
default:
fmt.Printf("Writing to Timer: invalid address %X\n", address)
os.Exit(1)
}
}

30
main.go
View File

@@ -1,16 +1,30 @@
package main
import (
"gb-player/ui"
"fmt"
"log"
"os"
// "gb-player/ui"
"gb-player/gb"
)
func main() {
// FIXME(m): Allow specifying rom file on command line
// if len(os.Args) != 2 {
// log.Fatalln("No rom file specified")
// }
// romPath := os.Args[1]
romPath := "rom.gb"
if len(os.Args) != 2 {
log.Fatalln("No rom file specified")
}
romPath := os.Args[1]
ui.Run(romPath)
// ui.Run(romPath)
console, err := gb.NewConsole(romPath)
if err != nil {
log.Fatal(err)
}
fmt.Println("Executing instructions")
for !console.CPU.Halted {
console.CPU.Step()
}
}

BIN
roms/01-special.gb Normal file

Binary file not shown.

BIN
roms/02-interrupts.gb Normal file

Binary file not shown.

BIN
roms/03-op sp,hl.gb Normal file

Binary file not shown.

BIN
roms/04-op r,imm.gb Normal file

Binary file not shown.

BIN
roms/05-op rp.gb Normal file

Binary file not shown.

BIN
roms/06-ld r,r.gb Normal file

Binary file not shown.

Binary file not shown.

BIN
roms/08-misc instrs.gb Normal file

Binary file not shown.

BIN
roms/09-op r,r.gb Normal file

Binary file not shown.

BIN
roms/10-bit ops.gb Normal file

Binary file not shown.

BIN
roms/11-op a,(hl).gb Normal file

Binary file not shown.

BIN
roms/cpu_instrs.gb Normal file

Binary file not shown.

BIN
roms/dmg-acid2.gb Normal file

Binary file not shown.

BIN
roms/failed-checksum.gb Normal file

Binary file not shown.

BIN
roms/mem_timing.gb Normal file

Binary file not shown.

View File

@@ -1,25 +1,37 @@
package ui
import (
"gb-player/gb"
"fmt"
"log"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
"gb-player/gb"
)
type Controller struct {
window *sdl.Window
renderer *sdl.Renderer
font *ttf.Font
view *View
timestamp uint64
}
func NewController(renderer *sdl.Renderer) *Controller {
func NewController(window *sdl.Window, renderer *sdl.Renderer, font *ttf.Font) *Controller {
controller := Controller{}
controller.window = window
controller.renderer = renderer
controller.font = font
return &controller
}
func (c *Controller) Start(path string) {
console := gb.NewConsole(path)
console, err := gb.NewConsole(path)
if err != nil {
log.Fatal(err)
}
c.view = NewView(c, console)
c.Run()
}
@@ -27,60 +39,73 @@ func (c *Controller) Start(path string) {
func (c *Controller) Step() {
timestamp := sdl.GetTicks64()
dt := timestamp - c.timestamp
c.view.Update(timestamp, dt)
c.timestamp = timestamp
c.view.Update(dt)
}
func (c *Controller) Run() {
var fps float64
var frameCount uint32
var startTicks = sdl.GetTicks64()
var now, last uint64
// cartridge := gb.Insert(romPath)
// fmt.Println(cartridge)
var frameStart = sdl.GetTicks64()
running := true
for running {
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
switch event.(type) {
switch e := event.(type) {
case *sdl.QuitEvent:
println("Quit")
running = false
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN {
if e.Keysym.Sym == sdl.K_ESCAPE {
running = false
}
}
last = now
now = sdl.GetPerformanceCounter()
deltaTime := float64((now - last) * 1000.0 / sdl.GetPerformanceFrequency())
_ = deltaTime
}
}
// Clear screen
renderer.SetDrawColor(0x63, 0x94, 0xED, 0xff)
renderer.Clear()
c.renderer.SetDrawColor(0x63, 0x94, 0xED, 0xff)
c.renderer.Clear()
// Update state
c.Step()
rect := sdl.Rect{X: 100, Y: 100, W: 200, H: 150}
renderer.SetDrawColor(0, 0, 255, 255)
renderer.FillRect(&rect)
drawDebugWindow(fps)
c.drawDebugWindow(fps)
// Present to screen
renderer.Present()
c.renderer.Present()
// Calculate framerate
frameCount++
currentTicks := sdl.GetTicks64()
elapsed := currentTicks - startTicks
frameEnd := sdl.GetTicks64()
elapsed := frameEnd - frameStart
if elapsed >= 1000 {
fps = float64(frameCount) / (float64(elapsed) / 1000.0)
// Reset counters
// Reset framerate counters
frameCount = 0
startTicks = currentTicks
frameStart = frameEnd
}
}
}
func (c *Controller) drawDebugWindow(fps float64) {
renderer := c.renderer
font := c.font
// FPS
textSurface, err := font.RenderUTF8Blended(fmt.Sprintf("FPS: %.2f", fps), sdl.Color{R: 0, G: 0, B: 0, A: 255})
if err != nil {
log.Fatal(err)
}
defer textSurface.Free()
textTexture, err := renderer.CreateTextureFromSurface(textSurface)
if err != nil {
log.Fatal(err)
}
defer textTexture.Destroy()
textRect := sdl.Rect{X: 0, Y: 0, W: textSurface.W, H: textSurface.H}
renderer.Copy(textTexture, nil, &textRect)
}

View File

@@ -1,7 +1,6 @@
package ui
import (
"fmt"
"log"
"runtime"
@@ -9,8 +8,10 @@ import (
"github.com/veandco/go-sdl2/ttf"
)
var renderer *sdl.Renderer
var font *ttf.Font
const (
windowWidth = 800
windowHeight = 600
)
func init() {
runtime.LockOSThread()
@@ -31,45 +32,27 @@ func Run(romPath string) {
"GB Player",
sdl.WINDOWPOS_UNDEFINED,
sdl.WINDOWPOS_UNDEFINED,
800, 600,
windowWidth, windowHeight,
sdl.WINDOW_SHOWN)
if err != nil {
panic(err)
}
defer window.Destroy()
renderer, err = sdl.CreateRenderer(window, -1, 0)
renderer, err := sdl.CreateRenderer(window, -1, 0)
if err != nil {
panic(err)
}
defer renderer.Destroy()
renderer.RenderSetVSync(true)
font, err = ttf.OpenFont("SourceCodePro.ttf", 18)
font, err := ttf.OpenFont("SourceCodePro.ttf", 18)
if err != nil {
log.Fatal(err)
}
defer font.Close()
font.SetStyle(ttf.STYLE_BOLD)
controller := NewController(renderer)
controller := NewController(window, renderer, font)
controller.Start(romPath)
}
func drawDebugWindow(fps float64) {
// FPS
textSurface, err := font.RenderUTF8Blended(fmt.Sprintf("FPS: %.2f", fps), sdl.Color{R: 0, G: 0, B: 0, A: 255})
if err != nil {
log.Fatal(err)
}
defer textSurface.Free()
textTexture, err := renderer.CreateTextureFromSurface(textSurface)
if err != nil {
log.Fatal(err)
}
defer textTexture.Destroy()
textRect := sdl.Rect{X: 0, Y: 0, W: textSurface.W, H: textSurface.H}
renderer.Copy(textTexture, nil, &textRect)
}

View File

@@ -1,6 +1,11 @@
package ui
import (
"image"
"log"
"github.com/veandco/go-sdl2/sdl"
"gb-player/gb"
)
@@ -12,3 +17,44 @@ type View struct {
func NewView(controller *Controller, console *gb.Console) *View {
return &View{controller, console}
}
func (view *View) Update(dt uint64) {
console := view.console
renderer := view.controller.renderer
console.StepMilliSeconds(dt)
buffer := console.Buffer()
drawBuffer(renderer, buffer)
}
func drawBuffer(renderer *sdl.Renderer, buffer *image.RGBA) {
width, height := gb.ConsoleWidth, gb.ConsoleHeight
imageTexture, err := renderer.CreateTexture(
uint32(sdl.PIXELFORMAT_RGBA32),
sdl.TEXTUREACCESS_STREAMING,
int32(width), int32(height))
if err != nil {
log.Fatal(err)
}
defer imageTexture.Destroy()
pixels, pitch, err := imageTexture.Lock(nil)
if err != nil {
log.Fatal(err)
}
for y := range height {
start := y * buffer.Stride
end := start + width*4
copy(pixels[y*pitch:y*pitch+width*4], buffer.Pix[start:end])
}
imageTexture.Unlock()
x := (windowWidth / 2) - width
y := (windowHeight / 2) - height
imageRect := sdl.Rect{X: int32(x), Y: int32(y), W: 2 * int32(width), H: 2 * int32(height)}
renderer.Copy(imageTexture, nil, &imageRect)
}