Writing a Chip 8 Emulator (Part 2)
Posted on Thu 17 July 2014 in Emulation
In a previous post, I wrote about the basic CPU structure needed to emulate a Chip 8. I also wrote about memory access, and how to define bytes and words. In this post, I will talk about how to interpret Chip 8 instructions, as well as how to set up a main emulator loop.
The Execution Cycle
Every computer executes a simple loop over and over again while it is powered up. This loop consists of the following steps:
- Fetch
- Decode
- Execute
During the fetch portion of the loop, the CPU will grab the contents of memory where the Program Counter is currently pointing. This memory content indicates the instruction that the CPU should execute. For example, let’s say that the program counter points at memory location $0200
, and that the data $1100
is stored there (technically, $11
is stored in location $0200
, and $00
is stored in location $0201
). The fetch portion of the cycle will load the CPU with the instruction $1100
(more on what this instruction actually does a little later).
The decode portion of the loop translates the instruction into a form that the CPU understands. Each instruction has a specific number of operands that are associated with it. These are essentially parameters that describe what registers or memory locations the instruction should actually be executed on. In the case of the Chip 8, the operands are all part of the 16-bit word that is read from the Program Counter. For example, with the instruction $1100
, the $1
indicates that the instruction is a JUMP
instruction, while the 100 is the operand that relates to the address that the CPU needs to jump to.
The execute portion of the loop actually executes the instruction on the operands that were previously decoded. In the case of the decoded JUMP
instruction $1100
, the CPU will advance the program counter to the memory location $100
.
The computer will continue performing this loop over and over again until the power is turned off. Now that we understand what we need to do in the main loop, let’s look at how we decode the different instructions.
Anatomy of a Chip 8 Instruction
Chip 8 instructions are actually really nice to work with. Each instruction that the CPU can execute is exactly two bytes long, making them a 16-bit word in length. All of the data needed to execute the instruction is stored within this 16-bit word. Let’s take a look at a simple instruction and dissect how the operation works.
The Jump Instruction
The Chip 8 JUMP
instruction tells the CPU to move the Program Counter to the address specified. The JUMP
instruction is encoded as follows:
1nnn
The n
characters are used to denote 4-bit numbers. When combined, the numbers form a 12-bit address that the CPU should jump to. For example, if the CPU encountered the instruction $1234
then the CPU would set the program counter to $234
. During the next loop of the CPU, the next instruction would be fetched from location $234
.
The Load Operation
Let’s look at another example. The LOAD
instruction tells the CPU to load a specific number into a register. The LOAD
instruction is encoded as follows:
6snn
The s
character is used to denote a 4-bit value that specifies a register to store the number in. The n
characters denote 4-bit numbers that, when combined, indicate the 8-bit value that should be loaded into that register. For example, if the CPU encountered the instruction $6248
the CPU would load the value $48
into register 2.
Chip 8 Operands
We can start to see patterns in the way a Chip 8 instruction is encoded. The first hex digit of the instruction usually provides a hint at what major operation is about to occur. The next three hex digits encode numeric information, or the registers that the operations work on. Here is a mostly complete set of Chip 8 instructions:
00E0
- Clear the screen00EE
- Return from subroutine1nnn
- Jump to addressnnn
2nnn
- Call routine at addressnnn
3snn
- Skip next instruction if registers
value equalsnn
4snn
- Do not skip next instruction if registers
value equalsnn
5st0
- Skip if registers
value equals registert
value6snn
- Load registers
with valuenn
7snn
- Add valuenn
to registers
8st0
- Move value from registers
to registert
8st1
- Perform logical OR on registers
andt
and store int
8st2
- Perform logical AND on registers
andt
and store int
8st3
- Perform logical XOR on registers
andt
and store int
8st4
- Adds
tot
and store ins
- register F set on carry8st5
- Subtracts
fromt
and store ins
- register F set on !borrow8s06
- Shift bits in registers
1 bit to the right - bit 0 shifts to register F8s0E
- Shift bits in registers
1 bit to the left - bit 7 shifts to register F9st0
- Skip next instruction if registers
not equal registert
Annn
- Load index with valuennn
Bnnn
- Jump to addressnnn
+ indexCtnn
- Generate random number between 0 andnn
and store int
Dstn
- Drawn
byte sprite at x location regs
, y location regt
Ft07
- Move delay timer value into registert
Ft0A
- Wait for keypress and store in registert
Fs15
- Load delay timer with value in registers
Fs18
- Load sound timer with value in registers
Fs1E
- Add value in registers
to indexFs29
- Load index with sprite from registers
Fs33
- Store the binary coded decimal value of registers
at indexFs55
- Store the values of registers
registers at indexFs65
- Read back the stored values at index into registers
Bit Shifting and Bit Masks
So, when writing an emulator, how do you separate out the various operands? The simple way is through bit masks and bit shifting. A bit mask is a binary pattern, usually used to turn certain bits on or off, or used to determine if a particular bit pattern is activated. Bit shifting is where the bits in a pattern are moved left or right by a certain number of spaces. Let's work through a simple mask and shift example.
Let's say that we had the hex number $23
. In binary, this would be 0010 0011
. The 0010
corresponds to the 2, while the 0011
corresponds to the 3. Now let's say you want to get just the hex value of the first digit. What we do is 1) apply a bit mask with a logical AND operation, and 2) shift the bits.
For the first step, what we want to do is isolate the high 4-bits in the pattern, and blank everything else out. We do that by applying a bit mask that has all ones in the high 4-bits. In binary, this would be1111 0000
- hex it is $F0
. Now that we know the mask value we need to apply, the next thing we do is apply a logical AND operation to $23
and $F0
. The way an AND works is if both digits are a 1, then a 1 is the result, otherwise, the value is a 0. When we apply the AND:
0010 0011
1111 0000
--------- &
0010 0000
The result we are left with is 0010 0000
, which is the hex value of $20
. Now that we have blanked everything else out, we perform the second step, which is the logical shift. We want to move all of the bits of the result 4 places to the left. In a language like C, this is done with the >>
operator.
0010 0000
--------- >> 4
0000 0010
This leaves us with 0000 0010
which is the hex value of $02
, which is exactly what we wanted!
Shifting and Masking Chip 8 Instructions
Let's take a look at the Chip 8 instruction to move a value of one register into another. This corresponds to the instruction 8st0
. This says to move the value in register s
into the register t
. An example of this would be $8620
, which would make the Chip 8 CPU move the value in register 6 into register 2.
The first thing we need is the value for s
. Using a bit mask, we want the following:
1000 0110 0010 0000
0000 1111 0000 0000
------------------- &
0000 0110 0000 0000
Once again, the magic hex value is $F
for the 4-bit pattern 1111
. The same operation in hex gives us:
$ 8 6 2 0
$ 0 F 0 0
------- &
$ 0 6 0 0
This gives us the result $0600
. Now we need to shift the bits 8 places to the left this time:
0000 0110 0000 0000
------------------- >> 8
0000 0000 0000 0110
The same operation in hex gives us:
$ 0 6 0 0
------- >> 8
$ 0 0 0 6
So now we know that the s
value should be $6! We do the same thing for the t
register, except this time we only need to perform a shift of 4 places to the left. The hex operations are as follows:
$ 8 6 2 0
$ 0 0 F 0
------- &
$ 0 0 2 0
------- >> 4
$ 0 0 0 2
This tells us that the t
value should be $2
!
The key points to take away here are:
- Groups of 4-bits correspond to a single hex number. For example,
1001
corresponds to the hex digit$9
. - Bit masks allow you to figure out specific bit patterns in a larger bit sequence.
- When performing bit shifting hex values, always shift in multiples of 4. So if you want to get the
$6
in$8620
, you need to shift 2 hex digits to the left, which is 8 binary bits to the left (2 x 4). - If you are decoding the
JUMP
instruction, which is1nnn
, you don’t need to shift anything! Just apply the bit mask of$0FFF
and you have the address you need to jump to.
Conclusion
In this post, we looked at the execution loop, and examined how Chip 8 instructions are encoded. We also looked at bit shifting and bit masks, and how those two simple tools can be used during the decoding phase. In a future post, I’ll look at some of the Chip 8 instructions in more detail, as well as how to code the execution loop in a language like C.