Writing a Chip 8 Emulator - Instruction Set (Part 5)
Posted on Fri 07 September 2018 in 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!