blog-static/content/blog/03_learning_emulation.md

4.8 KiB

title date tags
Learning Emulation, Part 2.5 - Implementation 2016-11-23 23:23:56.633942
C and C++
Emulation

This is the third post in a series I'm writing about Chip-8 emulation. If you want to see the first one, head [here]({{< relref "/blog/01_learning_emulation.md" >}}).

In the previous part of this tutorial, we created a type to represent a basic Chip-8 machine. However, we've done nothing to make it behave like one! Let's start working on that.

Initializing

If you're writing your emulator in C / C++, simply stating that we have a variable such as stackp (stack pointer) will not give you a clean value. Memory for these variables is allocated and never cleaned up, so they can (and do) contain gibberish. For some parts of our program, it's necessary to initialize everything we have to 0 or another meaningful value. A prime example is that of the timers, especially the sound timer. The sound timer beeps until its value is 0, and if it starts with a value of a few million, the game will make a sound without having asked for any. The stack pointer also needs to be set to 0. As it's proper style to initialize everything, we'll do just that.

  chip->pc = 0x200;
  chip->i = 0;
  chip->stackp = 0;
  chip->delay_timer = 0;
  chip->sound_timer = 0;

  for(int i = 0; i <   16; i++) chip->v[i] = 0;
  for(int i = 0; i < 4096; i++) chip->memory[i] = 0;
  for(int i = 0; i <   16; i++) chip->stack[i] = 0;
  for(int i = 0; i < (64 *32); i++) chip->display[i] = 0;

I set to program counter to 0x200 because, according to the specification on the Wiki page for Chip-8,

most programs written for the original system begin at memory location 512 (0x200)

The First Steps

We are now ready to start stepping the emulator. I'm going to omit loading a file into memory and assume that one is already loaded. However, I will remind the reader to load the programs into memory starting at address 0x200 when they write their file loading code.

According to the Chip-8 Wiki,

CHIP-8 has 35 opcodes, which are all two bytes long and stored big-endian.

So, the first thing we want to do in our step code is to combine the next two bytes at the program counter into a single short.

unsigned short int opcode = (chip->memory[chip->pc] << 8) | (chip->memory[chip->pc + 1]);

In this piece of code, we take the byte at the program counter, and shift it to the left 8 bits (the size of a byte). We then use binary OR to combine it with the next byte.

Now that we have an opcode, we need to figure out how to decode it. Most opcodes are discerned by the first hexadecimal digit that makes them up (for example, the F in 0xFABC), so let's first isolate that first digit. We can do that by using the binary AND operation on the program counter, with the second operand being 0xF000.

unsigned short int head = opcode & 0xf000;

If we had an opcode with a value such as 0x1234, running 0x1234 & 0xF000 will give us 0x1000. This is exactly what we want to tell each opcode apart. We can now start implementing the instructions!

The first instruction listed on the Wiki page is:

0x00E0: Clears the screen.

So, in our step code, we need to check if the opcode starts with a 0. If it does, then the whole head variable will be a 0.

if(head == 0) { 
...
}

Next, though, we have a problem. There are two codes on the Wiki page that start with 0:

0x00E0: Clears the screen.

0x00EE: Returns from a subroutine.

So now, we need to check if the ending of the instruction is 0xE0 and not 0xEE.

if((opcode & 0x00ff) == 0xe0) {
...
}

Now, we can just clear the screen the way we did when we initialized.

for(int i = 0; i < (64 *32); i++) chip->display[i] = 0;

All together, our code ends up being:

// Get the Opcode
unsigned short int opcode = (chip->memory[chip->pc] << 8) | (chip->memory[chip->pc + 1]);

// Decode opcode
if(head == 0) { 
  if((opcode & 0x00ff) == 0xe0) {
    for(int i = 0; i < (64 *32); i++) chip->display[i] = 0;
  }
}

// Increment the program counter; We don't want to execute the same thing twice. 
// We increment it by two because a single memory location is one byte, and an instruction is two bytes.
chip->pc += 2;

From here, we should be able to implement most of the instructions, as they are pretty basic. It's important that there is some independence here - though emulating a very popular thing like Chip-8 has plenty of tutorials, if we want to emulate something new, we need to be able to read the documentation and work from it alone. So, I leave the rest of the instructions up to you to implement. I might post another update with a guide on how to implement the draw instruction, as I had some trouble with it, but the documentation for Chip-8 is more than enough to help you with the opcodes on your own.