CHIP-8 interpreter #2 - interpreter
30/8/2020
(See my other CHIP-8 post focussing on graphics here)
ROMs (programs) are compiled to (or written in) bytecode. Bytecode is a kind of instruction set that is designed for execution by an interpreter. CHIP-8 has 2 byte (octet) instructions and is stored most significant byte first. The first byte of an instruction should be located at an even address, and in the case a program might contain sprite data, that sprite should be padded so the next instruction will be at an even location. CHIP-8 has 36 instructions, however the first is ignored by modern interpreters.
Each opcode (with exception of a few such as draw/return) has various operands that can be one of the following:
- lowest 12 bits
- lowest 4 bits
- lower 4 bits of the high byte
- upper 4 bits of the low byte
- lowest 8 bits
/* * the opcode is a 2 byte value, while our memory is 1 byte * so to get our opcode we need to combine 2 bytes located at PC and PC+1 * we left shift the first byte by 8 (1 byte) to place it in the high byte * and we store the second byte in the lower byte */ opcode = (memory[PC] << 8) | memory[PC+1]; uint16_t nnn = opcode & 0x0FFF; // lowest 12 bits uint8_t n = opcode & 0x000F; // lowest 4 bits uint8_t x = (opcode >> 8) & 0x000F; // lower 4 bits of the high byte, we discard the low byte by right shifting it out uint8_t y = (opcode >> 4) & 0x000F; // upper 4 bits of the low byte, so we need to discard the lower 4 bits uint8_t kk = opcode & 0x00FF; // lowest 8 bitsWith these values known I can continue into the execute stage of the cycle and interpret the correct instruction.
In my interpreter executing an instruction is done within a large, nesting switch statement. Considering the CHIP-8 has 35 used simple instructions, this is a manageable way to do this, however if I were writing an emulator for a real system I would create a function pointer table of handlers which would improve readability vastly.
An example of a few instructions: (note: the memory array is the 4KB of memory the CHIP-8 can access and PC is the program counter pointing to the current instruction)
switch (opcode & 0xF000) { /* * decode highest 4 bits * the highest 4 bits contains the instruction */ case 0x0000: { switch (kk) { case 0x00E0: /* cls (clear screen) */ memset(video, 0, (WIDTH*HEIGHT) * sizeof(uint32_t)); draw_flag = 1; break; case 0x00EE: /* ret (return from subroutine) */ stack[SP] = 0x0; PC = stack[--SP]; break; default: unknown_opcode(opcode); } break; } case 0x1000: /*JP addr (jump to memory address nnn) */ PC = nnn; break; case 0x2000: /* CALL addr (call subroutine at addr, increment SP and put current PC on top of stack, set PC to nnn) */ stack[SP++] = PC; PC = nnn; break; ... case 0x05: /* SUB Vx, Vy (subtract Vx from Vy, store in Vx, if Vx > Vy set V[F] 1, otherwise 0 */ V[0xF] = (V[x] > V[y]) ? 1 : 0; V[x] -= V[y]; break; case 0x06: /* SHR Vx (if the least significant bit of Vx is 1, V[F] set to 1, otherwise 0, then Vx is divided by 2 */ V[0xF] = V[x] & 0x01; V[x] >>= 1; break; case 0x07: /* SUBN Vx, Vy (if Vy > Vx then set V[F] 1 otherwise 0, then Vx is subtracted from Vy, result stored in Vx */ V[0xF] = (V[y] > V[x]) ? 1 : 0; V[x] = V[y] - V[x]; break; case 0x0E: /* SHL Vx (if the most significant bit of Vx is 1, V[F] set to 1, otherwise 0, then Vx is multiplied by 2 */ V[0xF] = (V[x] >> 7) & 0x01; V[x] <<= 1; break; ... }The instruction is stored within the upper nibble (upper 4 bits) of the opcode's most significant byte. This is enough to decode most instructions, however sometimes multiple instructions use the same upper nibble. In these cases the lowest nibble (variable n) and the lowest byte (variable kk) is used to identify which specific instruction is to be executed. Many instructions use the operands (nnn, n, x, y, kk) as extra information for the instructions, such as memory addresses, register numbers, constants, x/y screen positions etc.
Demonstration of my interpreter playing a breakout game and running a few demos: (the flashing is a byproduct of how the chip-8 is designed - it is intended)
The source code can be found in my git repository.