CHIP-8 Emulator

The defintion of the CHIP-8 language was tailored to the original hardware that it was used for.

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.

BCD

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.

Timers

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    
    }    
   }    

Keyboard

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 →