What is Chip-8 Programming Language?
CHIP-8 is an interpreted programming language, developed by Joseph Weisbecker. It was initially used on the COSMAC VIP and Telmac 1800 8-bit microcomputers in the mid-1970s. CHIP-8 programs are run on a CHIP-8 virtual machine. It was made to allow video games to be more easily programmed for these computers.
What is Emulator?
I think it is very important to first understand what is an emulator. An emulator is a computer program that mimics the interior design and functionality of a computer system. Almost people confuse a simulator with an emulator and vice versa. Note that this word is not synonymous.
Pong is a 2D tennis game that was developed by Atari and ran on their hardware. However, the game wasn’t just available on Atari systems, but also on rival platforms such as Amstrad, Amiga, and the C64.
How to write an Emulator?
This guide will give you ideas about simulation programs. It will also teach you how to write the program yourself from the beginning. Personally, I have been excited about emulators software since the late 90’s. As I didn’t own a console back in the days (only had a C64), I was pleasantly surprised when I learned that you could use an emulator to run console games on the PC.
Although this guide expects you to have some basic knowledge of computer systems and assumes that you know a programming language, it should also be an interesting lesson for people who are interested in imitation in general.
Game Loop
#include #include // OpenGL graphics and inputConnect Now #include "chip8.h" // Your cpu core implementation chip8 myChip8; int main(int argc, char **argv) { // Set up render system and register input callbacks setupGraphics(); setupInput(); // Initialize the Chip8 system and load the game into the memory myChip8.initialize(); myChip8.loadGame("pong"); // Emulation loop for(;;) { // Emulate one cycle myChip8.emulateCycle(); // If the draw flag is set, update the screen if(myChip8.drawFlag) drawGraphics(); // Store key press state (Press and Release) myChip8.setKeys(); } return 0; }
Emulation cycle
void chip8::initialize() { // Initialize registers and memory once } void chip8::emulateCycle() { // Fetch Opcode // Decode Opcode // Execute Opcode // Update timers }
CHIP-8 Opcode table
CHIP-8 has 35 opcodes, which are all two bytes long and stored big-endian. The opcodes are listed below, in hexadecimal and with the following symbols:
- NNN: address
- NN: 8-bit constant
- N: 4-bit constant
- X and Y: 4-bit register identifier
- PC: Program Counter
- I: 16bit register (For memory address) (Similar to void pointer);
- VN: One of the 16 available variables. N may be 0 to F (hexadecimal);
Opcode | Type | C Pseudo | Explanation |
---|---|---|---|
0NNN | Call | Calls machine code routine (RCA 1802 for COSMAC VIP) at address NNN. Not necessary for most ROMs. | |
00E0 | Display | disp_clear() |
Clears the screen. |
00EE | Flow | return; |
Returns from a subroutine. |
1NNN | Flow | goto NNN; |
Jumps to address NNN. |
2NNN | Flow | *(0xNNN)() |
Calls subroutine at NNN. |
3XNN | Cond | if(Vx==NN) |
Skips the next instruction if VX equals NN. (Usually, the next instruction is a jump to skip a code block); |
4XNN | Cond | if(Vx!=NN) |
Skips the next instruction if VX does not equal NN. (Usually, the next instruction is a jump to skip a code block); |
5XY0 | Cond | if(Vx==Vy) |
Skips the next instruction if VX equals VY. (Usually, the next instruction is a jump to skip a code block); |
6XNN | Const | Vx = N |
Sets VX to NN. |
7XNN | Const | Vx += N |
Adds NN to VX. (Carry flag is not changed); |
8XY0 | Assig | Vx=Vy |
Sets VX to the value of VY. |
8XY1 | BitOp | Vx=Vx|Vy |
Sets VX to VX or VY. (Bitwise OR operation); |
8XY2 | BitOp | Vx=Vx&Vy |
Sets VX to VX and VY. (Bitwise AND operation); |
8XY3 | BitOp | Vx=Vx^V |
Sets VX to VX xor VY. |
8XY4 | Math | Vx += V |
Adds VY to VX. VF is set to 1 when there’s a carry, and to 0 when there is not. |
8XY5 | Math | Vx -= V |
VY is subtracted from VX. VF is set to 0 when there’s a borrow, and 1 when there is not. |
8XY6 | BitOp | Vx>>=1 |
Stores the least significant bit of VX in VF and then shifts VX to the right by 1. |
8XY7 | Math | Vx=Vy-V |
Sets VX to VY minus VX. VF is set to 0 when there’s a borrow, and 1 when there is not. |
8XYE | BitOp | Vx<<=1 |
Stores the most significant bit of VX in VF and then shifts VX to the left by 1. |
9XY0 | Cond | if(Vx!=Vy) |
Skips the next instruction if VX does not equal VY. (Usually, the next instruction is a jump to skip a code block); |
ANNN | MEM | I = NN |
Sets I to the address NNN. |
BNNN | Flow | PC=V0+NN |
Jumps to the address NNN plus V0. |
CXNN | Rand | Vx=rand()&N |
Sets VX to the result of a bitwise and operation on a random number (Typically: 0 to 255) and NN. |
DXYN | Disp | draw(Vx,Vy,N) |
Draws a sprite at coordinate (VX, VY) that has a width of 8 pixels and a height of N+1 pixels. Each row of 8 pixels is read as bit-coded starting from memory location I; I value does not change after the execution of this instruction. As described above, VF is set to 1 if any screen pixels are flipped from set to unset when the sprite is drawn, and to 0 if that does not happen |
EX9E | KeyOp | if(key()==Vx) |
Skips the next instruction if the key stored in VX is pressed. (Usually, the next instruction is a jump to skip a code block); |
EXA1 | KeyOp | if(key()!=Vx) |
Skips the next instruction if the key stored in VX is not pressed. (Usually, the next instruction is a jump to skip a code block); |
FX07 | Timer | Vx = get_delay() |
Sets VX to the value of the delay timer. |
FX0A | KeyOp | Vx = get_key() |
A keypress is awaited and then stored in VX. (Blocking Operation. All instruction halted until next key event); |
FX15 | Timer | delay_timer(Vx) |
Sets the delay timer to VX. |
FX18 | Sound | sound_timer(Vx) |
Sets the sound timer to VX. |
FX1E | MEM | I +=V |
Adds VX to I. VF is not affected. |
FX29 | MEM | I=sprite_addr[Vx] |
Sets I to the location of the sprite for the character in VX. Characters 0-F (in hexadecimal) are represented by a 4×5 font. |
FX33 | BCD |
set_BCD(Vx)
*(I+0)=BCD(3);
*(I+1)=BCD(2);
*(I+2)=BCD(1);
|
Stores the binary-coded decimal representation of VX, with the most significant of three digits at the address in I, the middle digit at I plus 1, and the least significant digit at I plus 2. (In other words, take the decimal representation of VX, place the hundreds digit in memory at a location in I, the tens digit at location I+1, and the ones digit at location I+2.); |
FX55 | MEM | reg_dump(Vx,&I) |
Stores V0 to VX (including VX) in memory starting at the address I. The offset from I is increased by 1 for each value written, but I itself is left unmodified. |
FX65 | MEM | reg_load(Vx,&I) |
Fills V0 to VX (including VX) with values from memory starting at the address I. The offset from I is increased by 1 for each value written, but I itself is left unmodified. |
CHIP-8 Fontset
unsigned char chip8_fontset[80] = { 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 };
Installation
Go install github.com/bradford-hamilton/chippy