⮜ Previous (Running Programs)

Implementing Chip-8 Instructions

Welcome back to this series on building a Chip-8 emulator. Last time we looked at an outline for how the Chip-8 emulator works, and gave it the ability to run programs. But so far, it doesn’t know how to do anything inside that program.

This time we’ll add some basic operations aka instructions to our emulator.

Recognizing Chip-8 Instructions

Last time we saw that each operation is represented by two bytes, and we can combine those to get a 16 bit value that tells us which instruction to run.

Once we have that value, how does our emulator know what to do?

Looking at the table on the Chip-8 wikipedia page, you’ll see a table with opcodes and an explanation for what it means. You’ll notice that the values expressed in the opcode column aren’t simply numbers, but patterns. Take for example:

opcode type meaning
6XNN const set variable X to NN

Hex numbers only include 0-9, A-F. So the ‘6’ represents a hex value, but the ‘X’ and ‘NN’ are saying that those are variables. To implement this opcode, we’ll need to extract the X and NN fields, assuming that first digit is a ‘6’. We’ll need to use a few bitwise operations to extract these values.

Masking and selecting bits.

If you are already familiar with bitwise operations, feel free to skip this section. While computers commonly operate on large numbers, they’re also great at performing parallel operations across each bit in a number. You are likely already familiar with and, which results in true only when both inputs are true.

0 for False and 1 for True:

A B A and B
0 0 0
0 1 0
1 0 0
1 1 1

And can also be applied over each bit in a number. When using a bitwise and, the result will have a 1 bit wherever both inputs had a 1. This gives us a way of selecting only certain bits from a number.

Consider:

A    = 0b11010101
B    = 0b11110000
A & B= 0b11010000

We can think of B as selecting the bits on the far left in A. This gets us close to the value we want. And Chip-8 is simple in this regard, since each of the variables we want to extract take up a full hex digit (aka nibble).

Since 0xF is 0b1111, we essentially put an F in any position we want to extract. So for the pattern 6XNN, we can check the ‘6’ with ‘F000’, and extract the ‘X’ with ‘0F00’, and the NN with ‘00FF’.

However, there are still several trailing 0s. We want to be able to extract ‘X’, not ‘X00’. To get rid of the trailing 0s, we can shift all the bits over M places. We want the bits we selected to start in the 1s place, so there are no unselected zeros left on the right.

In the example above, this means shifting bits right by 4, since that is the number of 0s on the right of B, which we used to select. In most programming languages, (A >> 1), means A after shifting all the bits to the right by 1.

Since all the opcodes are expressed in Hexadecimal, we know that each symbol represents 4 bits. So we can look at the opcode pattern to figure out the number of bits we need to shift. We then multiply the number of positions by 4 to get the number of bits.

Lets look at the full decoding for 6XNN:

// Example in rust, feel free to adapt to your language.

let opcode = 0x6321;  // So X is 0x3 and NN is 0x21.

if (((opcode & 0xF000) >> 12) == 0x6) {
    let x = (opcode & 0x0F00) >> 8; // 2 hex digits * 4 bits = 8
    let nn = (opcode & 0x00FF); // no shifting needed, already right most bits
    // do seomthing with X and NN
} else if (...) {
    // implement other instructions
}
⮜ Previous (Running Programs)

We publish about 1 post a week discussing emulation and retro systems. Join our email list to get notified when a new post is available. You can unsubscribe at any time.