-
Notifications
You must be signed in to change notification settings - Fork 33
Home
How do emulators work? That is the goal of this project. Just learn the rough process to emulate a Gameboy. The first step is to setup a basic game loop as you might for any other game. This should initialize your render library (SDL2 in this case). In the game loop you will then want to execute one video frame of CPU cycles, then render the screen, and then wait any extra time to ensure your processor and video are running at the correct speed.
Key Links: http://problemkaputt.de/pandocs.htm
How long is one frame on the Gameboy? If you look in the link above and find the "Game Boy Technical Data" section, you will see that vertical sync is 59.73 Hz. This means, we need to render about 60 frames per second.
How may cycles does the Gameboy CPU process in one frame? If you look in the link above and find the "LCD Status Register" section, you will see that a complete screen refresh occurs every 70,244 clock cycles. The CPU should continue processing OpCodes until it meet or exceeds 70,244 clock cycles and then return to allow the emulator to refresh the screen.
A CPU is the brain of the emulator. It is responsible for tracking the state of memory, registers and executing code. CPU Register and Flags are fundamental. They are accessed often and change regularly. The first part of CPU implementation is getting the registers and flags setup.
CPU Specifications PDF - This contains more details than we need to implement, but contains VERY detailed description of everything related to the CPU. If you go to page 22 of the PDF you will see details on these registers. All page numbers in PDF are the GOTO page, not the page printed on the sheet.
There are 6 registers on the Gameboy CPU. Each is 16 bits and most can be accessed by the HIGH and LOW bits.
- AF: Accumulator and Flags
- BC: General purpose
- DE: General purpose
- HL: General purpose
- SP: Stack pointer
- PC: Program counter
All 8 bit arithmetic output is stored in the accumulator (A). This is often used for small values because it takes less CPU cycles.
Flags store state of the last operation such as CARRY, ZERO, ADD, SUBTRACT. These are detailed in the PDF on page 94. For now, we'll start by setting the initial PC as per the info at Powerup Sequence. It says it will start by running the first instruction at 0x0000. Eventually this will be a hidden chuck of BIOS memory that is 256 bytes long. For now, just create a 64k (0xFFFF) byte array that will act as the memory. Be sure to initialize this memory to all zeros.
Now, let's implement our first opcode, NOP (0x00). In the PDF referenced above, you can find the instruction for the NOP. It basically does nothing and uses 4 clock cycles. So, read from the PC (0x0000), to get the OpCode (0x00/NOP), then read for an array of function pointers. This should map to one for NOP. All that function does is increase the total cycles by 4 and increment the PC by 1.
Since the memory is all zeros, it'll run through the entire stack of memory (64k) running NOP, advancing the clock cycles by 4 every step. So, now would be a good time to make the CPU track the cycles and return once it gets to 70,244, or one frame. It is important to make sure you don't drop cycles, and you don't want to have to worry about the cycles overflowing, so every time you get to 70244 or higher, then subtract 70244 from the total cycles.
If my math is correct, then after running StepFrame() once, the Program Counter (PC) register should be around 17561. It might be a few higher or lower.
The Gameboy has a ROM built in that is 256 bytes long. This ROM is contains the basic initialization and Gameboy cartridge checksum. This is a good article that covers what the boot ROM does. It goes through line by line an breaks down what each byte does.
This ROM is loaded in to memory from 0x0000 to 0x00FF and execution starts at 0x0000. Per the Powerup Sequence, after the ROM has ran and reaches the end successfully, the internal ROM is disabled and execution of the cartridge starts at 0x0100. From this point forward 0x0000-0x00FF will not longer point to the internal boot ROM, but instead must map to the cartridge memory as shown in the Memory Map.
What this means is that the emulator memory needs to get smarter than just an array of bytes. You'll need a way to route memory requests to different pieces. For example:
- 0x0000->0x00FF [During Boot] goes to internal ROM, but is hidden and maps to the cartridge after boot.
- 0x0000->0x3FFF goes to the cartridge.
- 0x4000->0x7FFF goes to a switchable (which means it can point to different offsets on the cartrige) bank of memory on the cartridge.
- 0x8000->0x9FFF maps to the video RAM (used to load sprites, etc).
- 0xA000->0xBFFF maps to external RAM that may exist on the cartridge (switchable).
- 0xC000->0xCFFF goes to working RAM provided by the CPU/system. This is used by the game for calulations, etc.
- 0xD000->0xDFFF goes to a switchable bank of working RAM also provided by the CPU.
- 0xE000->0xFDFF is an echo of 0xC000->0xDDFF, but is really not ever used.
- 0xFE00->0xFE9F is the Sprite Attribute Table (OAM), more on this later.
- 0xFEA0->0xFEFF is not usable an can fail catastrophically if desired.
- 0xFF00->0xFF7F is used for I/O Ports (gamepad buttons, etc).
- 0xFF80->0xFFFE is High RAM (HRAM) provided by the CPU. It is very small, but can be accessed at times when other memory cannot. Some games may move OpCodes into here to run during this dead time.
- 0xFFFF is the Interrupt Enable Register, more on this later.
With all of this covered, we need to create a memory map unit (MMU) that maps the address to different classes that represent the various devices. And we need to load the BIOS ram into a 256 byte array chunk of memory that is used in "boot mode" to start execution. Finally, we need to create a Cartridge class that loads the game ROM into memory and is addressable vial the MMU to read data from the game ROM.
Once all of this is done, we can start working on the first OpCode of the boot ROM!