ghostpacket [ blog | about me | projects | contact ]

how did i create a chip8 emulator in c?

table of contents

⮐ back to blog

hello everyone, welcome to this little entry, where i'll try to explain as good as i can the way the chip8 works, and how did i implement an entire emulator for it using c and sdl.

the chip8

first, let's go over the technical details of how the chip8 works, before we take a look at the implementation of it, so we know what we are doing, and why we are doing so.

you can find a more complete reference here.

the memory layout

the chip8 has 4095 bytes of memory, they can be represented like this:

  +-----------------------+
  |                       | 0xFFF - End of Memory
  +-----------------------+
  |                       |
  |                       |
  |                       |
  |     0x200/0xFFF       |
  |     ProgramData       |
  |                       |
  |                       |
  +-----------------------+ 0x200 - Start of Chip8 programs
  |                       |
  |      0x00/0x1FF       |
  |   Reserved for Chip8  |
  |                       |
  +-----------------------+ 0x00 - Start of Chip8 RAM

at address 0x00 is where the start of the chip8 ram is marked, from address 0x00 to address 0x1ff (511 bytes) is a space reserved for chip8 to store some data, like the default character set (we'll go over that next). all the programs of the chip8 are loaded into memory 0x200, and they have from 0x200 to 0xfff to store all their instructions and custom sprites (we'll get to that too).

the chip8 has a stack that is an array of 16 different 16-bit values, used to store the addresses that the chip8 should return to when returning from a subroutine, meaning there can be 16 levels of nested subroutines. if that didn't make any sense to you, let's break it further, basically, when a subroutine is called, the memory address of the instruction where it was called is pushed to the stack, so if we have 3 nested subroutine calls, there are going to be 3 different entries in our stack, whenever any subroutine is finished its execution, the last entry from the stack will be popped, and we'll go back to that instruction.

the stack is a LIFO data structure (last in, first out), meaning that the last item that's pushed into the stack, will be the first one to be popped.

the character set

the chip8 contains some default sprites that are called character set. they are a group of sprites representing the hexadecimal digits form 0 to F. they are 5 bytes long (8x5 sprites). they are stored int he area reserved for the chip8 (0x00-0x1ff).

the following are some examples of the characters:

  +----+--------+----+  +----+--------+----+
  |"0" |Binary  |Hex |  |"1" |Binary  |Hex |
  +----+--------+----|  +----+--------+----+ 
  |****|11110000|0xF0|  |  1 |00100000|0x20|
  |*  *|10010000|0x90|  | 11 |01100000|0x60|
  |*  *|10010000|0x90|  |  1 |00100000|0x20|
  |*  *|10010000|0x90|  |  1 |00100000|0x20|
  |****|11110000|0xF0|  | 111|01110000|0x70|
  +----+--------+----+  +----+--------+----+

registers

the chip8 has 16 registers that can hold 8-bit values, that is, they can hold only one byte of information. the data registers are the following.

Data Registers
V0
V1
V2
V3
V4
V5
V6
V7
V8
V9
VA
VB
VC
VD
VE
VF

notice that the last register VF should not be used by a program, as some instructions will set this register as a flag when certain conditions are met, but all the others 15 registers can be used.

the chip8 also has some extra registers:

  • I - used to store memory addresses
  • PC - points to the address of the current instruction being executed. all the instructions of the chip8 are 2 bytes in size, so this should be incremented by 2 everytime that an instruction is executed
  • SP - this is the stack pointer, it basically points to a location in the stack
  • Sound Timer (ST) - this register allows the chip8 to play a beep sound. that beep sound will be played whenever this register is different from 0, it will be decrementing at a rate of 70z. if this register is 0, no beep sound will be played.
  • Delay Timer (DT) - the delay timer is used when we want to stop the execution of a program. this works pretty similar to the sound timer, when this register is different than 0, the chip8 will stop executing instructions, and will be decrementing at a rate of 60hz. when the delay timer is zero again, the program execution is resumed.

the display

the resolution of the chip8 display is 64x32 pixels, and it's monocrome, that means that it can display only black and white colours, they can be represented as booleans, 0 for black, 1 for white. when we draw to the screen, we are drawing sprites, not individual pixels. if a sprite overflows the screen, it will be wrapped back to the other side. the sprites can be a maximum of 8 bits of width, and up to 15 bytes in length.

the keyboard

the chip8 keyboard has only 16 keys: from the numbers 0 to F.

this is how it would look:

1 2 3 C
4 5 6 D
7 8 9 E
A 0 B F

instruction set

the instruction set of the chip8 is pretty small, it has only 36 different instructions. there are instructions for mathematical operations, drawing, and register manipulation.

the implementation

we will take as a reference my code (here)

the main chip8 structure

the core of the emulator is a structure called struct chip8 located in include/chip8, that structure is pretty simple, it looks like this:

  struct chip8
  {
    struct chip8_memory memory;
    struct chip8_registers registers;
    struct chip8_stack stack;
    struct chip8_keyboard keyboard;
    struct chip8_screen screen;
  };

that structure contains all the elements that were mentioned previously, the memory, the registers, the stack, the keyboard, and the screen.

the chip8_memory structure (defined in include/mem.h) is just a structure that contains a 4095 variable unsigned char memory, and has some functions to set some value at a certain index, to get a value at a certain index, and to get a short (two bytes) at a certain index.

the chip8_registers structure (defined in include/registers.h) contains all the registers that were mentioned previously, it has an array of unsigned chars called V with a size of 16, as well as some others unsigned char like the delay timer, the sound timer, and stack pointer, the other registers like the I and PC are of type unsigned short.

the chip8_stack structure (defined in include/stack.h), just has a variable of type unsigned short called stack, with a value of 16, and some functions to push and to pop.

the chip8_keyboard and chip8_screen are really simple structures, they just contain arrays.

there's an extra include/config.h file that won't be discussed in this entry, we will be using magic numbers instead of macros (mostly for the sake of simplicity).

the main function

in the main function of the emulator (defined in src/main.c) a lot of things are made (because i was lazy and didn't divide it into different functions), let's go in order of what happens there:

  • we get the arguments that are passed to the program so that the rom can be loaded. basically, the emulator needs to be executed like this: ./chip8 rom.ch8
  • we open the rom, read the entirety of it, and store it into a temporal variable called buf
  • we then call a function chip8_init which just memsets the entire struct chip8 to zeros, and copies the default character set (defined in src/chip8.c) to the chip8->memory.memory variable
  • the rom that we read before is loaded into the chip8 memory using the function chip8_load, which will memcpy the rom to chip8->memory.memory + 0x200, and sets the value of chip8.registers.PC to 0x200 as well
  • sdl is initialised, and a window is created. the size of the window is 640x320, it's multiplied by 10, as 64x32 is incredibly small.
  • we enter the main loop

    • we check for key presses at the beginning with the help of sdl functions, and update our virtual keyboard using functions defined in src/keyboard.c, we just map a physical key to our virtual keyboard map, and set whether it's down or up
    • we clear the screen, and set the render colour to white
    • we go through each of the pixels in schip8.screen.screen and draw a 10x10 rect in the actual sdl display
    • we then check for the DT and ST registers, if DT is set, sleep for about 100 milliseconds, and decrease 1 to the DT register.
    • we get the opcode in the memory of the chip8 memory that's in position chip8.registers.PC, we increment chip8.registers.PC by 2, and we finally execute it.
  • when we are out of the main loop, we destroy the sdl window.

executing instructions

there's a function that gets called in every iteration of the main loop called chip8_exec (defined in src/chip8.c) that receives as arguments the chip8 structure, and the opcode that's read from the memory positions 0x200 + PC.

this function is a huge collection of switches and nested switches that test for all the possible instructions that can be passed to the emulator. the instructions can look like this:

  • 0nnn
  • 3xkk
  • 5xy0
  • Dxyn

notice the letters n, nnn, kk, x and y. these letters, all mean different things:

  • nnn - a 12 bit value, i.e. 0x0fff
  • n (nibble) - a 4 bit value, i.e. 0x000f
  • x - a 4 bit value
  • y - a 4 bit value
  • kk (byte) - an 8 bit value, i.e 0x00ff

now, using as example the instructions shown before, they can be transformed into this:

  • 0nnn -> 0fff where nnn is fff
  • 3xkk -> 30ac where x is 0 and kk is ac
  • 5xy0 -> 5100 where x is 1 and y is 0
  • Dxyn -> Dae0 where x is a, y is e and n is 0

knowing that, we need to extract 5 things from each opcode that's passed to the execute function for us to interpret them, we need to know the operation (the most significant 4 bits of the opcode), nnn, x, y, kk, and n. we can do this with bitwise operations. let's go through each one of them (let's consider we have a variable opcode with the 2 byte instruction):

  unsigned short nnn = opcode & 0x0fff;

with that we will obtain the lowest 12 bits of the opcode.

  unsigned char x = (opcode >> 8) & 0x000f;

with that we will obtain the value of x, we will first shift it 8 bits, and then and it with 0x000f.

  unsigned char y = (opcode >> 4) & 0x000f;

this is pretty similar to obtaining x, we are shifting opcode 4 bits, and then and it with 0x000f

  unsigned char kk = opcode & 0x00ff;
  unsigned char n = opcode & 0x000f;

we can do it that way because these elements will always be in the same position, that's why we shift x by 8 bits, y by 4, and so on.

now, there are three functions in my src/chip8.c that execute opcodes:

  • chip8_exec - this one executes instructions so simple that don't need to extract any of the elements shown before
  • chip8_exec_extended - this one execute instructions that need to extract the previously mentioned elements
  • chip8_exec_extended_F - this one exists because there are soooo many instructions that start with F and its behaviour is defined by the last byte, for example: Fx07, Fx0a, Fx15, and many more.

the first function that is executed is chip8_exec, that has a switch like this:

  switch (opcode)
    {
      /* cases go here */

    default:
      chip8_exec_extended (chip8, opcode);
      break;
    }

that code will execute some very simple instructions (only 0x00E0 and 0x00EE), and if the opcode that was passed is none of that, it will now call chip8_exec_extended, which has a switch like this:

  switch (opcode & 0xf000)
    {
      /* cases go here */

    case 0xF000:
      chip8_exec_extended_F (chip8, opcode);
      break;
    }

in that block of code, we are switching for the most significant 4 bits of the opcode (meaning that they can only be values from 0 - F), and we compare them like 0x1000, 0x2000, 0x3000, and so on.

and finally, the chip8_exec_extended_F, as we already know that they start with F, we only need to compare the least significant 4 bits:

  switch (opcode & 0x000f)
    {
      /* cases go here */
    }

knowing how all the instructions are compared, we can now go through the instruction set of the chip8.

the instruction set

in the chip8_exec there are only three cases:

0x00E0

this instruction clears the screen. so it literally just does this:

  case 0x00E0:
    chip8_screen_clear (&chip8->screen);
    break;
0x00EE

this instruction is executed whenever we are finished with a subroutine and we want to return to the position where we called it, so we set the value of PC to the last element of the stack.

  case 0x00EE:
    chip8->registers.PC = chip8_stack_pop (chip8);
    break;

after that, we start checking opcodes in the chip8_exec_extended which compares for nnnn, n, k, x, y

0x1nnn

this instruction will jump to location nnn

  case 0x1000:
    chip8->registers.PC = nnn;
    break;
0x2nnn

this instruction will call subroutine at location nnn

  chip8_stack_push (chip8, chip8->registers.PC);
  chip8->registers.PC = nnn;
0x3xkk

this instruction will skip the next instruction if the register Vx = kk

  if (chip8->registers.V[x] == kk)
    chip8->registers.PC += 2;
0x4xkk

the next instruction will be skipped if the register Vx != kk

  if (chip8->registers.V[x] != kk)
    chip8->registers.PC += 2;
0x5xy0

the next instruction will be skipped if the register Vx = Vy

  if (chip8->registers.V[x] == chip8->registers.V[y])
    chip8->registers.PC += 2;
0x6xkk

register Vx = kk

  chip8->registers.V[x] = kk;
0x7xkk

adds kk to register Vx

  chip8->registers.V[x] += kk;
0x8xy0

sets the value of Vx = Vy

  chip8->registers.V[x] = chip8->registers.V[y];
0x8xy1

ors Vx and Vy, stores the result in Vy

  chip8->registers.V[x] |= chip8->registers.V[y];
0x8xy2

ands Vx and Vy, stores the result in Vx

  chip8->registers.V[x] &= chip8->registers.V[y];
0x8xy3

xors Vx and Vy, stores the result in Vx

  chip8->registers.V[x] ^= chip8->registers.V[y];
0x8xy4

this will add Vx and Vy, will set VF if the result is greater than 1 byte

  chip8->registers.V[0x0f] = chip8->registers.V[x] + chip8->registers.V[y] > 0xff;
  chip8->registers.V[x] += chip8->registers.V[y];
0x8xy5

this will sub Vx and Vy, will set VF if borrow is not necessary

  chip8->registers.V[0x0f] = chip8->registers.V[x] > chip8->registers.V[y];
  chip8->registers.V[x] -= chip8->registers.V[y];
0x8xy6

if the least significant bit of Vx is 1, VF will be set, Vx will be divided by 2

  chip8->registers.V[0x0f] = chip8->registers.V[x] & 0x01;
  chip8->registers.V[x] = chip8->registers.V[x] / 2;
0x8xy7

sets Vx = Vy - Vx, will set VF if borrow is not necessary

  chip8->registers.V[0x0f] = chip8->registers.V[y] > chip8->registers.V[x];
  chip8->registers.V[x] = chip8->registers.V[y] - chip8->registers.V[x];
0x8xyE

will set VF if the most significant bit of Vx is 1, and will multiply Vx by 2

  chip8->registers.V[0x0f] = chip8->registers.V[x] & 0b10000000;
  chip8->registers.V[x] = chip8->registers.V[x] * 2;
0x9xy0

skips next instruction if Vx != Vy

  if (chip8->registers.V[x] != chip8->registers.V[y])
    chip8->registers.PC += 2;
0xAnnn

sets the I register to nnn

  chip8->registers.I = nnn;
0xBnnn

jump to location nnn + V0

  chip8->registers.PC = nnn + chip8->registers.V[0x00];
0xCxkk:

will set Vx to a random byte and will and it with kk

  srand (clock ());
  chip8->registers.V[x] = (rand () % 255) & kk;
0xDxyn

will draw the sprite to which I is pointing, at position Vx, Vy with a height of

  1. VF will be set if there's a collision, basically, if we hit a pixel that has

already been marked as 1

  const char *sprite = (const char *)&chip8->memory.memory[chip8->registers.I];
  chip8->registers.V[0x0f] = chip8_screen_draw_sprite (&chip8->screen,
                                                       chip8->registers.V[x],
                                                       chip8->registers.V[y],
                                                       sprite, n);
0xEx9e

skip the next instruction if the key Vx is pressed

  if (chip8_keyboard_is_down (&chip8->keyboard, chip8->registers.V[x]))
    {
      chip8->registers.PC += 2;
    }
0xExa1

skip the next instruction if the key Vx is not pressed

  if (!chip8_keyboard_is_down (&chip8->keyboard, chip8->registers.V[x]))
    {
      chip8->registers.PC += 2;
    }
0xFx07

the value of Vx will be set to the value of the delay timer

  chip8->registers.V[x] = chip8->registers.DT;
0xFx0a

the program will completely stop until a key is pressed, and we will store the value of that key in Vx

  char key = chip8_wait_for_keypress (chip8);
  chip8->registers.V[x] = key;
0xFx18

this will set the sound timer to Vx

  chip8->registers.ST = chip8->registers.V[x];
0xFx1e

this will add Vx to I

  chip8->registers.I = chip8->registers.I + chip8->registers.V[x];
0xF29

this will set I to the value of Vx

  chip8->registers.I = chip8->registers.V[x] * CHIP8_SPRITE_DEFAULT_HEIGHT;
0xFx33

this will take a number stored in Vx, and will set the value of I to the hundreds, I + 1 to the tens, and 1 + 2 to the units of that number.

  unsigned char hundreds = chip8->registers.V[x] / 100;
  unsigned char tens = (chip8->registers.V[x] / 10) % 10;
  unsigned char units = chip8->registers.V[x] % 10;
  chip8_memory_set (&chip8->memory, chip8->registers.I, hundreds);
  chip8_memory_set (&chip8->memory, chip8->registers.I + 1, tens);
  chip8_memory_set (&chip8->memory, chip8->registers.I + 2, units);
0xFx55

store registers V0 through Vx in memory starting in location I

  for (int i = 0; i <= x; i++)
    {
      chip8_memory_set (&chip8->memory, chip8->registers.I + i,
                        chip8->registers.V[x]);
    }
0xFx65

read registers V0 through Vx from memory starting at location I

  for (int i = 0; i <= x; i++)
    {
      chip8->registers.V[i] = chip8_memory_get (&chip8->memory,
                                                chip8->registers.I + i);
    }

other things

if you implemented all those instructions, and the hardware mentioned before, you should now have a completely working chip8 emulator. the references in the code in this entry might change, as i also have planned to write an assembler, disassembler and debugger for the chip8, so the folder structure and filenames might change later.

if you want to look at the code in the same state as it was when this entry was written, you can refer to this commit, and check the code there.

when i am finished with the assembler, the disassembler, and the debugger, i will also write entries like this one for all those projects, i think they are really fun and teach a lot of stuff too.

i really hope this entry helps you, and motivates you to write your own emulators, or to get started in emulator dev. now it's your time to go ahead and implement the project by yourself (▰˘◡˘▰)

⮐ back to blog