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:
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:
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:
And let’s make sure clearDisplay
increments the program counter:
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.
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:
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.
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”:
We also had to combine each of the N
s (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
:
0x1NNN
The easiest one for last, tackling the unconditional jump:
Putting it together
The main switch
block of processOpCode
now looks like this:
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.