
CHIP-8是一个解释型语言,由Joseph Weisbecker开发。最初CHIP-8在上个世纪70年代被使用在COSMAC-VIP和Telmac 1800上。许多游戏如Pong,Space Invaders,Tetris都曾使用该语言编写。
我们今天用 Rust
来模拟实现这个 CHIP-8
CHIP-8 虚拟机构成
-
内存 Memory
: CHIP-8最常在4K系统上实现,如Cosmac VIP和Telmac 1800。这些机器有4096 (0x1000)内存位置,所有这些位置都是8位(字节),这就是 CHIP-8
的起源。但是,CHIP-8
解释器本身占用这些机器上的前 512
字节内存空间。由于这个原因,为原始系统编写的大多数程序开始于位置 512 (0x200)
的内存,不访问位置 512 (0x200)
以下的任何内存。最上面的 256
个字节( 0xF00
-0xFFF
)被保留用于显示刷新,下面的96个字节(0xEA0
-0xEFF
)被保留用于调用堆栈、内部使用和其他变量。
-
寄存器 Registers
: 16
个名为V0
到 VF
的 8
位数据寄存器。VF
寄存器可以作为一些指令的标志,在加法操作中,VF
是进位标志,而在减法操作中,它是“no borrow”标志。在绘制指令中,VF
设置为像素冲突。
地址寄存器,它被命名为I
,是16
位宽的,并且与几个涉及内存操作的操作码一起使用。
-
栈 Stack
: 堆栈仅用于在调用子例程时存储返回地址。
-
计时器 Timers
:
- 延迟计时器: 这个计时器是用来计时的比赛事件。可以设置和读取它的值。
- 声音定时器: 这个定时器用于声音效果。当它的值非零时,会发出哔哔声。
-
输入 Input
: 输入使用十六进制
键盘,有16个从0到f的键。“8”、“4”、“6”和“2”键通常用于定向输入。三个操作码用于检测输入。如果按了特定的键,一个跳过指令,而如果没有按特定的键,另一个也会这样做。第三个等待按键,然后将其存储在一个数据寄存器中。
-
图像 Graphics and sound
: 分辨率为 64×32
像素,单色。图形被绘制到屏幕单独的 sprites
。
-
操作符: CHIP-8
有 35
个操作符,详细的见 Opcode table
初始化结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| pub struct Cpu { pub i: u16,
pub pc: u16,
pub memory: [u8; 4096],
pub v: [u8; 16],
pub display: Display,
pub stack: [u16; 16],
pub sp: u8,
pub delay_timer: u8, }
|
所有的硬件设备我们都用软件来模拟,因为 Display
相对独立,因此我们将这部分抽象出来。
CPU
的执行就是一个大循环,不断的读取指令再运行。因为 CHIP-8
的操作码均是 u16
的,所以我们直接读取操作码,因此增加一个辅助函数来获得一个字节的数据。

整个处理过程是非常的清楚易懂的。
读取一个字节1 2 3 4
| fn read_word(memory: [u8; 4096], index: u16) -> u16 { (memory[index as usize] as u16) << 8 | (memory[(index + 1) as usize] as u16) }
|
执行循环就很容易写出来了
执行循环1 2 3 4 5 6 7 8
| impl Cpu { pub fn run(&mut self) { let opcode = read_word(self.memory, self.pc); self.process_opcode(opcode); }
fn process_opcode(&mut self, opcode: u16) {} }
|
对于 CPU
是常驻对象,将其静态化起来非常的合理,这里用了 lazy_static
非常方便的一个 lib
初始化1 2 3 4 5 6 7 8 9 10 11 12
| lazy_static! { pub static ref CPU: Mutex<Cpu> = Mutex::new(Cpu { i: 0, pc: 0, memory: [0; 4096], v: [0; 16], display: Display {}, stack: [0; 16], sp: 0, delay_timer: 0, }); }
|
对于所有的硬件系统我们都有时钟周期,也就是多久运行一次操作,我么也将这部分抽象出来。
执行循环1 2 3 4 5 6 7 8 9
| pub fn emulate_cycle(&mut self) -> bool { if self.pc >= 4094 { return true; } let op = read_word(self.memory, self.pc); self.process_opcode(op); false }
|
这里的话我们等待每一次的时钟周期触发,会触发这个回调,那我们的主循环就很好写了。
过下文我们会把自己嵌入别人的系统中。
main loop1 2 3 4 5 6
| fn main() { loop { cpu::execute_cycle(); sleep(Duration::from_secs(1)); } }
|
这里我们用一个 sleep
来假装我们的时钟周期,我们还是尽快进入我们的核心的模拟器的部分,这部分内容我们在后续再进行优化。
模拟器
模拟器的核心就是来实现我们的 Opcode table,我们先来一个简单的试试水
0NNN 操作符不怎么使用,这里就不实现了
00E0 disp_clear
对于这种常量的处理,我们直接进行操作即可
disp_clear1 2 3 4 5 6
| match opcode { 0x00E0 => { self.display.clear() } _ => {} }
|
1NNN goto NNN
这种直接跳转的处理逻辑也非常的轻松
1 2 3 4 5 6
| match opcode { 0x1000...0x1FFF => { self.pc = opcode & 0x0FFF } _ => {} }
|
使用 &
将最高位的操作符 1
掩盖,然后将后 3
位赋值给 PC
即可。下面我们搞点难的
函数调用
2NNN Calls subroutine at NNN.
函数调用,显然需要压栈,call nnn
也就是移动到 nnn
,但是在此之前记得保留上一次的 pc
计数器,对于 CHIP8
仅仅存存 Ret
地址,不会保留其他的寄存信息。
call1 2 3 4 5 6
| 0x2000..0x2FFF => { let nnn = opcode & 0x0FFF; self.stack[self.sp as usize] = self.pc; self.sp = self.sp + 1; self.pc = nnn; }
|
00EE return
那么 ret
函数就更简单了,还原下 SP
即可
ret1 2 3 4
| 0x00EE => { self.sp = self.sp - 1; self.pc = self.stack[self.sp as usize] }
|
条件语句
这里就不多写了,直接就写一个示例 3XNN if(Vx==NN)
if eq than jump1 2 3 4 5 6 7
| 0x3000..0x3FFF => { let register_index = (opcode & 0x0F00) >> 8; let nn = (opcode & 0x00FF); if self.v[register_index] == nn { self.sp += 2 } }
|
宏
到这里我们发现很多操作是可以重复的,在 Java
里面我们就会抽一个函数出来,在 Rust
中,我们有强大的 宏
,我们直接使用 宏
来构建这些通用的函数。
比如我们经常要取 X
作为寄存器的下标
arg_x1 2 3 4 5
| macro_rules! arg_x { ($opcode:expr) => { (($opcode & 0x0F00) >> 8) as usize }; }
|
其他可以依次类推,因此上文获得寄存器的代码可以改为
1 2 3 4 5 6 7
| 0x3000..0x3FFF => { let register_index = arg_x!(optcode); let nn = (opcode & 0x00FF); if self.v[register_index] == nn { self.sp += 2 } }
|
最终效果
process_opcode函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
| fn process_opcode(&mut self, opcode: u16) { self.pc += 2;
match opcode { 0x00E0 => { self.display.clear() } 0x00EE => { self.sp = self.sp - 1; self.pc = self.stack[(self.sp) as usize] + 2 } 0x1000..=0x1FFF => { self.pc = arg_nnn!(opcode) } 0x2000..=0x2FFF => { self.stack[self.sp as usize] = self.pc - 2; self.sp = self.sp + 1; self.pc = arg_nnn!(opcode); } 0x3000..=0x3FFF => { if self.v[arg_x!(opcode)] == arg_nn!(opcode) { self.pc += 2 } } 0x4000..=0x4FFF => { if self.v[arg_x!(opcode)] != arg_nn!(opcode) { self.pc += 2 } } 0x5000..=0x5FFF => { if self.v[arg_x!(opcode)] == self.v[arg_y!(opcode)] { self.pc += 2 } } 0x6000..=0x6FFF => { self.v[arg_x!(opcode)] = arg_nn!(opcode); } 0x7000..=0x7FFF => { self.v[arg_x!(opcode)] += arg_nn!(opcode); }
0x8000..=0x8FFF => { match opcode & 0x000F { 0 => { self.v[arg_x!(opcode)] = self.v[arg_y!(opcode)] } 1 => { self.v[arg_x!(opcode)] |= self.v[arg_y!(opcode)] } 2 => { self.v[arg_x!(opcode)] &= self.v[arg_y!(opcode)] } 3 => { self.v[arg_x!(opcode)] ^= self.v[arg_y!(opcode)] } 4 => { let (res, overflow) = self.v[arg_x!(opcode)].overflowing_add(self.v[arg_y!(opcode)]); match overflow { true => self.v[0xF] = 1, false => self.v[0xF] = 0, } self.v[arg_x!(opcode)] = res; } 5 => { let (res, overflow) = self.v[arg_x!(opcode)].overflowing_sub(self.v[arg_y!(opcode)]); match overflow { true => self.v[0xF] = 0, false => self.v[0xF] = 1, } self.v[arg_x!(opcode)] = res; } 6 => { self.v[0xF] = self.v[arg_x!(opcode)] & 0x1; self.v[arg_x!(opcode)] >>= 1; } 7 => { let (res, overflow) = self.v[arg_y!(opcode)].overflowing_sub(self.v[arg_x!(opcode)]); match overflow { true => self.v[0xF] = 0, false => self.v[0xF] = 1, } self.v[arg_x!(opcode)] = res; } 0xE => { self.v[0xF] = self.v[arg_x!(opcode)] & 0x80; self.v[arg_x!(opcode)] <<= 1; } _ => {} } } 0x9000..=0x9FF0 => { if self.v[arg_x!(opcode)] != self.v[arg_y!(opcode)] { self.sp += 2 } } 0xA000..=0xAFFF => { self.i = arg_nnn!(opcode) } 0xB000..=0xBFFF => { self.pc = self.v[0] as u16 + arg_nnn!(opcode) } 0xC000..=0xCFFF => { let rand: u8 = rand::random::<u8>(); self.v[arg_x!(opcode)] = rand & arg_nn!(opcode) } 0xD000..=0xDFFF => { let collision = self.display.draw(self.v[arg_x!(opcode)] as usize, self.v[arg_y!(opcode)] as usize, &self.memory[self.i as usize..(self.i + arg_n!(opcode) as u16) as usize]); self.v[0xF] = if collision { 1 } else { 0 }; } 0xE000..=0xEFFF => { if arg_nn!(opcode) == 0x9E { self.pc += match self.keypad.get_key_state(self.v[arg_x!(opcode)] as usize) { Keystate::Pressed => 2, Keystate::Released => 0, }; } else if arg_nn!(opcode) == 0xA1 { self.pc += match self.keypad.get_key_state(self.v[arg_x!(opcode)] as usize) { Keystate::Pressed => 0, Keystate::Released => 2, }; } } 0xF000..=0xFFFF => { match opcode & 0x00FF { 0x07..=0x07 => { self.v[arg_x!(opcode)] = self.delay_timer } 0x0A => { self.wait_for_key = (true, arg_x!(opcode) as u8); self.pc -= 2; } 0x15 => { self.delay_timer = self.v[arg_x!(opcode)] } 0x18 => { self.sound_timer = self.v[arg_x!(opcode)] } 0x1E => { self.i += self.v[arg_x!(opcode)] as u16 } 0x29 => { self.i = self.v[arg_x!(opcode)] as u16 * 5 } 0x33 => { self.memory[self.i as usize] = self.v[arg_x!(opcode)] / 100; self.memory[self.i as usize + 1] = (self.v[arg_x!(opcode)] / 10) % 10; self.memory[self.i as usize + 2] = (self.v[arg_x!(opcode)] % 100) % 10; } 0x55 => { self.memory[(self.i as usize)..(self.i + arg_x!(opcode) as u16 + 1) as usize] .copy_from_slice(&self.v[0..(arg_x!(opcode) as usize + 1)]) } 0x65 => { self.v[0..(arg_x!(opcode) as usize + 1)] .copy_from_slice(&self.memory[(self.i as usize)..(self.i + arg_x!(opcode) as u16 + 1) as usize]) } _ => println!("got unknown opcode: {}", opcode) } } _ => { println!("got unknown opcode: {}", opcode) } } }
|
下面我们进入我们的绘图系统
绘图
因为要对接 GUI
觉得有点烦,所以我就索性将 rust-chip8
将作为我们系统的模板,此项目已经帮我们完善了绝大多数的部分,并且使用的 SDL2
作为我们底层的依赖,因此我们把作文题变成填空题了。
rust-chip8 目录结构1 2 3 4 5 6 7 8 9 10 11
| ├── src │ ├── bin │ │ ├── chip8app.rs │ │ ├── chip8app_sdl2.rs │ │ ├── input.rs │ │ └── main.rs │ ├── display.rs │ ├── keypad.rs │ ├── lib.rs │ ├── vm.rs │ └── vm_test.rs
|
我们要替换掉 display | keypad | vm
这三个部分
对于 Display
的定义很简单
display1 2 3 4 5
| #[derive(Clone)] pub struct Display { pub gfx: [[u8; DISPLAY_WIDTH]; DISPLAY_HEIGHT], pub dirty: bool, }
|
核心的绘图其实只有一个函数
Draw1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| impl Display { pub fn draw(&mut self, xpos: usize, ypos: usize, sprite: &[u8]) -> bool { let mut collision = false; let h = sprite.len();
for j in 0..h { for i in 0..8 { let y = (ypos + j) % DISPLAY_HEIGHT; let x = (xpos + i) % DISPLAY_WIDTH;
if (sprite[j] & (0x80 >> i)) != 0x00 { if self.gfx[y][x] == 0x01 { collision = true; } self.gfx[y][x] ^= 0x01; } } } self.dirty = true;
collision } }
|
其他
对于键盘的输入输出,在我们的执行 VM
的 Loop
循环中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| pub fn exec_vm( vm: &mut Vm, cpu_clock: u32, tx: Sender<Chip8UICommand>, rx: Receiver<Chip8VMCommand>, ) { 'vm: loop { match rx.try_recv() { Ok(vm_command) => match vm_command { UpdateRunStatus(run) => running = run, UpdateKeyStatus(index, state) => match state { Keystate::Pressed => { if waiting_for_key && (index != wait_for_key_last_pressed) { vm.end_wait_for_key(index); wait_for_key_last_pressed = index; } else { vm.keypad.set_key_state(index, state); } } Keystate::Released => { wait_for_key_last_pressed = 0xFF; if !waiting_for_key { vm.keypad.set_key_state(index, state); } } } }, _ => {} } } }
|
我们通过 Channel
来消费我们的键盘键入和键出。而这个 Channel
的入口是在
Chip8EmulatorBackend exec1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| fn exec( &mut self, config: &Chip8Config, tx: Sender<Chip8VMCommand>, rx: Receiver<Chip8UICommand>, ) {
'main: loop { for event in event_pump.poll_iter() { match event { Event::Quit { .. } => { paused = true; tx.send(Quit).unwrap(); } Event::KeyDown { keycode, .. } => { if keys_pressed.contains(&keycode) { continue; } match keycode.unwrap() { Keycode::Escape => { paused = true; tx.send(Quit).unwrap(); } Keycode::Return => { tx.send(UpdateRunStatus(paused)).unwrap(); paused = !paused; } Keycode::Backspace => { info!("Reinitializing the virtual machine."); tx.send(Reset).unwrap(); } _ => { if !paused { if let Some(index) = key_binds.get(&keycode.unwrap()) { tx.send(UpdateKeyStatus(*index, Pressed)).unwrap(); } } } } keys_pressed.push(keycode); } Event::KeyUp { keycode, .. } => { for i in 0..keys_pressed.len() { if keys_pressed[i] == keycode { keys_pressed.remove(i); break; } } if let Some(index) = key_binds.get(&keycode.unwrap()) { tx.send(UpdateKeyStatus(*index, Released)).unwrap(); } } _ => continue, } } }
|
让我们看看最终效果吧
最终源码: https://github.com/yanickxia/impl-chip8

参考