Writing an emulator (Chip-8)

Table of Contents

1. Chip-8 简介

Chip-8 是一种简单的虚拟机,由 Joseph Weisbecker 于 70 年代开发。在 Chip-8 Games Pack 中可以下载到一些基于 Chip-8 虚拟机的游戏 ROM,比如俄罗斯方块等等。

ROM: Source Code

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

Table 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 来绘制。

3. 参考

Author: cig01

Created: <2020-05-11 Mon>

Last updated: <2020-06-20 Sat>

Creator: Emacs 27.1 (Org mode 9.4)