teal8: My CHIP-8 Implementation

9 minute read

Published:

CHIP-8 is an interpreted programming language developed by Joseph Weisbecker. teal8 is an interpreter for running CHIP-8 ROMs.

CHIP-8

CHIP-8 is a language interpreter that allowed for easy game programming. It was used in the 70s and 80s on computers such as the COSMAC VIP and the TELMAC 1800. The interpreter is very small due to the memory limitations of these computers, as the COSMAC VIP had 2K and the TELMAC 1800 had 4K.

teal8

Taking some inspiration from around the internet, I decided to create an implementation of CHIP-8 as a means of trying out emulation development and simply having fun. I also wanted to sharpen my skills with C, as I feel that the skills gained through working with C are more broadly applicable across all of software development than those from any other programming language.

Memory

All CHIP-8 programs, with few exceptions, start at memory address 0x200. The reason CHIP-8 programs begin at this address is because the interpreter used to live between addresses 0x000-0x1FF on the COSMAC VIP and the TELMAC 1800. Most implementations of CHIP-8, including my own, provide 4K of accessible and byte-addressable memory.

Implementation of Memory

We will implement the memory layout as an array of 4096 bytes (unsigned 8-bit integers):

#define MEMORY_BYTES 0x1000 // 4096 decimal ... uint8_t memory[MEMORY_BYTES]; // 4KB memory

teal8 writes programs into memory starting from memory address 0x200:
#define PROGRAM_START_ADDRESS 0x200 // 512 decimal ... chip8->pc = PROGRAM_START_ADDRESS; while (chip8->pc < MEMORY_BYTES && fread(&chip8->memory[chip8->pc], 1, 1, rom) == 1) chip8->pc++;

Font

CHIP-8 interpreters have built-in fonts that are drawn onto the screen just like sprites. Each font character is 4px wide and 5px tall. These font character sprites are represented in bytes as sequences of five hexadecimal numbers, with each hexadecimal number representing a row of 4px.

The location in memory at which you store the font data does not matter since CHIP-8 contains an instruction for setting the index register to a character's address. For this reason, modern interpreters typically use some of the unused memory addresses between 0x000-0x1FF to store font data.

Implementation of Font

Font data can be represented as an array of 80 bytes. We will store font data betwen memory addresses 0x000 and 0x050:
#define FONT_START_ADDRESS 0x000 // 0 decimal #define FONT_BYTES 0x50 // 80 decimal ... uint8_t font[FONT_BYTES] = { 0xF0, 0x90, 0x90, 0x90, 0xF0, // 0 0x20, 0x60, 0x20, 0x20, 0x70, // 1 0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2 0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3 0x90, 0x90, 0xF0, 0x10, 0x10, // 4 0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5 0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6 0xF0, 0x10, 0x20, 0x40, 0x40, // 7 0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8 0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9 0xF0, 0x90, 0xF0, 0x90, 0x90, // A 0xE0, 0x90, 0xE0, 0x90, 0xE0, // B 0xF0, 0x80, 0x80, 0x80, 0xF0, // C 0xE0, 0x90, 0x90, 0x90, 0xE0, // D 0xF0, 0x80, 0xF0, 0x80, 0xF0, // E 0xF0, 0x80, 0xF0, 0x80, 0x80 // F }; ... for (int i = 0x0; i < FONT_BYTES; i++) memory[FONT_START_ADDRESS + i] = font[i];

Registers

CHIP-8 contains 16 data registers used for storing bytes. These registers are called V0...VF. VF is used for carrying when using arithmetic instructions, and for collision detection when drawing sprites.

CHIP-8 contains one address register, I. This register is 2-bytes wide and is used for pointing at locations in memory.

CHIP-8 contains a program counter, internally referred to as PC. The 2-byte counter is used for pointing at the current instruction in memory.

CHIP-8 contains two timers that are simply 8-bit numbers that are decremented ~60 times per second when non-zero. One of these timers is the delay timer, which is generally used to make delay loops. The other timer is the sound timer, which is used to control the sound since the system will beep while the timer is zero.

CHIP-8 has a stack of 16 levels, each able to store two-byte addresses. The stack is used to call subroutines (push) and return from them (pop).

Implementation of Registers

The data registers are implemented as an array of 16 bytes:
#define REGISTERS 16 ... uint8_t v[REGISTERS]; // 16 8-bit registers (V0 to VF)

The address register is implemented as a 16-bit value:
uint16_t i; // 16-bit address register

The program counter is implemented as a 16-bit value, similarly to I:
uint16_t pc; // program counter

The timers are implemented as their own structure. This structure contains two 8-bit values, and one bigger value used to handle the timing of decrements:
typedef struct timers { uint8_t delay; uint8_t sound; uint32_t lastUpdate; } timers;

The stack is also implemented as its own structure. The stack structure contains an array of 16 2-byte addresses, and a byte value used as a pointer to the top of the stack.
#define STACK_SIZE 16 // 16 levels of stack typedef struct stack { uint16_t s[STACK_SIZE]; // 16 addresses uint8_t sp; // stack pointer index (0x0-0xF) } stack;

Display

The CHIP-8 display is 64px wide and 32px tall. Since the display is monochrome, each pixel is either on or off.

Implementation of Display

To understand the display implementation, I recommend reading some of the relevant source code to get the full picture.

In short, since we are using the SDL2 library, pixels and their display states are handled as a list of dynamically allocated SDL_Rects and SDL_bools:
SDL_Rect *pixels; SDL_bool *pixelDrawn;

The display structure also contains the window and the renderer, which is used to draw the pixels on screen and handle events:
SDL_Window *window; SDL_Renderer *renderer;

The only SDL events that the interpreter needs to worry about are KEYUP, KEYDOWN, and QUIT events. The interpreter stores the states of each key in arrays, and the arrays are updated as needed while processing SDL events:
SDL_bool keyDown[KEY_COUNT]; SDL_bool keyUp[KEY_COUNT];

Instructions

From David Winter's CHIP8 document:
NNN is an address, KK is an 8 bit constant X and Y are two 4 bits constants 0NNN Call 1802 machine code program at NNN (not implemented) 00CN Scroll down N lines (***) 00FB Scroll 4 pixels right (***) 00FC Scroll 4 pixels left (***) 00FD Quit the emulator (***) 00FE Set CHIP-8 graphic mode (***) 00FF Set SCHIP graphic mode (***) 00E0 Erase the screen 00EE Return from a CHIP-8 sub-routine 1NNN Jump to NNN 2NNN Call CHIP-8 sub-routine at NNN (16 successive calls max) 3XKK Skip next instruction if VX == KK 4XKK Skip next instruction if VX != KK 5XY0 Skip next instruction if VX == VY 6XKK VX = KK 7XKK VX = VX + KK 8XY0 VX = VY 8XY1 VX = VX OR VY 8XY2 VX = VX AND VY 8XY3 VX = VX XOR VY (*) 8XY4 VX = VX + VY, VF = carry 8XY5 VX = VX - VY, VF = not borrow (**) 8XY6 VX = VX SHR 1 (VX=VX/2), VF = carry 8XY7 VX = VY - VX, VF = not borrow (*) (**) 8XYE VX = VX SHL 1 (VX=VX*2), VF = carry 9XY0 Skip next instruction if VX != VY ANNN I = NNN BNNN Jump to NNN + V0 CXKK VX = Random number AND KK DXYN Draws a sprite at (VX,VY) starting at M(I). VF = collision. If N=0, draws the 16 x 16 sprite, else an 8 x N sprite. EX9E Skip next instruction if key VX pressed EXA1 Skip next instruction if key VX not pressed FX07 VX = Delay timer FX0A Waits a keypress and stores it in VX FX15 Delay timer = VX FX18 Sound timer = VX FX1E I = I + VX FX29 I points to the 4 x 5 font sprite of hex char in VX FX33 Store BCD representation of VX in M(I)...M(I+2) FX55 Save V0...VX in memory starting at M(I) FX65 Load V0...VX from memory starting at M(I) FX75 Save V0...VX (X<8) in the HP48 flags (***) FX85 Load V0...VX (X<8) from the HP48 flags (***) (*): Used to be undocumented (but functional) in the original docs. (**): When you do VX - VY, VF is set to the negation of the borrow. This means that if VX is superior or equal to VY, VF will be set to 01, as the borrow is 0. If VX is inferior to VY, VF is set to 00, as the borrow is 1. (***): SCHIP Instruction. Can be used in CHIP8 graphic mode.

Implementation of Instructions

All source code relevant to fetching, decoding, and executing instructions/opcodes can be found in these two functions that are declared in include/emulator.h and defined in src/emulator.c:
uint16_t fetchOpcode(emulator *chip8); void decodeAndExecuteOpcode(emulator *chip8, unsigned short opcode);

These functions are called directly from the main function. They represent the heart of the interpreter's main loop:
/* fetch, decode, and execute opcode */ opcode = fetchOpcode(&chip8); chip8.pc += 2; // increment program counter decodeAndExecuteOpcode(&chip8, opcode);

What I Learned: Emulation Development

CHIP-8 is widely considered to be the best project for getting into emulation development, and I certainly agree with this idea. This project has tremendously helped me understand the absolute basics of emulation, and I feel excited to eventually take on a new project of this sort.

Another thing that I learned from this project is working with hexadecimal values. This was a new concept for me, but it is certainly not complicated. I feel that working with these values is not at all scary once you realize that hexadecimal values are merely different representations of the decimal values you're used to.

Sources

All information provided here is based off of David Winter's CHIP-8 Emulation Page and tobiasvl's Guide To Making a CHIP-8 Emulator.