It’s been a while since I finished the CHIP-8 emulator, the CHIP-8 emulator was mostly capable of running PONG, but at the end I still haven’t figured out how to get input work correctly. This time I want to try to experiment with the Gameboy, it will be much harder to emulate given the increased number of instructions and a palette.
DMG-01
Also known as the “original gameboy”
Hardware Overview
CPU: Custom 8-bit sharp at 4.19MHZ
Resolution: 160*144
Framerate: 59.727hz
Memory: 8KiB internal S-RAM
VRAM: 8KiB
Color: 2-bit, 4 shades of “gray”
Input Dpad + action buttons
Serial I/O (Link Cable)
CPU
7 8-bit register (A,B,C,D,E,H,L)
5 16-bit register (HL, BC, DE, AF)
1 Flag register (Z, H, N, C)
Stack pointer register (16-bit)
Program Counter register (16-bit)
Also includes:
Pixel Processing Unit (PPU)
Audio Processing Unit (APU)
Graphics
OAM stores the sprite information, used during rendering
Palette stores color information, determine how a sprite appears on the screen
Tile data + tile maps is loaded from cartridge at startup to the VRAM
Anti Piracy
Done in bootrom
After the scrolling of the logo, bootrom will check the logo stored in the bootrom against the one in cartridge.
If checksum match, will disable bootrom and jump to the cartridge and start executing.
If checksum fails, the console will freeze (jump loop)
Due to the logo is copyrighted and trademarked by Nintendo, pirated and un-licensed games will not be including this logo in their cartridge unless they are ready to face Nintendo’s legal department.
Building the emulator
There is no magic. 💫
Each component is pretty straight forward to emulate if you have the spec sheet.
We can treat CPU just a block of code that modify internal state + memory when given input
We can treat the PPU as a block code that takes some internal state and render it to a picture.
debug!("Loaded cartridge: {}", cartridge.title()); debug!("Cartridge type is: {}", cartridge.get_cart_info()); debug!("Cartridge rom size is: {:?}", cartridge.get_rom_size()); debug!("Cartridge ram size is: {:?}", cartridge.get_ram_size());
cartridge }
Determine cartridge type at 0x147.
Return an MBC type according to the cartridge type.
Example MBC1 read
Each MBC has address space that is banked and unbanked
When accessing spaces that is banked, according the spec, we need to adjust the actual address we are reading from according to the current bank selection.
The implementation uses a Vector.
We will need to translate the memory address into an index for that Vector
// This register assigns gray shades for sprite palette 0. It works exactly as BGP (FF47), except that the lower // two bits aren't used because sprite data 00 is transparent. op0: u8, // This register assigns gray shades for sprite palette 1. It works exactly as BGP (FF47), except that the lower // two bits aren't used because sprite data 00 is transparent. op1: u8,
// Window Y Position (R/W), Window X Position minus 7 (R/W) wy: u8, wx: u8,
// If no boot rom is set, we simulate the boot rom states on the mmu and cpu if !has_bootrom { mmu.borrow_mut().simulate_boot_rom(); cpu.simulate_boot_rom(); }
Self { cpu, mmu } }
pubfntick(&mutself) ->u32 { // Execute one cpu cycle letcycles = self.cpu.tick(); // Update the mmu with the cycles self.mmu.borrow_mut().tick(cycles); cycles }