The defintion of the CHIP-8 language was tailored to the original hardware that it was used for.
4K RAM - by convention programs start in RAM at 0x200
Display is 64x32 (1 bit per pixel).
Display buffer is in RAM at 0xF00.
Stack is at 0xEA0.
16 8-bit registers named V0, V1, V2, ... VF
A memory address register called I
Has stack instructions (so needs an SP)
2 timers, one for delay, and one for sound
I'll define a data structure to handle the state definition, and a function to allocate one:
typedef struct Chip8State { uint8_t V[16]; uint16_t I; uint16_t SP; uint16_t PC; uint8_t delay; uint8_t sound; uint8_t *memory; uint8_t *screen; //this is memory[0xF00]; } Chip8State; Chip8State* InitChip8(void) { Chip8State* s = calloc(sizeof(Chip8State), 1); s->memory = calloc(1024*4, 1); s->screen = &s->memory[0xf00]; s->SP = 0xfa0; s->PC = 0x200; return s; }
I'll make a shell routine for the opcodes, and call "Unimplemented" for each opcode. I'll also implement a few opcodes here to get a flavor.
void EmulateChip8Op(Chip8State *state) { uint8_t *op = &state->memory[state->PC]; int highnib = (*op & 0xf0) >> 4; switch (highnib) { case 0x00: UnimplementedInstruction(state); break; case 0x01: //JUMP $NNN { uint16_t target = ((code[0]&0xf)<<8) | code[1]; state->PC = target; } break; case 0x02: UnimplementedInstruction(state); break; case 0x03: //SKIP.EQ VX,#$NN { uint8_t reg = code[0] & 0xf; if (state->V[reg] == code[1]) state->PC+=2; state->PC+=2; } break; case 0x04: UnimplementedInstruction(state); break; case 0x05: UnimplementedInstruction(state); break; case 0x06: //MOV VX,#$NN { uint8_t reg = code[0] & 0xf; state->V[reg] = code[1]; state->PC+=2; } break; case 0x07: UnimplementedInstruction(state); break; case 0x08: UnimplementedInstruction(state); break; case 0x09: UnimplementedInstruction(state); break; case 0x0a: //MOV I, #$NNN { state->I = ((code[0] & 0xf)<<8) | code[1]; state->PC+=2; } break; case 0x0b: UnimplementedInstruction(state); break; case 0x0c: UnimplementedInstruction(state); break; case 0x0d: UnimplementedInstruction(state); break; case 0x0e: UnimplementedInstruction(state); break; case 0x0f: UnimplementedInstruction(state); break; } }
Most of the instructions are straighforward, but there are several that deserve special coverage. Let's talk about some here, then cover sprites in a separate section.
Opcode FX33 converts the number in register X into 3 Binary-coded decimal (BCD) digits, storing them into the memory location pointed to by the I register. Humans like to read base-10 numbers, and this instruction is convenient to convert hex numbers to base-10 for display. Here is one possible way to do it:
case 0x33: //BCD MOV { int reg = code[0]&0xf; uint8_t ones, tens, hundreds; uint8_t value=state->V[reg]; ones = value % 10; value = value / 10; tens = value % 10; hundreds = value / 10; state->memory[state->I] = hundreds; state->memory[state->I+1] = tens; state->memory[state->I+2] = ones; } break;
A CHIP-8 program can combine this with the built-in font handling to present numbers in human readable form.
The timers are defined to count down by 60 counts per second. Since the timer is tied to clock time, it will have to be tied to the platform code somehow. However, we can implement all the logic for the instructions, the timer just won't advance until we do something in the platform code. The 3 timer instructions are all in the Fxxx opcode space. Their implementation is straightforward:
static void OpF(Chip8State *state, uint8_t *code) { int reg = code[0]&0xf; switch (code[1]) { case 0x07: state->V[reg] = state->delay; break; //MOV VX, DELAY case 0x15: state->delay = state->V[reg]; break; //MOV DELAY, VX case 0x18: state->sound = state->V[reg]; break; //MOV SOUND, VX } }
The machines that were the target for CHIP-8 had 16 key keyboards, one key for each hexadecimal digit. I'll make an array of 16 flags to indicate which key is down. It is up to the platform code to set the values in this array.
There are three keyboard opcodes. Two of them just check to see if a key is down and possibly skip the next instruction, those are easy to implement:
static void OpE(Chip8State *state, uint8_t *code) { int reg = code[0]&0xf; switch (code[1]) { case 0x9e: //SKIP.KEY VX //Skips the next instruction if the key stored in VX is down if (state->key_state[state->V[reg]] != 0) state->PC+=2; break; case 0xa1: //SKIP.NOKEY VX //Skips the next instruction if the key stored in VX us up if (state->key_state[state->V[reg]] == 0) state->PC+=2; break; default: UnimplementedInstruction(state); break; } state->PC+=2; }
The third (Opcode FX0A, or "KEY VX") is a little more difficult, it is supposed to wait until a key is pressed. I'll accomplish that with this algorithm:
if (not waiting_key)
waiting_key = yes
save_key_flags = key_flags
stay on KEY instruction; return without advancing PC
else
if a key has been pressed
move which key into VX
waiting_key = no
done with KEY; advance PC and return
else
stay on KEY instruction; return without advancing PC
The emulator code will not advance the PC until a key is pressed. C Code for this will be in the final emulator.
← Prev: chip-8-instruction-set Next: chip-8-sprites →Post questions or comments on Twitter @realemulator101, or if you find issues in the code, file them on the github repository.