Writing a Chip 8 Emulator - Instruction Set (Part 5)

By Craig Thomas, Fri 07 September 2018, in category Emulation

chip8, emulation

Last time I wrote about the Chip 8 interpreter, and how the first 512 bytes of RAM were used for the built in font set. In this post, I want to go over all the instructions for the Chip 8 interpreter, and discuss how they actually work. I also wanted to talk more about the Chip 8 hardware.

If you want to see more about how I implemented the instruction set, and the virtualized hardware, please feel free to check out some of my projects:

Also, if you are looking for an assembler to help you build various Chip 8 programs, then check out:

Let's get started!

Chip 8 Hardware

The Chip 8 hardware is generally pretty simple. Here is a breakdown of each of the components.

General Purpose Registers

The Chip 8 has 16 general purpose registers. They are all 8 bits long. These registers in an emulator are easy to implement as a simple array of bytes. They are typically labelled as R[1], R[2], ..., R[E], R[F].

The R[F] register (or 16th register) is special. On operations where overflow may occur, it is used to store the overflow bit.

Program Counter

The Chip 8 contains a single 16-bit program counter known as PC. The PC is a pointer into memory where the current instruction should be fetched from.

The general flow of the CPU is basically to fetch the next opcode from memory where the PC is pointing to. Then, it usually increments the program counter by one, and executes the instruction that was fetched. There are some instructions however, that will modify the program counter directly.

Note that for the most part, the program counter is protected from the programmer. This means that the programmer can't store any values in the PC register directly, nor can you read from the PC register.

The program counter typically has a starting address of $0200.

Stack Pointer

The stack pointer register is a 16-bit register known as SP. The stack pointer points to a location somewhere in memory where values can be saved. With operations like CALL or RTS, the stack pointer is used to determine what values should be stored from the program counter or loaded to the program counter.

Like the program counter, the stack pointer register cannot be directly accessed by the programmer. Instead, the Chip 8 hardware is responsible for setting up the stack pointer area, and will update the pointer when various instructions are called.

The stack pointer typically starts at address $0052.

Index Register

The index register is a 16-bit register known as I. The index register is used to store addresses that other operations will make use of, typically with some sort of additional offset given. For example, the STOR command ($Fs55) will store the value of all of the general purpose registers into a memory location starting at the index register.

This particular register can be manipulated by the programmer. A series of functions are available that allow a programmer to load a value, and perform various functions against the index register.

Delay and Sound Timer Registers

These two 8-bit registers are known as SOUND and DELAY. They store a single 8 bit value. The value inside of each register is decremented 60 times a second until the value hits zero.

When the value of the sound timer register is non-zero, then the Chip 8 will beep. Once it hits zero, then it will remain at zero until reset by the user. With the delay register, nothing happens when the value is non-zero - it is up to the programmer to use the delay register to determine timing for various loops. Like the sound timer register, when the delay timer register hits zero, nothing further will happen until reset by the programmer.

Instruction Set

Below is a mostly complete listing of instructions for the Chip 8. For each instruction, I have provided what the hexadecimal opcode looks like, and then a mnemonic that can be used with my Chip 8 Assembler.

00E0 - CLR - Clear Screen

This instruction is fairly straightforward. The contents of the Chip 8 screen are cleared. All pixels on the display are set to off. No changes to the registers are made.


00EE - RTS - Return from Subroutine

Two values are popped off the stack forming a 16-bit address. The program counter is updated to the value that was popped. The stack pointer is decremented by 2. The operation is described as follows:

PC' = MEM[SP-1]:MEM[SP-2]
SP' = SP - 2

For example, if the stack pointer points at $0103, and memory location $0102 contains the value $12, and memory location $0101 contains the value $34, and the opcode $00EE is encountered, then the program counter will contain the value $1234, and the stack pointer will be updated to point to $0101.


1nnn - JUMP - Jump to Address

The program counter is set to the 12-bit address encoded by the nibbles NNN. The operation is described as follows:

PC' = OPCODE & 0x7

For example, if the opcode $1625 is encountered, the program counter will be set to $0625. To get the address, mask off the lower 3 bits of the opcode.


2nnn - CALL - Call Subroutine

The current value of the program counter is pushed onto the stack. The stack pointer is incremented by 2. Next, the program counter is updated to the 12-bit address encoded by the nibbles NNN. The operation is described as follows:

MEM[SP] = (PC & 0x00FF)
MEM[SP+1] = (PC & 0xFF00) >> 8
SP' = SP + 2
PC' = OPCODE & 0x7

For example, if the opcode $2625 is encountered, and the stack pointer contains $0400, then the current contents of the program counter will be placed in memory addresses $0400 (the lower 8 bits of the PC), and $0401 (the upper 8 bits of the PC). The stack pointer will then point at $0402. The program counter will be set to the value $0625.


3snn - SKE - Skip if Register Equal Value

Skip the next instruction if the value contained in register S equals the 8-bit value encoded by NN. The operation is described as follows:

PC' = PC + 2 IF R[S] == NN

For example, if R[1] equals $29, and the opcode $3129 is encountered, then the program counter will be updated by 2. Otherwise, the program counter will be incremented by one as usual.


4snn - SKNE - Skip if Register Not Equal Value

Skip the next instruction if the value contained in register S does not equal the 8-bit value encoded by NN. The operation is described as follows:

PC' = PC + 2 IF R[S] != NN

For example, if R[1] equals $29, and the opcode $3130 is encountered, then the program counter will be updated by 2. If the opcode $3129 is encountered, the program counter will be incremented by one as usual.


5st0 - SKRE - Skip if Register Equal Register

Skip the next instruction if the contents of register S equals the contents of register T. The operation is described as follows:

PC' = PC + 2 IF R[S] == R[T]

For example, if R[1] equals $FF, R[2] equals $FF, and the opcode $5120 is encountered, then the program counter will be updated by 2.


6snn - LOAD - Load Register with Value

Loads register S with the 8-bit byte NN. The operation is described as follows:

R[S]' = NN

For example, if the opcode $638E is encountered, then register R[3] will be loaded with the value of $8E.


7snn - ADD - Add value to register

Adds the 8-bit value NN to register S. If the addition causes an overflow, then register 16 is updated with a $01. The operation is described as follows:

R[S]' = R[S] + NN
R[F]' = $01 IF R[S] + NN > 255

For example, if the opcode $7202 is encountered, and register R[2] has the value $04, then following execution of the instruction, register R[2] will have the value $06.


8ts0 - MOVE - Move value between registers

Copies the contents of register S into register T. The operation is described as follows:

R[T]' = R[S]

For example, if the opcode $8340 is encountered, and register R[4] contains the value $EE, then following execution of the instruction, register R[3] will contain the value $EE. The register R[4] will also maintain a separate copy of $EE.


8ts1 - OR - Logical OR

Performs a logical OR operation of the value within register S and T and stores the result in register T. The operation is described as follows:

R[T]' = R[S] | R[T]

For example, if the opcode $8341 is encountered, and the value in R[3] is $02, and the value in R[4] is $01, then following execution of the instruction, R[3] will contain the value $03.


8ts2 - AND - Logical AND

Performs a logical AND operation of the value within register S and T and stores the result in reigster T. The operation is described as follows:

R[T]' = R[S] & R[T]

For example, if the opcode $8342 is encountered, and the value in R[3] is $03, and the value in R[4] is $01, then following execution of the instruction, R[3] will contain the value $01.


8ts3 - XOR - Logical XOR

Performs a logical XOR operation of the value within register S and T and stores the result in register T. The operation is described as follows:

R[T]' = R[S] ^ R[T]

For example, if the opcode $8343 is encountered, and the value in R[3] is $01, and the value in R[4] is $01, then following execution of the instruction, R[3] will contain the value $00.


8ts4 - ADDR - Add Register to Register with Overflow

Adds the value in register T to the value in register S and stores the result in register T. If an overflow occurs, then the value of $1 is written into register 16. The operation is described as follows:

R[T]' = R[S] + R[T]
R[F]' = 1 IF R[S] + R[T] > 255

For example, if the opcode $8344 is encountered, and the value in R[3] is $FF, and the value in R[4] is $01, then following execution of the instruction, R[3] will be $00, and R[F] will be $1.


8ts5 - SUB - Subtract Register from Register

Subtracts the value in register T from the value in register S, and stores the result in register T. If a borrow is not generated, then set carry flag in register 16. The operation is described as follows:

R[T]' = R[S] - R[T]
R[F]' = 1 IF R[T] > R[S]

For example, if the opcode $8345 is encountered, and the value in R[3] is $07 and the value in R[4] is $08, then following execution of the instruction, R[4] will be $FF, and R[F] will be $01.


8s06 - SHR - Shift Right

Shifts the bits in register S one bit to the right. Bit zero will be shifted into register 16. The operation is described as follows:

R[S]' = R[S] >> 1
R[F]' = R[S]:0

For example, if the opcode $8106 is encountered, and the value in R[1] is $07, then following execution of the instruction, R[1] will be $03, and the value in R[F] will be $01.


8s0E - SHL - Shift Left

Shifts the bits in register S one bit to the left. Bit seven will be shifted into register 16. The operation is described as follows:

R[S]' = R[S] << 1
R[F]' = R[S]:7

For example, if the opcode $810E is encountered, and the value in R[1] is $03, then following execution of the instruction, R[1] will be $07, and the value in R[F] will be $00.


9st0 - SKRNE - Skip if Register not Equal Register

Skip the next instruction if the value contained in register S does not equal the value in register T. The operation is described as follows:

PC' = PC + 2 IF R[S] != R[T]

For example, if R[1] equals $29, and R[2] equals $30 and the opcode $9120 is encountered, then the program counter will be updated by 2.


Annn - LOADI - Load Index

Load the index register with the 12-bit value encoded by NNN. The operation is described as follows:

I' = NNN

For example, if the opcode $A123 is encountered, then the index register will contain the value $123.


Bnnn - JUMPI - Jump to Address with Index

Load the program counter to the index value added with NNN. The operation is described as follows:

PC' = I + NNN

For example, if the index register is $0100, and the opcode $B011 is encountered, then the program counter will be updated with the value $0111.


Ctnn - RAND - Generate Random Number

Generates a random number between 0 and 255, and logical AND the result with NN. Store the result in the target register. The operation is described as follows:

R[T]' = RAND(255) & NN

For example, if the opcode $C10A is encountered, R[1] will be updated to contain a random number between 0 and 10 (inclusive).


Dstn - DRAW - Draw Sprite

Draw an N byte sprite at the locations contained within register S (the x position), and register T (the y position). Drawing is completed using an XOR routine, meaning that if the target pixel is already on, then that pixel will be turned off. In the case where a pixel is turned off, then register 16 will contain a $1. See my post Writing a Chip 8 Emulator – DRAW command (Part 3) for more in-depth information about writing to the screen.


Ft07 - MOVED - Move Delay Register into Register

Copies the current value of the delay register into the target register. The operation is described as:

R[T]' = DELAY

For example, if the opcode $F107 is encountered, then R[1] will contain a copy of the value that is currently in the delay register.


Ft0A - KEYD - Wait for Keypress

Stop execution until a key is pressed. Copies the value of the keypress (values between $0 and $F) into the target register. The operation is described as:

R[T]' = KEYPRESS VALUE

For example, if the opcode $F10A is encountered, then execution will halt until a key is pressed. The value of the key pressed is copied into R[1].


Fs15 - LOADD - Load Register into Delay Register

Copy the value in the source register S into the delay register. The operation is described as:

DELAY' = R[S]

For example, if R[1] contains the value $04, and the opcode $F115 is encountered, then the delay register will be loaded with the value $04.


Fs18 - LOADS - Load Register into Sound Register

Copy the value in the source register S into the sound register. The operation is described as:

SOUND' = R[S]

For example, if R[1] contains the value $04, and the opcode $F118 is encountered, then the sound register will be loaded with the value $04.


Fs1E - ADDI - Add Register into Index

Adds the value in the source register S into the index register. The operation is described as:

I' = I + R[S]

For example, if the index register contains the value $0100, and R[1] contains the value $34, and the opcode $F11E is encountered, then the index register R[1] will contain the value $0134.


Fs29 - LDSPR - Load Index with Sprite

Load the index with the ROM sprite indicated in the source register. All ROM sprites are 5 bytes long, so the location of the specified sprite is its index multiplied by 5. See my post Writing a Chip 8 Emulator – Built In Font Set (Part 4) for more information on the load index with sprite operation.


Fs33 - BCD - Store Binary Coded Decimal

Converts the 8-bit number stored in the source register S into a decimal value and store the hundreds value at the index register, the tens value at the index + 1, and the ones at the index + 2. The operation is described as follows:

MEM[I]' = HUNDREDS VALUE
MEM[I+1]' = TENS VALUE
MEM[I+2]' = ONES VALUE

For example, if register R[1] contains the value $FF, and the index register I contains the value $0100, and the opcode $F133 is encountered, then memory location $0100 will contain the value $02, memory location $0101 will contain the value $05, and memory location $0102 will contain the value $05.


Fs55 - STOR - Store Registers in Index

Stores register values in memory starting at the location pointed to by the index register. The number of registers to store is contained in source register S. The operation is best described by an example.

If register R[1] contains the value $02, and the index register contains the value $0100, and the opcode $F155 is encountered, the memory location $0100 will contain the value of register R[0], and memory location $0101 will contain the value of register R[1].


Fs65 - READ - Read Stored Registers

Restores register values from memory starting at the location pointed to by the index register. The number of registers to restore is contained in source register S. The operation is best described by an example.

If register R[1] contains the value $02, and the index register contains the value $0100, and the opcode $F165 is encountered, then register R[0] will contain the value that exists at memory location $0100, and register R[1] will contain the value that exists at memory location $0101.


Conclusion

The Chip 8 hardware is very minimal, with a single program counter, 16 general purpose registers, index and stack pointer registers, as well as a delay and sound timer. The instruction set is very minimal, and is composed only of 16-bit words.

Tune in next time when I will go over the Super Chip 8 instruction set, and how the Super Chip 8 extends the original hardware!