Continuing from the previous post, we’ll implement more of the codes defined here.
- Setting registers -
0x6xnn, 0x7xnn
- Conditionals -
0x3xnn, 0x4xnn, 0x5xy0, 0x9xy0
- Assignment -
0x8xy0
- Bitwise operations, operators -
0x8xy1, 0x8xy2, 0x8xy3
- Bitwise operations, bit shifting -
0x8xy6, 0x8xye
- Maths operations on registers -
0x8xy4, 0x8xy5, 0x8xy7
- Conclusion
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!