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.
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 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 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|
+----+--------+----+ +----+--------+----+
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:
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 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 |
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.
we will take as a reference my code (here)
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).
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:
./chip8
rom.ch8
buf
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
variablechip8_load
, which will memcpy the rom to chip8->memory.memory + 0x200
, and
sets the value of chip8.registers.PC
to 0x200 as wellwe enter the main loop
src/keyboard.c
,
we just map a physical key to our virtual keyboard map, and set whether it's
down or upschip8.screen.screen
and draw a 10x10
rect in the actual sdl displaychip8.registers.PC
, we increment chip8.registers.PC
by 2, and we finally
execute it.
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. 0x0fffn
(nibble) - a 4 bit value, i.e. 0x000fx
- a 4 bit valuey
- a 4 bit valuekk
(byte) - an 8 bit value, i.e 0x00ffnow, 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 beforechip8_exec_extended
- this one execute instructions that need to extract the
previously mentioned elementschip8_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.
in the chip8_exec
there are only three cases:
this instruction clears the screen. so it literally just does this:
case 0x00E0:
chip8_screen_clear (&chip8->screen);
break;
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
this instruction will jump to location nnn
case 0x1000:
chip8->registers.PC = nnn;
break;
this instruction will call subroutine at location nnn
chip8_stack_push (chip8, chip8->registers.PC);
chip8->registers.PC = nnn;
this instruction will skip the next instruction if the register Vx = kk
if (chip8->registers.V[x] == kk)
chip8->registers.PC += 2;
the next instruction will be skipped if the register Vx != kk
if (chip8->registers.V[x] != kk)
chip8->registers.PC += 2;
the next instruction will be skipped if the register Vx = Vy
if (chip8->registers.V[x] == chip8->registers.V[y])
chip8->registers.PC += 2;
register Vx = kk
chip8->registers.V[x] = kk;
adds kk to register Vx
chip8->registers.V[x] += kk;
sets the value of Vx = Vy
chip8->registers.V[x] = chip8->registers.V[y];
ors Vx and Vy, stores the result in Vy
chip8->registers.V[x] |= chip8->registers.V[y];
ands Vx and Vy, stores the result in Vx
chip8->registers.V[x] &= chip8->registers.V[y];
xors Vx and Vy, stores the result in Vx
chip8->registers.V[x] ^= chip8->registers.V[y];
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];
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];
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;
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];
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;
skips next instruction if Vx != Vy
if (chip8->registers.V[x] != chip8->registers.V[y])
chip8->registers.PC += 2;
sets the I register to nnn
chip8->registers.I = nnn;
jump to location nnn + V0
chip8->registers.PC = nnn + chip8->registers.V[0x00];
will set Vx to a random byte and will and it with kk
srand (clock ());
chip8->registers.V[x] = (rand () % 255) & kk;
will draw the sprite to which I is pointing, at position Vx, Vy with a height of
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);
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;
}
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;
}
the value of Vx will be set to the value of the delay timer
chip8->registers.V[x] = chip8->registers.DT;
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;
this will set the sound timer to Vx
chip8->registers.ST = chip8->registers.V[x];
this will add Vx to I
chip8->registers.I = chip8->registers.I + chip8->registers.V[x];
this will set I to the value of Vx
chip8->registers.I = chip8->registers.V[x] * CHIP8_SPRITE_DEFAULT_HEIGHT;
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);
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]);
}
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);
}
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 (▰˘◡˘▰)