CHIP-8 Emulation

Since I was a kid games always fascinated me, so my first step in programming was coding little games. Recently I questioned myself about how old console game emulators work. I came across this fascinating topic, as always I searched how I start coding my own emulator to learn more about it, and with a few searches on the internet (especially on /r/emudev ) you can easily realize that CHIP-8 emulators are the “Hello World” of this subject.

This is not a full walkthrough post, but a guide to anyone who wants to start coding emulators or just understand how this kind of software works. I have no past experience with this and was able to achieve a result by searching on the internet and reading a lot of code. During the post, I’ll provide some golang code snippets and useful resources.

Many resources were used to make me understand how to do it, but the most used were:

What is CHIP-8?

CHIP-8, created by Joseph Weisbecker in the mid-70s, functions as an interpreter rather than an emulator, yet it serves as a crucial starting point for those delving into emulator programming.

This system was designed to run on 8-bit microcomputers, providing developers with a convenient platform for game development during that era.

Joseph Weisbecker also released the COSMAC-VIP (which you can see on the picture) in 1977, featuring 2KB of RAM (expandable to 4KB), a 512-byte operating system, and preloaded with 20 games programmed in CHIP-8.

COSMAC-VIP (1977) was shipped with 20 games in CHIP-8.

CHIP-8 has only 35 opcode (instructions), which make it a perfect start point to emulation.

How it works?

It contains stacks, registers, displays, input, and a program counter, its behavior is not different from basic interpreters. For a detailed list of how each component of CHIP-8 works, this post offers a deep explanation of each component.

Some important observations about it:

  • Our machines are way faster than that time, most interpreters fix their processing time to 700 instructions/second. Don’t be scared, a simple sleep solves this.
  • The timers are simple one-byte-sized counters, when they’re set to more than 0, they should decrease 60 times/second by one and do their action. Ex. The sound timer should produce a beep sound.
  • It has 4Kb of memory, and the ROM should be loaded into memory during startup.
  • The program Counter (PC) will point to the current instruction being read in the memory. It will be sequential but gotos may happen.
  • Index Register (also called I by CHIP-8 developers), is a simple register that stores memory addresses.
  • Registers used for general-purpose tasks. Usually referred as Vx where x is a hexadecimal digit (0 through F).
  • A simple stack of 16 16-bit values is used to store subroutines/function calls and its return. The Stack Frame or Stack Pointer is a simple pointer to the top of the stack.

Based on it, we can build a simple diagram to mentalize how the chips are working while coding.

CHIP-8 Simple mental model.

Input / Keyboard

Computers where CHIP-8 was made to run, have hexadecimal keyboards from 0 to F, if we made our emulator with this in mind, the experience while playing would not be good enough, so while developing you can choose a better key mapping to it.

Most modern emulators use any 4×4 mapping of the keyboard, the most common is to use 1, 2, 3, 4, Q, W, E, R,A,S,D, F, Z, X, C, V.

Display

The display on CHIP-8 is small and monochromatic, 64×32 pixels wide.

CHIP-8 Display

Memory

The Chip-8 language is capable of accessing up to 4KB (4096 bytes), but the first 512 bytes was where the Chip-8 interpreter itself was located, most interpreters load its rooms starting at address 512 (0x200) to emulate original behavior.

Representing Components

With this information in mind, we can start modeling our emulator into code. In Golang, our structure will look something like this:

type Chip8 struct {
	stack         [32]uint16 
	stackFrame    int
	indexRegister uint16
	registers     [16]byte
	pc            uint16
	memory        [4096]byte
	delayTimer    byte
	soundTimer    byte
}

byte in golang is just an alias for uint16, but on this struct, I used both words more logically.

Reading Files (ROMs)

To make our emulator gain life, we’ll need to read ROMs containing games, software, and many other applications, but how do we do that? Most of the developers are used to reading text data (UTF-8) on normal day jobs, and the task of reading binary data like a ROM can sound difficult, but it’s not!

var (
  MEMORY_OFFSET = 0x200
)

// code to open the file and load it on programData slice

for i := range programData {
		c.memory[MEMORY_OFFSET+i] = programData[i]
}

Instructions

To start decode instructions, you’ll have a infinite for loop that will exists during the entire emulator execution. The program counter (pc) starting position, is the first position in the program memory where the ROM was loaded (0x200).

Decoding instructions is easy if you did it before, but for people with no experience with it, working with binary data can be tricky.

All CHIP-8 instructions are 2 bytes long and are stored in the most significant byte first. The common names used are:

  • NNN or ADR: The lowest 12 bits of the instruction.
  • N or Nibble: The lowest 4 bits of the instruction.
  • X: The lower 4 bits of the high byte of the instruction.
  • Y: The upper 4 bits of the lower byte of the instruction.
  • KK or NN: The lowest 8 bits of the instruction.

Only reading can sound confusing for most people, but let’s take a look at a graphical view of what it really represents.

To decode the instruction in these variables the code in Golang, you can use this code:

b0 := c.memory[c.pc]
b1 := c.memory[c.pc+1]
c.pc += 2

instr := (b0 & 0xF0) >> 4

X := b0 & 0x0F
Y := (b1 & 0xF0) >> 4   
N := b1 & 0x0F       
NN := b1                        
NNN := uint16(X)<<8 | uint16(NN) 

This code in Golang was originally proposed in this post that I used to help me.

This code for newcomers to binary data programming and emulators can sound like black magic, but let’s analyze it line by line:

In the first and second lines, we extract the first and second instructions from the memory that we previously loaded. We load it using a PC (program counter) whose main function is to just point to which memory the software is currently reading. After we load it on b0 and b1 we just increment the program pointer in two (because we already read the two instructions).

After that, we extract the instruction on line five, as we say on the image, the instruction is the first 4 bits of the first byte, to do it we need to do an AND operation with binary masking (b0 & 0xF0) >> 4. Let’s visualize this operation with images:

In golang & are the binary AND operator.

It then shifts these 4 bits to the right by 4 positions to align them to the least significant bits of the variable with >> 4, and it extracts the instruction of the first byte!

On the next three lines, X, Y, and N are extracted with the same technique, and on line 10 we just set NN to the second instruction byte (b1).

After all that we just need to extract NNN on the last line NNN := uint16(X)<<8 | uint16(NN) , but what this line is doing? This line is basically merging the value of X (last 4 bits of first byte) with the NN (second byte) into only one value, to do it it uses binary OR operation, represented by | in Golang. The same concept from previous instructions, but now with OR.

Drawing on the screen

I thought this part would be the easiest, but, I was wrong! Draw things on the screen is hard, and I tried on my first attempt. After reading some posts on the internet (and almost giving up the entire challenge of building my own emulator), I decided to go with a game engine.

The game engine will have many helpers as needed to help reproduce sounds, scan the keyboard, and draw on the screen with a solid refresh rate among other features.

Every language will have a decent library to draw things on the screen, if it’s your first emulator, choose one and use it.

func (r *Runtime) Set(col int, row int, on bool) {
	if on {
		r.image.Set(col, row, colorWhite)
	} else {
		r.image.Set(col, row, colorBlack)
	}
}

Conclusion: Is it worth time?

Most of the programming challenges of this project is not common for most developers, to me for example, all the decoding instructions part sounds really hard at the beginning, and at the end, I felt like a learning something new. Most of this project was fun to do, and if you’re searching for a fun project (and understand how things work), this is a great one.