Continuing from the previous post, we’ll implement all the remaining codes defined here.
- I-related -
0xannn, 0xfx1e
- PC-related -
0xbnnn
- Storing/loading registers -
0xfx55, 0xf65
- Random number -
0xcxnn
- Setting sprites -
0xfx29
- Displaying sprites -
0xdxyn
- Keyboard -
0xex9e, 0xexa1, 0xfx0a
- Binary-coded decimal -
0xfx33
- Timers -
0xfx07, 0xfx15, 0xfx18
- Conclusion
I-related - 0xannn, 0xfx1e
The tests:
static void test_memory_set_i(void **state)
{
/*
The test ROM will look like this:
0x0200 0xa123 # set i to 0x123
0x0202 0x6102 # set r1 to 0x2
0x0204 0xf11e # add the contents of r1 to i
*/
// init
State chip8State = {.pc = ROM_OFFSET};
uint8_t memory[MEM_SIZE];
memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
uint8_t rom[] = {0xa1, 0x23, 0x61, 0x02, 0xf1, 0x1e};
memcpy(memory + ROM_OFFSET, rom, sizeof(rom));
// i should be 0 upon initialisation
assert_int_equal(chip8State.i, 0x0);
// i should now be 0x123
processOp(&chip8State, memory);
assert_int_equal(chip8State.i, 0x123);
// set the register
processOp(&chip8State, memory);
// add to i
processOp(&chip8State, memory);
assert_int_equal(chip8State.i, 0x125);
}
The only bit of interest implementation-wise is how we concatenate the operands to form a memory address:
void setI(State *state, uint8_t top, uint8_t bottom)
{
state->i = (top << 8) | bottom;
state->pc += 2;
}
void addRegToI(State *state, uint8_t reg)
{
state->i += state->registers[reg];
state->pc += 2;
}
PC-related - 0xbnnn
This really isn’t much:
static void test_memory_set_pc(void **state)
{
/*
The test ROM will look like this:
0x0200 0x6002 # set r0 to 0x2
0x0202 0xb123 # set the program counter to r0 + 0x123
*/
// init
State chip8State = {.pc = ROM_OFFSET};
uint8_t memory[MEM_SIZE];
memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
uint8_t rom[] = {0x60, 0x02, 0xb1, 0x23};
memcpy(memory + ROM_OFFSET, rom, sizeof(rom));
// set the register
processOp(&chip8State, memory);
// set i
processOp(&chip8State, memory);
assert_int_equal(chip8State.pc, 0x125);
}
And the implementation is equally straight-forward:
void setPC(State *state, uint8_t top, uint8_t bottom)
{
state->pc = state->registers[0] + ((top << 8) | bottom);
}
Storing/loading registers - 0xfx55, 0xf65
As the latter is the inverse of the former, it makes for a nice test:
static void test_save_load_registers(void **state)
{
/*
The test ROM will look like this:
0x0200 0xa300 # set i to 0x0300
0x0200 0x6101 # set r1 to 0x1
0x0200 0x6202 # set r2 to 0x2
0x0200 0x6302 # set r3 to 0x3
0x0202 0xf355 # store r0 to r3 at i
0x0200 0x6109 # set r1 to 0x1
0x0200 0x6208 # set r2 to 0x2
0x0200 0x6307 # set r3 to 0x3
0x0202 0xf365 # load r0 to r3 from i
*/
// init
State chip8State = {.pc = ROM_OFFSET};
uint8_t memory[MEM_SIZE];
memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
uint8_t rom[] = {0xa3, 0x00, 0x61, 0x01, 0x62, 0x02, 0x63, 0x03, 0xf3, 0x55, 0x61, 0x09, 0x62, 0x08, 0x63, 0x07, 0xf3, 0x65};
memcpy(memory + ROM_OFFSET, rom, sizeof(rom));
// set the registers and save
for (int i = 0; i < 5; i++)
{
processOp(&chip8State, memory);
}
uint8_t expected[] = {0x0, 0x1, 0x2, 0x3};
assert_memory_equal(memory + chip8State.i, expected, 4);
// we now set the registers to something else and load
for (int i = 0; i < 4; i++)
{
processOp(&chip8State, memory);
}
for (int i = 0; i < 4; i++)
{
assert_int_equal(chip8State.registers[i], i);
}
}
The implementation was surprisingly straight-forward:
void saveRegisters(State *state, uint8_t reg, uint8_t memory[])
{
uint16_t memIdx = state->i;
for (int regIdx=0; regIdx<=reg; regIdx++){
memory[memIdx++] = state->registers[regIdx];
}
state->pc += 2;
}
void loadRegisters(State *state, uint8_t reg, uint8_t memory[])
{
uint16_t memIdx = state->i;
for (int regIdx=0; regIdx<=reg; regIdx++){
state->registers[regIdx] = memory[memIdx++];
}
state->pc += 2;
}
Random number - 0xcxnn
Note that in the test we use srand
to set the seed for further calls to the rand
function - this makes our tests reproducible. And actually once we start debugging actual ROMs, we may want this as an option to make things a little more deterministic.
static void test_rand(void **state)
{
/*
The test ROM will look like this:
0x0200 0xc50f # generate a random number between 0-255 & 0x0f
*/
// init
State chip8State = {.pc = ROM_OFFSET};
uint8_t memory[MEM_SIZE];
memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
uint8_t rom[] = {0xc5, 0x0f};
memcpy(memory + ROM_OFFSET, rom, sizeof(rom));
processOp(&chip8State, memory);
// set the seed so we always get the same random number
srand(0xdeadbeef);
assert_in_range(chip8State.registers[0x5],0x0,0xf);
assert_int_equal(chip8State.registers[0x5],0x7);
}
Nothing fancy for the implementation - by default rand
will generate a number between 0 and RAND_MAX
, so we % 256
to ensure we get something in the 0-255
range.
void getRandomNumber(State *state, uint8_t reg, uint8_t mask)
{
state->registers[reg] = rand() % 256 & mask;
state->pc += 2;
}
Setting sprites - 0xfx29
The CHIP-8 specification stores a pre-defined set of “sprites” in memory. Those represent an 5x8 bit array where a 1 is a pixel turned on, and 0 turned off - one for each hex character. See here for a visual representation. Note each hex character occupies only half the size (so 5x4).
I’m not entirely sure which offset those should start at so let’s keep all the sprites in a single block starting at SPRITES_OFFSET
which we’ll set to 0x0
for now.
void copySpritesToMemory(uint8_t memory[])
{
uint8_t sprites[] = {
0xf0,0x90,0x90,0x90,0xf0, //0
0x20,0x60,0x20,0x20,0x70, //1
0xf0,0x10,0xf0,0x80,0xf0, //2
0xf0,0x10,0xf0,0x10,0xf0, //3
0x90,0x90,0xf0,0x10,0x10, //4
0xf0,0x80,0xf0,0x10,0xf0, //5
0xf0,0x80,0xf0,0x90,0xf0, //6
0xf0,0x10,0x20,0x40,0x40, //7
0xf0,0x90,0xf0,0x90,0xf0, //8
0xf0,0x90,0xf0,0x10,0xf0, //9
0xf0,0x90,0xf0,0x90,0x90, //a
0xe0,0x90,0xe0,0x90,0xe0, //b
0xf0,0x80,0x80,0x80,0xf0, //c
0xe0,0x90,0x90,0x90,0xe0, //d
0xf0,0x80,0xf0,0x80,0xf0, //e
0xf0,0x80,0xf0,0x80,0x80, //f
};
memcpy(memory + SPRITES_OFFSET, sprites, sizeof(sprites));
}
This will need to be called during the ROM’s initialisation routine but we can handle that later.
Here’s the test:
static void test_set_sprite(void **state)
{
/*
The test ROM will look like this:
0x0200 0xff29 # set i to the location of the sprite for 0xf
*/
// init
State chip8State = {.pc = ROM_OFFSET};
uint8_t memory[MEM_SIZE];
memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
uint8_t rom[] = {0xff, 0x29};
copySpritesToMemory(memory);
memcpy(memory + ROM_OFFSET, rom, sizeof(rom));
processOp(&chip8State, memory);
assert_int_equal(chip8State.i, 5*0xf);
uint8_t spriteF[] = { 0xf0,0x80,0xf0,0x80,0x80 };
assert_memory_equal(memory + SPRITES_OFFSET + chip8State.i, spriteF, 5);
}
And the implementation:
void setIToSprite(State *state, uint8_t reg)
{
// a sprite is 5 bytes long and indexing starts at 0 for this interpreter
state->i = reg*5;
state->pc += 2;
}
Note we expose copySpritesToMemory
in mylib.h
so it’s available in the test.
Displaying sprites - 0xdxyn
This is the instruction we need to start displaying things! very excited
It essentially takes (x,y)
coordinates and a value n
representing how many “rows” we write. The data is what the register I is point to. So for instance if we wanted to display the character 0x0
in the top-left corner, we would set i
to the 0x0
sprite address (SPRITES_OFFSET
) and as the sprite is 5-rows high, n
would be 5.
The second part is that if we flip any pixels whilst doing so (so turning one off or on), we set the register 0xf
to 1.
As always, let’s start with a test!
static void test_draw_sprite(void **state)
{
/*
The test ROM will look like this:
0x0200 0xff29 # set i to the location of the sprite for 0xf
0x0202 0xd005 # draw the 5x8 sprite defined at i at (0,0)
0x0204 0xd005 # draw the 5x8 sprite defined at i at (0,0)
Redrawing the same sprite should cause the 0xf register to be set to 1
*/
// init
State chip8State = {.pc = ROM_OFFSET};
uint8_t memory[MEM_SIZE];
memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
uint8_t rom[] = {0xff, 0x29, 0xd0, 0x05, 0xd0, 0x05};
copySpritesToMemory(memory);
memcpy(memory + ROM_OFFSET, rom, sizeof(rom[0]) * 6);
// set i
processOp(&chip8State, memory);
assert_int_equal(chip8State.i, 5*0xf);
// draw
processOp(&chip8State, memory);
// confirm the pixels have been set accordingly
uint8_t pixels[] = { 0xf0,0x80,0xf0,0x80,0x80 };
for (int row = 0; row < 5; row++) {
assert_int_equal(memory[MEM_DISPLAY_START + row*SCREEN_WIDTH/8], pixels[row]);
}
// draw flag should be set
assert_true(chip8State.draw);
assert_int_equal(chip8State.registers[0xf], 1);
// simulate a screen refersh by setting the flag back to false
chip8State.draw = false;
// draw the same sprite again
processOp(&chip8State, memory);
// flag should be unchanged
assert_false(chip8State.draw);
assert_int_equal(chip8State.registers[0xf], 0);
}
And here’s the implementation:
void setPixels(State *state, uint8_t x, uint8_t y, uint8_t height, uint8_t memory[])
{
uint8_t flipped = 0;
uint16_t idx = state->i;
uint16_t offset = 0;
for (int row = 0; row < height; row++) {
offset = MEM_DISPLAY_START + x + (SCREEN_WIDTH/8)*(y+row);
flipped += memory[offset] ^ memory[idx];
memory[offset] = memory[idx++];
}
state->draw = flipped ? true : false;
state->registers[0xf] = state->draw ? 1 : 0;
state->pc += 2;
}
Since the width of a sprite is always 8 bites we need to move in (SCREEN_WIDTH/8)*(y+row)
increments.
From a state-perspective, we set the draw
boolean if any bits have been flipped - this will trigger a screen refresh if there has been any changes.
Keyboard - 0xex9e, 0xexa1, 0xfx0a
The first 2 are essentially conditionals on key presses. We can mimic that behaviour in our test by setting the appropriate key registers. I originally misread the spec as this being a conditional on whether key x
was pressed - it’s actually whether the key corresponding to the value in rX
has been pressed. Subtle, but without it nothing will behave as expected :)
Unlike most of our previous tests, we’ll run this one twice - once with a key depressed, and one with it pressed:
static void test_keyboard(void **state)
{
/*
The test ROM will look like this:
0x0200 0x6001 # set register r0 to 1
0x0202 0xe19e # skip the next instruction if key pointed to by r1 is down
0x0204 0xe1a1 # skip the next instruction if key pointed to by r1 is *not* down
This test is a little different in that state will be changed from the outside.
*/
// init
State chip8State = {.pc = ROM_OFFSET};
uint8_t memory[MEM_SIZE];
memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
uint8_t rom[] = {0x60, 0x01, 0xe1, 0x9e, 0xe1, 0xa1};
memcpy(memory + ROM_OFFSET, rom, sizeof(rom));
// check that key 1 is depressed
assert_int_equal(chip8State.input[1], false);
// set the register
processOp(&chip8State, memory);
// skip 0x0204 if the key is pressed (which it isn't)
processOp(&chip8State, memory);
assert_int_equal(chip8State.pc, 0x204);
processOp(&chip8State, memory);
assert_int_equal(chip8State.pc, 0x208);
// now run through this again but setting the key as pressed
State chip8State2 = {.pc = ROM_OFFSET};
chip8State2.input[0x1] = true;
processOp(&chip8State2, memory);
// skip 0x0204 if the key is pressed (which it is this time)
processOp(&chip8State2, memory);
// key 1 is still pressed so we should proceed to the next instruction
processOp(&chip8State2, memory);
assert_int_equal(chip8State2.pc, 0x208);
}
The implementation of one is the opposite of the other:
void jumpIfKeyPressed(State *state, uint8_t reg)
{
state->pc += state->input[state->registers[reg]] ? 4 : 2;
}
void jumpIfKeyNotPressed(State *state, uint8_t reg)
{
state->pc += state->input[state->registers[reg]] ? 2 : 4;
}
Adding this to processOp
covers the whole 0xe
block:
case (0xe):
{
switch (opCodeRight)
{
case (0x9e):
jumpIfKeyPressed(state, opCodeB);
break;
case (0xa1):
jumpIfKeyNotPressed(state, opCodeB);
break;
default:
error = true;
break;
}
}
The last one is a bit trickier as it’s a blocking operation - that is, we need to wait until a key is pressed before we proceed - and whilst we wait, nothing happens. The easiest way to implement this is to simply not increment the program counter until a key is pressed.
static void test_keyboard_blocking(void **state) {
/*
The test ROM will look like this:
0x0200 0xf50a # wait until a key is pressed, store it in r5
*/
// init
State chip8State = {.pc = ROM_OFFSET};
uint8_t memory[MEM_SIZE];
memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
uint8_t rom[] = {0xf5, 0x0a};
memcpy(memory + ROM_OFFSET, rom, sizeof(rom[0]) * 2);
// wait for a key to be pressed
processOp(&chip8State, memory);
assert_int_equal(chip8State.pc, ROM_OFFSET);
// simulate a key getting pressed
chip8State.input[1] = true;
// check again
processOp(&chip8State, memory);
// we should now proceed to the next instruction
assert_int_equal(chip8State.pc, 0x202);
// and register 5 should have value 1
assert_int_equal(chip8State.registers[0x5], 0x1);
}
Note that the implementation of this function is stateless - if key 1 is already being pressed, this function will store key 1 into the relevant register and proceed. I don’t know if hardware implementations of CHIP-8 support multiple key presses at the same time but for simplicity, we’ll assume not for now.
void waitForKey(State *state, uint8_t reg)
{
// any key that is pressed if valid
for (int key=0; key<16;key++) {
if (state->input[key]) {
state->registers[reg] = key;
state->pc += 2;
}
}
}
One way I could see working around this is to store some sort of timestamp along with each key press, which would mean making the evaluation loop time-aware. But let’s kick this down the road.
Binary-coded decimal - 0xfx33
I didn’t expect to come across such an instruction. If I=0x300
and register 1 has 0x81
(which is 129 in decimal), we then have the following:
0x300 = 1
0x301 = 2
0x302 = 9
A small test should suffice:
static void test_bcd(void **state)
{
/*
The test ROM will look like this:
0x0200 0x6181 # set register 1 to 0x81, or 129 in decimal
0x0202 0xa300 # set i to 0x300
0x0203 0xf133 # store the binary representation of r1 at I
Redrawing the same sprite should cause the 0xf register to be set to 1
*/
// init
State chip8State = {.pc = ROM_OFFSET};
uint8_t memory[MEM_SIZE];
memset(memory, 0x0, MEM_SIZE * sizeof(uint8_t));
uint8_t rom[] = {0x61, 0x81, 0xa3, 0x0, 0xf1, 0x33};
copySpritesToMemory(memory);
memcpy(memory + ROM_OFFSET, rom, sizeof(rom));
// set register 1
processOp(&chip8State, memory);
// set i to 0x300
processOp(&chip8State, memory);
assert_int_equal(chip8State.i, 0x300);
// BCD of 0x81 is 129
processOp(&chip8State, memory);
assert_int_equal(memory[chip8State.i], 1);
assert_int_equal(memory[chip8State.i+1], 2);
assert_int_equal(memory[chip8State.i+2], 9);
}
And the implementation:
void setIToBCD(State *state, uint8_t reg, uint8_t memory[])
{
uint8_t val = state->registers[reg];
for (int offset = 2; offset >= 0; offset--)
{
memory[state->i + offset] = val % 10;
val -= memory[state->i + offset];
val /= 10;
}
state->pc += 2;
}
Crude I know, but it works.
Timers - 0xfx07, 0xfx15, 0xfx18
Mmm - so timing is something we didn’t really cover in our main event loop. Right now our interpreter is simply processing instructions as quickly as it can without any notion of “wall clock time”. What this really means is that if our CPU is slow instructions will be processed slowly, and if it’s super fast your game of Snake will be Game Over before you can even blink.
We’ll sort this out in a separate post. Right now we’ll simply implement stubs that set the registers and do nothing else.
void setRegisterToDelayTimer(State *state, uint8_t reg)
{
state->registers[reg] = state->delay_timer;
state->pc += 2;
}
void setDelayTimerFromRegister(State *state, uint8_t reg)
{
state->delay_timer = state->registers[reg];
state->pc += 2;
}
void setSoundTimerFromRegister(State *state, uint8_t reg)
{
state->sound_timer = state->registers[reg];
state->pc += 2;
}
Conclusion
OK - I think that means we’re ready to go back to our test ROM and start debugging. Hopefully we have implemented all the instructions required with minimal issues (but I know better than thinking it’ll work flawlessly from the get-go :-/)