Writing an emulator (Chip-8)
Table of Contents
1. Chip-8 简介
Chip-8 是一种简单的虚拟机,由 Joseph Weisbecker 于 70 年代开发。在 Chip-8 Games Pack 中可以下载到一些基于 Chip-8 虚拟机的游戏 ROM,比如俄罗斯方块等等。
For TETRIS, W = left, E = right, R = fall faster, Q = rotate piece. For WIPEOFF, Q = left, E = right.
2. 实现 Chip-8 虚拟机
下面将先介绍 Chip-8 的基础设施,然后介绍如何实现 Chip-8 虚拟机。
2.1. Chip-8 基础设施
2.1.1. 指令集
Chip-8 有 35 条指令,如表 1 所示,每条指令占两字节,指令可参考 Chip-8 opcode
Opcode | Type | C Pseudo | Explanation |
---|---|---|---|
0NNN | Call | Calls machine code routine (RCA 1802 for COSMAC VIP) at address NNN. Not necessary for most ROMs. | |
00E0 | Display | disp_clear() | Clears the screen. |
00EE | Flow | return; | Returns from a subroutine. |
1NNN | Flow | goto NNN; | Jumps to address NNN. |
2NNN | Flow | *(0xNNN)() | Calls subroutine at NNN. |
3XNN | Cond | if(Vx==NN) | Skips the next instruction if VX equals NN. (Usually the next instruction is a jump to skip a code block) |
4XNN | Cond | if(Vx!=NN) | Skips the next instruction if VX doesn't equal NN. (Usually the next instruction is a jump to skip a code block) |
5XY0 | Cond | if(Vx==Vy) | Skips the next instruction if VX equals VY. (Usually the next instruction is a jump to skip a code block) |
6XNN | Const | Vx = NN | Sets VX to NN. |
7XNN | Const | Vx += NN | Adds NN to VX. (Carry flag is not changed) |
8XY0 | Assign | Vx=Vy | Sets VX to the value of VY. |
8XY1 | BitOp | Vx=Vx\vertVy | Sets VX to VX or VY. (Bitwise OR operation) |
8XY2 | BitOp | Vx=Vx&Vy | Sets VX to VX and VY. (Bitwise AND operation) |
8XY3 | BitOp | Vx=Vx^Vy | Sets VX to VX xor VY. |
8XY4 | Math | Vx += Vy | Adds VY to VX. VF is set to 1 when there's a carry, and to 0 when there isn't. |
8XY5 | Math | Vx -= Vy | VY is subtracted from VX. VF is set to 0 when there's a borrow, and 1 when there isn't. |
8XY6 | BitOp | Vx>>=1 | Stores the least significant bit of VX in VF and then shifts VX to the right by 1. |
8XY7 | Math | Vx=Vy-Vx | Sets VX to VY minus VX. VF is set to 0 when there's a borrow, and 1 when there isn't. |
8XYE | BitOp | Vx<<=1 | Stores the most significant bit of VX in VF and then shifts VX to the left by 1. |
9XY0 | Cond | if(Vx!=Vy) | Skips the next instruction if VX doesn't equal VY. (Usually the next instruction is a jump to skip a code block) |
ANNN | MEM | I = NNN | Sets I to the address NNN. |
BNNN | Flow | PC=V0+NNN | Jumps to the address NNN plus V0. |
CXNN | Rand | Vx=rand()&NN | Sets VX to the result of a bitwise and operation on a random number (Typically: 0 to 255) and NN. |
DXYN | Disp | draw(Vx,Vy,N) | 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 doesn’t 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 doesn’t happen |
EX9E | KeyOp | if(key()==Vx) | Skips the next instruction if the key stored in VX is pressed. (Usually the next instruction is a jump to skip a code block) |
EXA1 | KeyOp | if(key()!=Vx) | Skips the next instruction if the key stored in VX isn't pressed. (Usually the next instruction is a jump to skip a code block) |
FX07 | Timer | Vx = get_delay() | Sets VX to the value of the delay timer. |
FX0A | KeyOp | Vx = get_key() | A key press is awaited, and then stored in VX. (Blocking Operation. All instruction halted until next key event) |
FX15 | Timer | delay_timer(Vx) | Sets the delay timer to VX. |
FX18 | Sound | sound_timer(Vx) | Sets the sound timer to VX. |
FX1E | MEM | I +=Vx | Adds VX to I. VF is not affected. |
FX29 | MEM | I=sprite_addr[Vx] | Sets I to the location of the sprite for the character in VX. Characters 0-F (in hexadecimal) are represented by a 4x5 font. |
FX33 | BCD | set_BCD(Vx); *(I+0)=BCD(3); *(I+1)=BCD(2); *(I+2)=BCD(1); | Stores the binary-coded decimal representation of VX, with the most significant of three digits at the address in I, the middle digit at I plus 1, and the least significant digit at I plus 2. (In other words, take the decimal representation of VX, place the hundreds digit in memory at location in I, the tens digit at location I+1, and the ones digit at location I+2.) |
FX55 | MEM | reg_dump(Vx,&I) | Stores 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. |
FX65 | MEM | reg_load(Vx,&I) | Fills V0 to VX (including VX) with values from memory starting at address I. The offset from I is increased by 1 for each value written, but I itself is left unmodified. |
2.1.2. 寄存器
Chip-8 有 12 个 8 位的寄存器,记为 V0 到 VF,可以用下面数组模拟它:
unsigned char V[16];
Chip-8 有一个名为 I 的地址寄存器(相关指令为 ANNN、FX1E、FX29、FX55 和 FX65);此外,为了记录指令执行到哪里了,还需要一个 pc 寄存器。可以用下面变量模拟它们:
unsigned short I; unsigned short pc;
2.1.3. 内存
Chip-8 有 4K 内存:
+---------------+= 0xFFF (4095) End of Chip-8 RAM | | | | | | | | | | | 0x200 to 0xFFF| | Chip-8 | | Program / Data| | Space | | | | | | | | | +---------------+= 0x200 (512) Start of most Chip-8 programs | 0x000 to 0x1FF| | Reserved for | | interpreter | +---------------+= 0x000 (0) Start of Chip-8 RAM
用下面数组可模拟 4K 内存:
unsigned char memory[4096];
Chip-8 支持“过程调用”(相关指令为 2NNN 和 00EE),我们用栈来保存下一条指令地址,这样从“子过程”返回后可以接着执行。用大小为 16 的数组模拟栈,支持 16 层嵌套调用:
unsigned short stack[16]; unsigned short sp;
2.1.4. 显示屏
Chip-8 有个分辨率为 64×32 的显示屏,像素点仅显示两种颜色(如黑和白):
+--------------------------------------+ |(0,0) (63,0)| | | | | | | |(0,31) (63,31)| +--------------------------------------+
可以用下面一组数组模拟它:
unsigned char display[64 * 32];
2.1.5. 定时器
Chip-8 有两个定时器,delay timer(相关指令为 FX07 和 FX15)和 sound timer(相关指令为 FX18),它们都以 60 Hz 的频率减小(如果初值为 60,那么 1 秒就可减小到 0)。用两个变量可模拟它,程序中当它们大于 0 时就以一定的频率减小即可。
2.1.6. 键盘
Chip-8 支持 16 个按键(相关指令为 EX9E、EXA1 和 FX0A),其布局为下面左边所示,在模拟时,一般把它映射为右边所示布局:
Chip-8 Keypad Mapped Keyboard +-+-+-+-+ +-+-+-+-+ |1|2|3|C| |1|2|3|4| +-+-+-+-+ +-+-+-+-+ |4|5|6|D| |Q|W|E|R| +-+-+-+-+ => +-+-+-+-+ |7|8|9|E| |A|S|D|F| +-+-+-+-+ +-+-+-+-+ |A|0|B|F| |Z|X|C|V| +-+-+-+-+ +-+-+-+-+
2.2. 框架性代码
虚拟机的框架性代码如下所示:
#include "chip8.h" // Your cpu core implementation chip8 myChip8; int main(int argc, char **argv) { // Initialize the Chip8 system and load the game into the memory myChip8.loadGame("tetris"); // Emulation loop for(;;) { // Emulate one cycle myChip8.emulateCycle(); // If the draw flag is set, update the screen if(myChip8.drawFlag) drawGraphics(); } return 0; }
上面代码片断中的 for
无限循环模拟了虚拟机的不断执行。 emulateCycle
的功能是“获取”并“执行”指令,如:
void chip8::emulateCycle() { // Fetch opcode opcode = memory[pc] << 8 | memory[pc + 1]; // Decode opcode switch(opcode & 0xF000) { // Some opcodes // case 0xA000: // ANNN: Sets I to the address NNN // Execute opcode I = opcode & 0x0FFF; pc += 2; break; // More opcodes // default: printf ("Unknown opcode: 0x%X\n", opcode); } }
2.3. 实现源码
实现 Chip-8 模拟器是比较简单的,上节介绍的 emulateCycle 函数中已经实现了指令 ANNN,按照文档实现其它的指令也不难。
这里 https://github.com/10gic/chip8 是笔者的一个 JavaScript 实现,显示屏由 canvas API 来绘制。