Emulate a CPU
2022-01-08
TL;DR
I'm still reading 'Rust in Action' and in this article, I explain how to simulate the CHIP-8 system, a simple 1970s CPU. This article is useful for understanding the basics of system programming in Rust by both writing and explaining. I have added some functions while keeping the same style as 'Rust in Action'.
Toc
[
Hide]
CHIP-8 Introduction
Memory
Chip-8 is a very simple interpreted programming language that was popular in the 1970s and was initially used on the Cosmac VIP. It was implemented on 4K systems with 4096 (0x1000) memory locations, all of which are a byte in size. The interpreter occupies the first 512 bytes, and most programs begin at location 0x200 (512). The uppermost 256 bytes (0xF000 - 0xFFF) are reserved for display refresh, and the 96 bytes below are reserved for the call stack, internal use, and other variables.
Registers
It has 16 8-bit registers.
Stack
Originally it has 12 level of nesting but I implemented 16.
Timers
2 timers for display and for sounds.
Opcodes
35 opcodes which are all two bytes long and stored big-endian:
- NNN: address
- NN: 8-bit constant
- N: 4-bit constant
- X and Y: 4-bit register identifier
- PC: Program counter
- I: 16-bit register (for memory address)
- VN: One of the 16 variables. N may be 0 to F
| OPCODE | Explanation | Status |
|---|---|---|
| 0NNN | Calls machine code routine (RCA 1802 for COSMAC VIP) at address NNN | - |
| 00E0 | Clears the screen. | - |
| 00EE | Returns from a subroutine. | ok |
| 1NNN | Jumps to address NNN. | ok |
| 2NNN | Calls subroutine at NNN. | ok |
| 3XNN | Skips the next instruction if VX equals NN | ok |
| 4XNN | Skips the next instruction if VX does not equal NN | ok |
| 5XY0 | Skips the next instruction if VX equals VY | ok |
| 6XNN | Sets VX to NN. | ok |
| 7XNN | Adds NN to VX | ok |
| 8XY0 | Sets VX to the value of VY. | ok |
| 8XY1 | Sets VX to VX OR VY. | ok |
| 8XY2 | Sets VX to VX AND VY. | ok |
| 8XY3 | Sets VX to VX XOR VY. | ok |
| 8XY4 | Adds VY to VX. VF is set to 1 when there's a carry, and to 0 when there is not. | ok |
| 8XY5 | VY is subtracted from VX. VF is set to 0 when there's a borrow, and 1 when there is not. | ok |
| 8XY6 | Stores the least significant bit of VX in VF and then shifts VX to the right by 1. | ok |
| 8XY7 | Sets VX to VY minus VX. VF is set to 0 when there's a borrow, and 1 when there is not. | ok |
| 8XYE | Stores the most significant bit of VX in VF and then shifts VX to the left by 1. | ok |
| 9XY0 | Skips the next instruction if VX does not equal VY. | ok |
| ANNN | Sets I to the address NNN. | n |
| BNNN | Jumps to the address NNN plus V0. | n |
| CXNN | Sets VX to the result of a bitwise and operation on a random number (Typically: 0 to 255) and NN. | n |
| DXYN | Draws a sprite at coordinate (VX, VY) that has a width of 8 pixels and a height of N 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. | n |
| EX9E | Skips the next instruction if the key stored in VX is pressed | n |
| EXA1 | Skips the next instruction if the key stored in VX is not pressed. | n |
| FX07 | Sets VX to the value of the delay timer. | n |
| FX0A | A key press is awaited, and then stored in VX. | n |
| FX15 | Sets the delay timer to VX. | n |
| FX18 | Sets the sound timer to VX. | n |
| FX1E | Adds VX to I. VF is not affected. | n |
| FX29 | Sets I to the location of the sprite for the character in VX. Characters 0-F (in hexadecimal) are represented by a 4x5 font. | n |
| FX33 | Stores the binary-coded decimal representation of VX, with the hundreds digit in memory at location in I, the tens digit at location I+1, and the ones digit at location I+2. | n |
| FX55 | Stores from V0 to VX (including VX) in memory, starting at address I. The offset from I is increased by 1 for each value written, but I itself is left unmodified. | n |
| FX65 | Fills from V0 to VX (including VX) with values from memory, starting at address I. The offset from I is increased by 1 for each value read, but I itself is left unmodified. | n |
CPU Struct
struct CPU{
registers: [u8; 16],
position_in_memory: usize,
memory: [u8; 4096],
stack: [u16; 16],
stack_pointer: usize
}
the code is self-explanatory, as I said in TL;DR: there are 16 8-bit registers, a 4096 Ram and the stack can nest 16 ops. Position_in_memory is the program counter, is a register that contains the address of the current istruction.
Read the OPCODE
to read the every single opcode it is necessary to join two 8-bit cells, to do so the program get the first byte, it switches the byte to the left and add the second byte with a bit-a-bit OR.
let op_byte1 = self.memory[self.position_in_memory] as u16;
let op_byte2 = self.memory[self.position_in_memory + 1] as u16;
let opcode = op_byte1 << 8 | op_byte2;
to get the registers the operation is pretty the same, the program isolates the part of the cell with a bit-a-bit AND.
let x = ((opcode & 0x0F00) >> 8) as u8;
let y = ((opcode & 0x00F0) >> 4) as u8;
//let n = ((opcode & 0x000F) >> 4) as u8;
let kk = (opcode & 0x00FF) as u8;
let op_minor= (opcode & 0x000F) as u8;
let addr = opcode & 0x0FFF;
Execute the operations
once the program reads the opcode it can execute the program with a simple match expression:
match opcode {
0x000 => { return; },
0x00E0 => { /* Clear Screen */},
0x00EE => { self.ret(); },
0x1000..=0x1FFF => { self.jmp(addr); },
0x2000..=0x2FFF => { self.call(addr); },
0x3000..=0x3FFF => { self.se(x, kk); },
0x4000..=0x4FFF => { self.sne(x, kk); },
0x5000..=0x5FFF => { self.se(x, y); },
0x6000..=0x6FFF => { self.ld(x, kk); },
0x7000..=0x7FFF => { self.add_xy(x, kk); },
0x8000..=0x8FFF => {
match op_minor {
0 => { self.ld(x, self.registers[y as usize ])},
1 => { self.or_xy(x,y)},
2 => { self.and_xy(x, y) },
3 => { self.xor_xy(x, y) },
4 => { self.add_xy(x, y) },
5 => { self.sub_xy(x, y) },
6 => { self.lsb(x) },
7 => { self.sub_yx(x, y) },
8 => { self.msb(x) },
9 => { self.sne(x, y) },
_ => { todo!("opcode: {:04x}", opcode)},
}
},
_ => { todo!("opcode: {:04x}", opcode)},
}
Source
visit the Github Repo to see the full code.