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!