⭅ Previous (Running Programs) Next (Control Flow and Graphics) ⭆

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 first four bits of A. This gets us closer to the value we want.

For each variable we want to extract, we’ll need to come up with an appropriate ‘B’ to select only the relevant bits. The design of Chip-8 makes this easy for us, since variables always take up a full hex digit.

Remember that hex 0xF = binary 0b1111 (all bits set). So to we create a mask, 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’.

Shifting bits

Though we have selected only the bits we care about, there are still several trailing 0s. We want to be able to extract ‘X’, not ‘X00’. To get rid of these 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 code in Rust, feel free to adapt to your language.

// Instruction pattern is 0x6XNN
// For this example consider a hard-coded instruction:
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
}

Registers and implementing the 6XNN instruction

Now that we have our instruction parameters extracted, we’re nearly ready to implement this instruction. First, we will need to add registers to our emulator.

A register is a single value that can be stored and manipulated within the CPU. The Chip-8 system contains 16 variables. These are named V0 (aka register 0) up to VF (aka register 0xF or 16). These each hold an 8 bit value. Since most of the instructions that work with registers express logic in terms of “do something with Nth register”, it makes sense to story these as an array.

And finally we can implement the 0x6XNN instruction which should set register X to the value NN.

struct Chip8 {
    // ...
    // newly added registers:
    registers: [u8; 16],  // 16 values, each u8
}

fn execute(&mut self, opcode: u16) {
    // instruction decoding we saw earlier:
    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
        self.registers[x] = nn;
    } else if (...) {
        // implement other instructions
    }
}

Conclusion

And now we have added basic register assignment instructions to our emulator. With registers and instruction decoding, we have everything needed to implement most of the Chip-8 instructions.

Based on the ’type’ column on the Wikipedia table, you should be able to implement any instruction of type:

Type Number of instructions
Const 2
Assig 1
BitOp 5
Math 3
Mem 5

Giving us 16 / 35 total Chip-8 instructions. Next up we’ll look at the display, and cover a few more instruction types so that our emulator is ready for basic graphical programs.

Until next time!

⭅ Previous (Running Programs) Next (Control Flow and Graphics) ⭆

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.