teal8: My CHIP-8 Implementation
Published:
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_Rect
s and SDL_bool
s:
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.