A fast CHIP-8 emulator for the TI-84+ CE, written in Ez80 Assembly and C.
Uses the CE-Programming toolchain.
$ make
I've always been interested in really low level programming, but never quite got into it simply because it was too weird and difficult. I decided that if I wanted to dip my toe in the water, so to speak, I should write something relatively simple to start. I wrote this to familiarize myself with assembly in general, but quickly discovered that ez80 assembly (or z80 assembly, for that matter) was even more difficult than I anticipated. The consequence of the z80 design team using random-logic in their processor was a more feature rich processor but also a weirdly unorthogonal instruction set, at least by modern standards.
For instance, it's my impression that the three "general-purpose" registers, BC, DE, and HL are less "general purpose" than one would think. HL is really only used as a pointer, for example. BC and DE are used for looping, but BC gains more use because of the djnz instruction. However by far the greatest hurdle for me was the lack of some kind of base+offset addressing mode. Any pointer arithmetic basically has to go through a bunch of loads, adds, and stores which is only made more frustrating by the add instruction's limited addressing modes. That's why C is quite slow on the z80 as it requires a lot of pointer arithmetic for arrays and certain stack loads or stores. Amusingly, on the actual CHIP-8 pretty much every register is general purpose (and there are more of them) except for VF and sometimes V0. Although the ez80 apparently benefits from a full 24-bit ALU and a pretty good pipeline, it's still limited by the instruction set but not nearly as badly as the earlier Ti-8x series.
Another aspect of writing this was that I wanted to write an emulator (or, in this case, a bytecode interpreter). This isn't anything fancy like a JIT or whatnot, it's a simple fetch, decode, execute loop type operation not unlike the same operations that were often found in older CPUS.
This entire experience has been really humbling. I had pretty good faith in my ability to code and learn new languages, so I decided that a challenge would be fun. The truth is that debugging this has mostly been a huge nightmare, but learning and initially writing assembly was weirdly soothing, and extremely rewarding.
Opcode | Mnemonic | Description | C Pseudocode | Status |
---|---|---|---|---|
0NNN | SYS $NNN | Run RCA1802 program at address NNN. Unused in most modern implementations. | N/A | N/A |
00E0 | CLS | Clear screen (graphics memory) | gfx_clear(); | ✔️ |
00EE | RET | Pop an address from the stack and set PC to this address. | return; | ✔️ |
1NNN | JP $NNN | Set PC to NNN. | PC = 0xNNN; | ✔️ |
2NNN | CALL $NNN | Push PC to the stack and jump to address NNN. | *(0xNNN)(); | ✔️ |
3XNN | SE VX, $NN | Skip the next instruction if VX equals NN. | if (V[X] == 0xNN) { PC += 2; } | ✔️ |
4XNN | SNE VX, $NN | Skip the next instruction if VX does not equal NN. | if (V[X] != 0xNN) { PC += 2; } | ✔️ |
5XY0 | SE VX, VY | Skip the next instruction if VX equals VY. | if (V[X] == V[Y]) { PC += 2; } | ✔️ |
6XNN | LD VX, $NN | Set VX to NN. | V[X] = 0xNN; | ✔️ |
7XNN | ADD VX, $NN | Set VX to VX + NN | V[X] = V[X] + 0xNN; | ✔️ |
8XY0 | LD VX, VY | Set VX to VY. | V[X] = V[Y]; | ✔️ |
8XY1 | OR VX, VY | Set VX to VX OR VY. | V[X] = V[X] | V[Y]; | ✔️ |
8XY2 | AND VX, VY | Set VX to VX AND VY. | V[X] = V[X] & V[Y]; | ✔️ |
8XY3 | XOR VX, VY | Set VX to VX XOR VY. | V[X] = V[X] ^ V[Y]; | ✔️ |
8XY4 | ADD VX, VY | Set VX to VX + VY. Set VF to carry. | V[X] = V[X] + V[Y]; V[F] = carry; | ✔️ |
8XY5 | SUB VX, VY | Set VX to VX - VY. Set VF to not borrow. | V[X] = V[X] - V[Y]; V[F] = !borrow; | ✔️ |
8X06 | SHR VX | Set VF to VX's least significant bit. Set VX to VX shifted right 1. | V[X] = V[X] >> 1; V[F] = lsb; | ✔️ |
8XY7 | SUBN VX, VY | Set VX to VY - VX. Set VF to not borrow. | V[X] = V[Y] - V[X]; V[F] = !borrow; | ✔️ |
8X0E | SHL VX | Set VF to VX's most significant bit. Set VX to VX shifted left 1. | V[X] = V[X] << 1; V[F] = msb; | ✔️ |
9XY0 | SNE VX, VY | Skip the next instruction if VX does not equal VY. | if (V[X] != V[Y]) { PC += 2; } | ✔️ |
ANNN | LD I, $NNN | Set I to NNN. | I = 0xNNN; | ✔️ |
BNNN | JP V0, $NNN | Set PC to NNN + V0. | PC = 0xNNN + V[0]; | ✔️ |
CXNN | RND VX, $NN | Set VX to a random value ANDed with NN. | V[X] = rand() & 0xNN; | ✔️ |
DXYN | DRW VX, VY, $N | Draw a bitmapped sprite at (VX, VY) with height N starting from M[I]. | gfx_sprite(V[X], V[Y], 0xN, memory[I]); | ✔️ |
EX9E | SKP VX | Skip the next instruction if key VX is pressed. | if (keypad[V[X] & 0xF]) { PC += 2; } | ✔️ |
EXA1 | SKNP VX | Skip the next instruction if key VX is not pressed. | if (!keypad[V[X] & 0xF]) { PC += 2; } | ✔️ |
FX07 | LD VX, DT | Set VX to the delay timer. | V[X] = timer_delay; | ✔️ |
FX0A | LD VX, K | Wait until a key is pressed, and then set VX to the key. | while (!key_pressed) {} V[X] = key; | ✔️ |
FX15 | LD DT, VX | Set the delay timer to VX. | timer_delay = V[X]; | ✔️ |
FX18 | LD ST, VX | Set the sound timer to VX. | timer_sound = V[X]; | ✔️ |
FX1E | ADD I, VX | Set I to I + VX. | I = I + V[X]; | ✔️ |
FX29 | FNT VX | Set I to the location of character VX's font sprite. | I = font[V[X]]; | ✔️ |
FX33 | BCD VX | Set M[I, I+1, I+2] to the decimal digits of the BCD character in VX. | bcd_store(&(memory[I]), V[X]); | ✔️ |
FX55 | SR (I), VX | Store registers up to VX at M[I..I+X]. | for (int k=0;k<X;k++) {memory[I+k] = V[k];} | ✔️ |
FX65 | LR VX, (I) | Load registers from M[I..I+X] into registers up to VX. | for (int k=0;k<X;k++) {V[k] = memory[I+k];} | ✔️ |
Opcode | Mnemonic | Description | C Pseudocode | Status |
---|---|---|---|---|
00CN | SCD $N | Scroll the screen down N lines. | gfx_scroll_down(0xN); | ❌ |
00FB | SCR | Scroll the screen right four pixels. | gfx_scroll_right(4); | ❌ |
00FC | SCL | Scroll the screen left four pixels. | gfx_scroll_left(4); | ❌ |
00FD | EXIT | Exit the program. | exit(0); | ❌ |
00FE | LOW | Set the graphics mode to regular CHIP-8 mode (64x32 pixels) | gfx_mode = 0; | ❌ |
00FF | HIGH | Set the graphics mode to SUPERCHIP mode (128*64 pixels) | gfx_mode = 1; | ❌ |
DXY0 | XDRW VX, VY | If in SUPERCHIP graphics, draw a 1616 sprite at (VX*, VY) from M[I]. | if (gfx_mode == 1) {gfx_xsprite(V[X], V[Y], memory[I]);} | ❌ |
FX30 | XFNT VX | Set I to the location of character VX's large font sprite. | I = xfont[V[X]]; | ❌ |
FX75 | SRPL VX | Store V0..VX in some kind of non-volatile storage. | for (int i=0;i<X;i++) {write(V[i]);} | ❌ |
FX85 | LRPL VX | Load V0..VX from some kind of non-volatile storage. | for (int i=0;i<X;i++) {V[i] = read();} | ❌ |
While writing this, I used Cowgod's excellent documentation of the CHIP-8. This was a huge help. Without it the emulator wouldn't be nearly as accurate.