Now that we have the required stubs in place, it’s time to implement some opcodes! For reference, those are available here.

Clear Display - 0x00e0

This will be the first instruction we’ll implement because it’s relatively simple (it doesn’t require being split in various ways)x. Our processOp function is an empty stub:

void processOp(State *state, uint8_t memory[])
{
}

So it’ll do, well, nothing. Which is perfect! Let’s write a test for the functionality first. In test/test_a.c we can start with the below. There’s a bit of set up (and oops - pc needs to be 16 bits wide, not 8) required:

static void test_clear_display(void **state)
{
    /*
    Let's assume we have some pixels set in the display section
    Calling 0x00e0 should zero those out
    */

    // init
    State chip8State = {.pc = ROM_OFFSET};
    uint8_t memory[MEM_SIZE];
    memset(memory, 0x0, MEM_SIZE*sizeof(uint8_t));
    // let's turn on some pixels
    memset(memory + MEM_DISPLAY_START, 0xa, 256*sizeof(uint8_t));
    // add a "clear display" instruction
    memory[ROM_OFFSET] = 0x0;
    memory[ROM_OFFSET+1] = 0xe0;
    // process
    processOp(&chip8State, memory);
    // validate
    uint8_t expected[256];
    memset(expected, 0, 256*sizeof(uint8_t));
    // ensure we have incremented the program counter to point to the next instruction
    assert_int_equal(ROM_OFFSET+2, chip8State.pc);
    // we should set the draw flag since we (presumably) modified the pixels
    assert_true(chip8State.draw);
    // and that the memory is indeed zero
    assert_memory_equal(memory+MEM_DISPLAY_START, expected, 256);
}

And running it fails - as expected (never trust a test you haven’t seen fail - ever):

❯ ./bin/test_a
[==========] Running 1 test(s).
[ RUN      ] test_clear_display
[  ERROR   ] --- 0x202 != 0x200
[   LINE   ] --- /home/axiomiety/repos/fish8/test/test_a.c:30: error: Failure!
[  FAILED  ] test_clear_display
[==========] 1 test(s) run.
[  PASSED  ] 0 test(s).
[  FAILED  ] 1 test(s), listed below:
[  FAILED  ] test_clear_display

 1 FAILED TEST(S)

Now let’s fix this one failure at a time.

First, we need to switch on the opcode. Generally speaking, the first 4 bits indicate the “type” of the code so let’s split accordingly:

void processOp(State *state, uint8_t memory[])
{
    // memory is byte-addressable, but opcodes are 2-bytes long
    // for simplicity, we break this as such:
    // 0x0123
    // opCodeLeft = 0x01
    // opcodeRight = 0x23
    // opCodeA,B,C,D = 0x0, 0x1, 0x2, 0x3
    uint8_t opCodeLeft, opCodeRight, opCodeA, opCodeB, opCodeC, opCodeD;
    opCodeLeft = memory[state->pc];
    opCodeRight = memory[state->pc + 1];
    opCodeA = opCodeLeft >> 4;
    opCodeB = opCodeLeft & 0x0f;
    opCodeC = opCodeRight >> 4;
    opCodeD = opCodeRight & 0x0f;
    bool error = false;
    SDL_Log("Decoding %02x%02x (A:%x, B:%x, C:%x, D:%x)", opCodeLeft, opCodeRight, opCodeA, opCodeB, opCodeC, opCodeD);
    switch (opCodeA)
    {
    case (0x0):
    {
        switch (opCodeB)
        {
        case (0x0):
        {
            switch (opCodeRight)
            {
            case (0xe0):
                clearDisplay(state, memory);
                break;
            default:
                error = true;
                break;
            }
        }
        break;
        default:
            error = true;
            break;
        }
    }
    break;
    default:
        error = true;
        break;
    }
    if (error)
    {
        // we could do a bit more like dumping the state/memory
        SDL_Log("Unknown/unimplemented opcode %x%x", opCodeLeft, opCodeRight);
        exit(1);
    }
}

And let’s make sure clearDisplay increments the program counter:

void clearDisplay(State *state, uint8_t memory[])
{
    state->pc += 2;
}

Re-running the tests, we now get:

[==========] Running 1 test(s).
[ RUN      ] test_clear_display
INFO: Decoding 00e0 (A:0, B:0, C:e, D:0)
[  ERROR   ] --- chip8State.draw
[   LINE   ] --- /home/axiomiety/repos/fish8/test/test_a.c:32: error: Failure!
[  FAILED  ] test_clear_display
[==========] 1 test(s) run.
[  PASSED  ] 0 test(s).
[  FAILED  ] 1 test(s), listed below:
[  FAILED  ] test_clear_display

 1 FAILED TEST(S)

Progress! We forgot to set the draw flag - easy fix.

void clearDisplay(State *state, uint8_t memory[])
{
    state->draw = true;
    state->pc += 2;
}

Lather, rinse, repeat:

[==========] Running 1 test(s).
[ RUN      ] test_clear_display
INFO: Decoding 00e0 (A:0, B:0, C:e, D:0)
[  ERROR   ] --- difference at offset 0 0x0a 0x00
difference at offset 1 0x0a 0x00
difference at offset 2 0x0a 0x00
difference at offset 3 0x0a 0x00
difference at offset 4 0x0a 0x00
difference at offset 5 0x0a 0x00
difference at offset 6 0x0a 0x00
difference at offset 7 0x0a 0x00
difference at offset 8 0x0a 0x00
difference at offset 9 0x0a 0x00
difference at offset 10 0x0a 0x00
difference at offset 11 0x0a 0x00
difference at offset 12 0x0a 0x00
difference at offset 13 0x0a 0x00
difference at offset 14 0x0a 0x00
difference at offset 15 0x0a 0x00
...
256 bytes of 0x7ffe04c81ea0 and 0x7ffe04c80ea0 differ
[   LINE   ] --- /home/axiomiety/repos/fish8/test/test_a.c:34: error: Failure!
[  FAILED  ] test_clear_display
[==========] 1 test(s) run.
[  PASSED  ] 0 test(s).
[  FAILED  ] 1 test(s), listed below:
[  FAILED  ] test_clear_display

 1 FAILED TEST(S)

And finally, let’s reset all those pixels:

void clearDisplay(State *state, uint8_t memory[])
{
    memset(memory+MEM_DISPLAY_START, 0, 256*sizeof(uint8_t));
    state->draw = true;
    state->pc += 2;
}

And voila:

[==========] Running 1 test(s).
[ RUN      ] test_clear_display
INFO: Decoding 00e0 (A:0, B:0, C:e, D:0)
[       OK ] test_clear_display
[==========] 1 test(s) run.
[  PASSED  ] 1 test(s).

Flow control

With an instruction under out belt, let’s tackle flow control. This is what happens when we either call a subroutine or perform a jump. As with the previous one, let’s start with a test to ensure we understand the expected behaviour.

static void test_flow(void **state)
{
    /*
    When performing a call, we need to:
    - push the next address on the stack
    - increment the stack pointer
    - set the PC to the address of the subroutine
    Returning is essentially those in reverse:
    - decrement the stack pointer
    - load the address pointed to by the stack pointer into the PC

    The test ROM will look like this
        0x0200  0x00e0 # clear the screen
        0x0202  0x00ee # return
        0x0204  0x2200 # call the subroutine at 0x0200
        0x0206  0x1204 # jump to 0x204

    So after processing 4 operations, we should have PC set back to 0x0204
    */

    // here we're starting at the jump instruction
    State chip8State = {.pc = ROM_OFFSET + 4};
    uint8_t memory[MEM_SIZE];
    memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
    uint8_t rom[] = {0x0, 0xe0, 0x0, 0xee, 0x22, 0x00, 0x12, 0x04};
    memcpy(memory + ROM_OFFSET, rom, sizeof(rom));

    // process the jump
    processOp(&chip8State, memory);
    assert_int_equal(ROM_OFFSET, chip8State.pc);
    // we should have incremented our stack pointer by 1
    assert_int_equal(1, chip8State.sp);
    // and the first entry should be the return address (the one after the call)
    assert_int_equal(ROM_OFFSET + 6, chip8State.stack[0]);
    // clearing the screen - we test this elswhere
    processOp(&chip8State, memory);
    assert_int_equal(ROM_OFFSET + 2, chip8State.pc);
    // the return
    processOp(&chip8State, memory);
    // validate the return address is set correctly
    assert_int_equal(ROM_OFFSET + 6, chip8State.pc);
    // and that our stack pointer is back to 0
    assert_int_equal(0, chip8State.sp);
    // the unconditional jump
    processOp(&chip8State, memory);
    // we should have updated the program counter
    assert_int_equal(ROM_OFFSET + 4, chip8State.pc);
    // no change to the stack - this isn't a subroutine call
    assert_int_equal(0, chip8State.sp);
}

This is a bit lengthly but it’s also very explicit. We handcraft our test ROM and use memcpy to copy this into memory - which is just what is described in the comments but split in bytes.

Let’s run our tests:

[==========] Running 2 test(s).
[ RUN      ] test_clear_display
INFO: Decoding 00e0 (A:0, B:0, C:e, D:0)
[       OK ] test_clear_display
[ RUN      ] test_flow
INFO: Decoding 2200 (A:2, B:2, C:0, D:0)
INFO: Unknown/unimplemented opcode 2200

We know what to do next! For brievty I won’t bother posting the output of the tests after each op code implementation but it’s strongly recommended you do this if you’re following along.

0x2NNN

This one is interesting because we need to push the return address, which is a PC+2, onto our in-memory “stack”:

void callSubroutine(State *state, uint8_t opCodeB, uint8_t opCodeRight) {
    // we need to combine opCodeB and opCodeRight to form a 12-bit address
    uint16_t address = (opCodeB << 8) | opCodeRight;
    // push the return address onto the stack
    state->stack[state->sp] = state->pc + 2;
    state->sp += 1;
    state->pc = address;
}

We also had to combine each of the Ns (technically the left-most and the right pair) into a single address.

0x00ee

The other (but no less important) half of the instruction above, the return - which decrements the stack pointer, fetches whatever address we had stored in there and pushes it into PC:

void returnFromSubroutine(State *state) {
    state->sp -= 1;
    state->pc = state->stack[state->sp];
}

0x1NNN

The easiest one for last, tackling the unconditional jump:

void jumpToAddress(State *state, uint8_t opCodeB, uint8_t opCodeRight) {
    // we need to combine opCodeB and opCodeRight to form a 12-bit address
    uint16_t address = (opCodeB << 8) | opCodeRight;
    state->pc = address;
}

Putting it together

The main switch block of processOpCode now looks like this:

    switch (opCodeA)
    {
    case (0x0):
    {
        switch (opCodeB)
        {
        case (0x0):
        {
            switch (opCodeRight)
            {
            case (0xe0):
                clearDisplay(state, memory);
                break;
            case (0xee):
                returnFromSubroutine(state);
                break;
            default:
                error = true;
                break;
            }
        }
        break;
        default:
            error = true;
            break;
        }
    }
    break;
    case (0x1):
        jumpToAddress(state, opCodeB, opCodeRight);
        break;
    case (0x2):
        callSubroutine(state, opCodeB, opCodeRight);
        break;
    default:
        error = true;
        break;
    }
    if (error)
    {
        // we could do a bit more like dumping the state/memory
        SDL_Log("Unknown/unimplemented opcode %02x%02x", opCodeLeft, opCodeRight);
        exit(1);
    }

Let’s run our tests:

[==========] Running 2 test(s).
[ RUN      ] test_clear_display
INFO: Decoding 00e0 (A:0, B:0, C:e, D:0)
[       OK ] test_clear_display
[ RUN      ] test_flow
INFO: Decoding 2200 (A:2, B:2, C:0, D:0)
INFO: Decoding 00e0 (A:0, B:0, C:e, D:0)
INFO: Decoding 00ee (A:0, B:0, C:e, D:e)
INFO: Decoding 1204 (A:1, B:2, C:0, D:4)
[       OK ] test_flow
[==========] 2 test(s) run.
[  PASSED  ] 2 test(s).

Success!

Conclusion

That’s 4 op codes done! We can’t run anything meaningful with it yet, but it already gives us an idea as to how to implement the rest - see you in part 5.