Writing a CHIP-8 Emulator: Barebones setup (Part 1)

Posted on Aug 24, 2024

Hello, wizards! Today we will look into the magical world of emulation. We will look at the CHIP-8 programming language, how it works, and later on even begin writing a CHIP-8 emulator.

This tutorial will only serve as a high-level guide for beginner emulator developers, so you’ll have to write your own code ;) Worry not however, there will be some code snippets here and there, and incase you get really stuck, feel free to read my own implementation.

Wait, we’re going to write an emulator for… a programming language?

Yes. CHIP-8 is an interpreted programming language developed by Joseph Weisbecker in the 70s. We’re going to emulate the hardware that the first CHIP-8 interpreters ran on, Cosmac VIP.

This comment on Reddit nicely explains what CHIP-8 is.

The Hardware

RCA Cosmac VIP

RCA Cosmac VIP

Photo By Dave Ruske - https://www.flickr.com/photos/druske/29620857115, CC BY 2.0

As previously mentioned, we’re going to emulate the Cosmac VIP, which was released in 1977. Its “brain” was RCA CDP1802, an 8-bit microprocessor, originally also built by Joseph Weisbecker using TTL components at his home in the early 70s.

Processor

The processor contains 19 registers total:

  • 16 Data registers V0 - VF. All registers are 8-bits wide. VF register is used as a “carry” or “collision detection” register.
  • 2 Timer registers T0 and T1. Both are 8-bits wide, and count down every second until their value is non-zero.
  • 1 Address register I. It is 16-bits wide, however usually only the lower 12 of them are used.

There are two additional internal registers that are reserved for the processor only. These are the program counter and stack pointer, which are both 16 bits long. We will talk about them later, when we will actually do something with them.

Memory

The Cosmac VIP featured 2 KB of RAM, expandable up to whopping 4 KB (or 32 KB via an expansion slot). Most programs will happily fit into 4 KB, so there’s no reason to reserve more memory than that.

The operating system (CHIP-8 interpreter) lies at the first 512 bytes of memory (starting at address 0x000), so all programs must be loaded after these first 512 bytes (at address 0x200). This first 512-byte is however not used in emulators, as the emulator lives outside of this space, so we will use this space to store the font.

Display

Original CHIP-8 implementation supports 64x32 monochrome displays. As this would be almost impossible to see anything on today’s computer screens, this value is often scaled (a sane number is 20, resulting in a 1280x640 window).

Graphics are drawn as 8 pixels wide and 1-15 pixels high. All coordinates are positive and start at 0, with coordinates 0x0 being the most upper left pixel of the screen.

Timers

CHIP-8 uses two timers which both count down at 60 Hz (1 tick every second). The delay timer’s value can be read/written, and is intended for event timing. The sound timer can only be written to, and makes a beeping sound every time its value is not zero (ie. setting its value to 3 would produce a 3-second beep).

Input

A 4x4 16-key hex keyboard (spanning from hex digits from 0 - F) is used as an input device. You can map the keys as follows:

Cosmac VIPEmulator
1, 2, 3, C1, 2, 3, 4
4, 5, 6, DQ, W, E, R
7, 8, 9, EA, S, D, F
A, 0, B, FZ, X, C, V

Keys 2, 4, 6 and 8 are typically used for game input (similarly to arrows on today’s computer keyboards).

Sound

The VIP features a very simple integrated speaker. My implementation creates a 440 Hz square wave sound to emulate the speaker sound.

Font

As previously mentioned when talking about memory, we’re going to use the first 512 bytes to store the font. It consists of 16 8x5 characters, representing hex values 0 - F. Most emulators use this font:

const uint8_t font[] = {
	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
};

Setting up the project

SDL2 can output video, sound and process input, making writing the emulator (and everything around it) very easy. Here’s a simple Makefile that compiles the source code and automatically links it with SDL2:

# User-changeable variables
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O3
LDFLAGS ?=

BIN_DIR := bin
OBJ_DIR := build
SRC_DIR := src

CFLAGS += $(shell pkg-config sdl2 --cflags)
LDFLAGS += $(shell pkg-config sdl2 --libs)

EXECUTABLE_NAME := chip8emu
EXECUTABLE := $(BIN_DIR)/$(EXECUTABLE_NAME)

SRC := $(foreach x, $(SRC_DIR), $(wildcard $(addprefix $(x)/*,.c*)))
OBJ := $(addprefix $(OBJ_DIR)/, $(addsuffix .o, $(notdir $(basename $(SRC)))))

.PHONY: all
all: $(EXECUTABLE)

$(EXECUTABLE): $(OBJ)
	@mkdir -p $(@D)
	$(CC) $(LDFLAGS) $(OBJ) -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	@mkdir -p $(@D)
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:
	@rm -rf $(BIN_DIR) $(BUILD_DIR) $(OBJ_DIR)

This Makefile depends on the following directory structure (which can be easily modified):

├── Makefile
├── bin/ (Executable)
├── build/ (Object files)
└── src/ (Source files)

Emulator initialization

Initialization is very easy and requires only a few things:

Processor

  • All data registers V0 through VF and address register I are set to 0.
  • Stack pointer points to the beginning of the stack (which is 24 bytes long).
  • Program counter is set to 0x200 (513th byte of the memory).
  • Both timers are set to 0.

Memory

  • First 80 bytes of memory should contain the font data (see font).
  • ROM is loaded at address 0x200.

Main loop

After initializing the emulator, loading the ROM into memory and creating a window, we can begin our main loop, where we will stay until it is time to exit:

while (true) {
	/* do stuff */
}

Try to dump the memory and see how everything is laid out:

Font Data

Font located at 0x000

Breakout dump in memory

Breakout written by David Winter in 1997 starts at address 0x200

That’s all for now, folks!

In the next part we’ll focus on drawing some nice images on screen, making our emulator do beeps and boops implementing a basic instruction set and lastly making it slow.