Continuing from the previous post, we’ll implement more of the codes defined here.

Setting registers - 0x6xnn, 0x7xnn

Those 2 operations are relatively straight forward. The only “edge” case is what happens when the sum instruction 0x7 results in a number greater to 0xff. Let’s illustrate with a test:

static void test_const(void **state)
{
    /*
    The test ROM will look like this:
        0x0200 0x6001 # set register 0 to 0x1
        0x0202 0x6cab # set register c to to 0xab
        0x0204 0x70ff # add 0xff to register 0 - carry flag remains unchanged
    */

    // init
    State chip8State = {.pc = ROM_OFFSET};
    uint8_t memory[MEM_SIZE];
    memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
    uint8_t rom[] = {0x60, 0x01, 0x6c, 0xab, 0x70, 0xff};
    memcpy(memory + ROM_OFFSET, rom, sizeof(rom));

    // let's make sure our registers are all 0
    for (int i=0l;i < 0xf; i++){
        assert_int_equal(chip8State.registers[i], 0);
    }
    processOp(&chip8State, memory);
    // register 0 should be set to 1
    assert_int_equal(chip8State.registers[0], 0x1);
    processOp(&chip8State, memory);
    // register c should be set to 0xab
    assert_int_equal(chip8State.registers[0xc], 0xab);
    processOp(&chip8State, memory);
    // and adding such that it overflows discards any other digits
    assert_int_equal(chip8State.registers[0], 0x0);
    // and the carry flag hasn't changed
    assert_int_equal(chip8State.registers[0xf], 0x0);
}

Running the test gives us INFO: Unknown/unimplemented opcode 6001, so let’s add those to processOp’s growing switch statement:

    case (0x6):
        setRegister(state, opCodeB, opCodeRight);
        break;
    case (0x7):
        addToRegister(state, opCodeB, opCodeRight);
        break;

where:

void setRegister(State *state, uint8_t reg, uint8_t opCodeRight) {
    // reg is a byte long, but we only care for the last 4 bits
    state->registers[reg] = opCodeRight;
    state->pc += 2;
}
void addToRegister(State *state, uint8_t reg, uint8_t opCodeRight) {
    // reg is a byte long, but we only care for the last 4 bits
    state->registers[reg] = (state->registers[reg] + opCodeRight) & 0xff;
    state->pc += 2;
}

And the test passes first time:

...
[ RUN      ] test_const
INFO: Decoding 6001 (A:6, B:0, C:0, D:1)
INFO: Decoding 6cab (A:6, B:c, C:a, D:b)
INFO: Decoding 70ff (A:7, B:0, C:f, D:f)
[       OK ] test_const
[==========] 3 test(s) run.
[  PASSED  ] 3 test(s).

Conditionals - 0x3xnn, 0x4xnn, 0x5xy0, 0x9xy0

All 3 instructions essentially skip the next instruction (so we increment our program counter by 4 bytes instead of 2) if they evaluate to true. The first 2 compare registers to scalar values and the last one compares 2 registers.

As usualy, let’s start with a test:

static void test_cond(void **state)
{
    /*
    The test ROM will look like this:
        0x0200 0x31ab # compare register 1 to 0xab, jump over if equal to 0xab
        0x0202 0x61ab # set register 1 to 0xab
        0x0204 0x41ab # compare register 1 to 0xab, jump over if not equal to 0xab
        0x0206 0x62ab # set register 2 to 0xab
        0x0208 0x5100 # compare register 1 to register 0, jump over if equal
        0x020a 0x9120 # compare register 1 to register 2, jump over if equal
    */

    // init
    State chip8State = {.pc = ROM_OFFSET};
    uint8_t memory[MEM_SIZE];
    memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
    uint8_t rom[] = {0x31, 0xab, 0x61, 0xab, 0x41, 0xab, 0x62, 0xab, 0x51, 0x0, 0x91, 0x20};
    memcpy(memory + ROM_OFFSET, rom, sizeof(rom));

    processOp(&chip8State, memory);
    // we should not have skipped over 0x0202
    assert_int_equal(chip8State.pc, 0x202);
    // set register 1 to 0xab
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.pc, 0x204);
    // compare
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.pc, 0x206);
    // set register 2 to 0xab
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.pc, 0x208);
    // compare register 1 and register 0, jump if equal
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.pc, 0x20a);
    // compare register 1 and register 2, jump if not equal
    processOp(&chip8State, memory);
    // they're both the same so we should just be at the next isntruction
    assert_int_equal(chip8State.pc, 0x20c);
}

The jump instructions are pretty straight forward to implement:

void jumpIfRegEqualToConst(State *state, uint8_t reg, uint8_t value)
{
    state->pc += (state->registers[reg] == value) ? 4 : 2;
}
void jumpIfRegNotEqualToConst(State *state, uint8_t reg, uint8_t value)
{
    state->pc += (state->registers[reg] != value) ? 4 : 2;
}
void jumpIfRegEqualToReg(State *state, uint8_t reg1, uint8_t reg2)
{
    state->pc += (state->registers[reg1] == state->registers[reg2]) ? 4 : 2;
}
void jumpIfRegNotEqualToReg(State *state, uint8_t reg1, uint8_t reg2)
{
    state->pc += (state->registers[reg1] != state->registers[reg2]) ? 4 : 2;
}
void setRegisterToRegister(State *state, uint8_t reg1, uint8_t reg2)
{
    state->registers[reg2] = state->registers[reg1];
    state->pc += 2;
}

We just need to make sure than when passing the 2nd argument for 0x5xy0 we pass in y and not y0:

    case (0x3):
        jumpIfRegEqualToConst(state, opCodeB, opCodeRight);
        break;
    case (0x4):
        jumpIfRegNotEqualToConst(state, opCodeB, opCodeRight);
        break;
    case (0x5):
        jumpIfRegEqualToReg(state, opCodeB, opCodeC);
        break;
    case (0x9):
        jumpIfRegNotEqualToReg(state, opCodeB, opCodeC);
        break;

Assignment - 0x8xy0

The test is quite simple:

static void test_assign(void **state)
{
    /*
    The test ROM will look like this:
        0x0200 0x61ab # set register 1 to 0xab
        0x0202 0x8120 # set register 2 to the same value as register 1
        0x0204 0x5120 # compare regsiter 1 and 2, jump over if equal
    */

    // init
    State chip8State = {.pc = ROM_OFFSET};
    uint8_t memory[MEM_SIZE];
    memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
    uint8_t rom[] = {0x61, 0xab, 0x81, 0x20, 0x51, 0x20};
    memcpy(memory + ROM_OFFSET, rom, sizeof(rom));

    // make sure both regs are 0 at the start
    assert_int_equal(chip8State.registers[1], 0x0);
    assert_int_equal(chip8State.registers[2], 0x0);
    // assign 0xab to register 1
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.registers[1], 0xab);
    // set register 2 to register 1
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.registers[2], 0xab);
    // compare both registers
    processOp(&chip8State, memory);
    // they should be equal, so the pc should have incremented by 4
    assert_int_equal(chip8State.pc, 0x208);
}

And the same goes for the implementation:

void setRegisterToRegister(State *state, uint8_t reg1, uint8_t reg2)
{
    state->registers[reg2] = state->registers[reg1];
    state->pc += 2;
}

Note that for all 0x8xyz instructions the last 4 bits, z, dictate what happens between registers x and y - so that’s what the nested switch switches on:

    case (0x8):
    {
        switch (opCodeD)
        {
        case (0x0):
            setRegisterToRegister(state, opCodeB, opCodeC);
            break;
        default:
            error = true;
            break;
        }
    }
    break;

Bitwise operations, operators - 0x8xy1, 0x8xy2, 0x8xy3

This being C, it’s straight-forward:

static void test_bitwise_operators(void **state)
{
    /*
    The test ROM will look like this:
        0x0200 0x610f # set register 1 to 0x0f
        0x0202 0x62f0 # set register 2 to 0xf0
        0x0204 0x8121 # set register 1 to r1 | r2
        0x0206 0x8112 # set register 1 to r1 & r1
        0x0208 0x8113 # set register 1 to r1 ^ r1
    */

    // init
    State chip8State = {.pc = ROM_OFFSET};
    uint8_t memory[MEM_SIZE];
    memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
    uint8_t rom[] = {0x61, 0x0f, 0x62, 0xf0, 0x81, 0x21, 0x81, 0x12, 0x81, 0x13};
    memcpy(memory + ROM_OFFSET, rom, sizeof(rom));

    // set the regs
    processOp(&chip8State, memory);
    processOp(&chip8State, memory);
    // |=
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.registers[1], 0xff);
    // &=
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.registers[1], 0xff);
    // ^=
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.registers[1], 0x0);
}

Ditto for the implementation:

void setRegisterToBitwiseOr(State *state, uint8_t reg1, uint8_t reg2)
{
    state->registers[reg1] |= state->registers[reg2];
    state->pc += 2;
}
void setRegisterToBitwiseAnd(State *state, uint8_t reg1, uint8_t reg2)
{
    state->registers[reg1] &= state->registers[reg2];
    state->pc += 2;
}
void setRegisterToBitwiseXor(State *state, uint8_t reg1, uint8_t reg2)
{
    state->registers[reg1] ^= state->registers[reg2];
    state->pc += 2;
}

And the addition to processOp:

    case (0x8):
    {
        switch (opCodeD)
        {
        case (0x0):
            setRegisterToRegister(state, opCodeB, opCodeC);
            break;
        case (0x1):
            setRegisterToBitwiseOr(state, opCodeB, opCodeC);
            break;
        case (0x2):
            setRegisterToBitwiseAnd(state, opCodeB, opCodeC);
            break;
        case (0x3):
            setRegisterToBitwiseXor(state, opCodeB, opCodeC);
            break;
        default:
            error = true;
            break;
        }
    }

Bitwise operations, bit shifting - 0x8xy6, 0x8xye

For this op code, the y value is irrelevant. Some implementations bitshift by the value in register y (so if r2 is 4, 0x812e shifts r1 to the left by 4 bits), but given we’re going with the implementation defined on Wikipedia which shifts by a single bit either way, let’s go with this for now. This is more of a note that certain ROMs might behave somewhat differently depending on the implementation/variant they were written for.

static void test_bitwise_shift(void  **state) {
    /*
    The test ROM will look like this:
        0x0200 0x61f0 # set register 1 to 0xf0
        0x0202 0x810e # store the MSB in register 0xf, left shift by 1
        0x0204 0x6f00 # set regsiter 0xf to 0
        0x0206 0x6201 # set register 1 to 0x01
        0x0208 0x8206 # store the LSB in register 0xf, right shift by 1
    */

    // init
    State chip8State = {.pc = ROM_OFFSET};
    uint8_t memory[MEM_SIZE];
    memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
    uint8_t rom[] = {0x61, 0xf0, 0x81, 0x0e, 0x6f, 0x0, 0x62, 0x01, 0x82, 0x06};
    memcpy(memory + ROM_OFFSET, rom, sizeof(rom));

    processOp(&chip8State, memory);
    // left shift
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.registers[1], 0xe0);
    // the "overflown" bit should be stored in 0xf
    assert_int_equal(chip8State.registers[0xf], 0x1);
    // reset 0xf
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.registers[0xf], 0x0);
    // store 1 in r2
    processOp(&chip8State, memory);
    // bitshift to the right
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.registers[2], 0x0);
    assert_int_equal(chip8State.registers[0xf], 0x1);
}

Implementation:

void leftShift(State *state, uint8_t reg)
{
    uint16_t val = state->registers[reg] << 1;
    state->registers[reg] = val & 0xff;
    state->registers[0xf] = (val & 0x100) >> 8;
    state->pc += 2;
}
void rightShift(State *state, uint8_t reg)
{
    state->registers[0xf] = state->registers[reg] & 0x1;
    state->registers[reg] >>= 1;
    state->pc += 2;
}

And for processOp:

        case (0x6):
            rightShift(state, opCodeB);
            break;
        case (0xe):
            leftShift(state, opCodeB);
            break;

Maths operations on registers - 0x8xy4, 0x8xy5, 0x8xy7

This is essentially how CHIP8 handles carries and borrows for addition/subtraction. Addition is relatively straightforward because we can “overflow” into a larger-sized unsigned integer, but for subtraction we need to check whether the quantity being subtracted is larger than the one being subtracted from. The one thing I found a little confusing was that if a borrow is required, the VF register is set to 0 - and 1 otherwise - whereas for carry it’s the other way (1 if one is required, 0 if not).

The test case is a little long but it covers the carry/borrow use-cases:

static void test_register_maths(void  **state) {
    /*
    The test ROM will look like this:
        0x0200 0x61f0 # set register 1 to 0xf0
        0x0202 0x6210 # set register 2 to 0x10
        0x0204 0x8124 # add r2 to r1
        0x0206 0x8f00 # set register f to 0x0
        0x0208 0x6301 # set register 3 to 0xf0
        0x020a 0x640f # set register 4 to 0x10
        0x020c 0x8345 # subtract r4 from r3
        0x020e 0x8335 # subtract r3 from r3
        0x0210 0x6502 # set register 5 to 0x1
        0x0212 0x6601 # set register 6 to 0x2
        0x0214 0x8567 # subtract r5 from r6 and store in r5
    */

    // init
    State chip8State = {.pc = ROM_OFFSET};
    uint8_t memory[MEM_SIZE];
    memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
    uint8_t rom[] = {0x61, 0xf0, 0x62, 0x10, 0x81, 0x24, 0x6f, 0x0, 0x63, 0x01, 0x64, 0x0f, 0x83, 0x45, 0x83, 0x35, 0x65, 0x02, 0x66, 0x01, 0x85, 0x67};
    memcpy(memory + ROM_OFFSET, rom, sizeof(rom));

    // set both registers
    processOp(&chip8State, memory);
    processOp(&chip8State, memory);
    // perform the addition
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.registers[1], 0x00);
    // carry flag should be set
    assert_int_equal(chip8State.registers[0xf], 0x1);
    // reset flag register
    processOp(&chip8State, memory);
    // set both registers
    processOp(&chip8State, memory);
    processOp(&chip8State, memory);
    // perform the addition
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.registers[3], 0xf1);
    // there *was* a borrow, so this should be 0
    assert_int_equal(chip8State.registers[0xf], 0x0);
    // subtract r3 from r3
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.registers[3], 0x0);
    // there wasn't any borrow, so this should be 1
    assert_int_equal(chip8State.registers[0xf], 0x1);
    // set the registers
    processOp(&chip8State, memory);
    processOp(&chip8State, memory);
    // subtract r6 from r5
    processOp(&chip8State, memory);
    assert_int_equal(chip8State.registers[5], 0xfe);
    assert_int_equal(chip8State.registers[0xf], 0x0);
}

The implementation for 0x8xy7 just switches which register is being subtracted from what (vs 0x8xy5):

void addRegisters(State *state, uint8_t reg1, uint8_t reg2)
{
    uint16_t val = state->registers[reg1] + state->registers[reg2];
    state->registers[reg1] = val & 0xff;
    state->registers[0xf] = (val & 0x100) >> 8;
    state->pc += 2;
}
void subtractRegisters(State *state, uint8_t reg1, uint8_t reg2)
{
    bool needBorrow = state->registers[reg2] > state->registers[reg1];
    state->registers[reg1] = (state->registers[reg1] - state->registers[reg2]) + (needBorrow ? 0xff : 0 );
    // if we need a borrow, this is set  0
    state->registers[0xf] = needBorrow ? 0 : 1;
    state->pc += 2;
}
void subtractRightFromLeft(State *state, uint8_t reg1, uint8_t reg2)
{
    bool needBorrow = state->registers[reg1] > state->registers[reg2];
    state->registers[reg1] = (state->registers[reg2] - state->registers[reg1]) + (needBorrow ? 0xff : 0 );
    // if we need a borrow, this is set  0
    state->registers[0xf] = needBorrow ? 0 : 1;
    state->pc += 2;
}

And let’s skip the processOp integration going forward - that’s enough switch statements for a lifetime ^_^

Conclusion

That’s quite a few op codes covered - a bit boiler plate but in the part we should be able to look at more fancy instructions - namely drawing sprites and user inputs!