模拟器架构

Chip8简介 介绍了 Chip8 的历史以及其基本组成。本文在此基础上,针对 Chip8 的各个组件的开始编码,构建 Chip8 模拟器的基本骨架。 接下来再根据 SPEC,把各个组件的功能实现完成。Chip8 模拟器就大功告成了。

组成结构

chip8-emulator-design-overview

从结构图看到 Chip8 的组件有 CPU,Memory,Display 和 Keyboard 四个主要的模块。使用 Python 定义这些组件为 class。模拟器的主要程序结构如下

src/tutorial/emulator/1_machine.py

class Memory:
    pass


class Display:

    def render(self):
        pass


class Keyboard:
    def poll_event(self):
        pass


class CPU:
    def __init__(self):
        self.memory = Memory()

    def cycle(self):
        pass


class Machine:
    def __init__(self):
        self.display = Display()
        self.keyboard = Keyboard()
        self.cpu = CPU()

    def run(self):
        while True:
            # cpu 指令循环
            self.cpu.cycle()
            # 监听处理键盘事件
            self.keyboard.poll_event()
            # 渲染屏幕
            self.display.render()


def main():
    machine = Machine()
    machine.run()

上述的代码是 Chip8 的基本骨架。与 pygame 的主框架非常相似。Machine 有处理器CPU,外设键盘Keyboard和外设显示器Display。 三个类组成。Machine 运行的时候,模拟器处于一个无限循环中,然后执行 CPU的指令循环,随后响应键盘事件和绘制屏幕。此时一个时钟流程完成,进入下一个周而复始的流程。

在 CPU 内部,内存 Memory 抽象为 CPU 的一个内部属性。这样方便 CPU 读写内存的操作。真实的硬件系统,CPU 通过内存总线和 Memory 进行通信。模拟器就忽略内存总线,直接把内存作为CPU的一个内部属性。

Memory

    +---------------+= 0x000 (0) Start of Chip-8 RAM 
    | 0x000 to 0x1FF|
    | Reserved for  |
    |  interpreter  |
    +---------------+= 0x200 (512) Start of most Chip-8 programs
    |               |
    |               |
    |               |    
    +- - - - - - - -+= 0x600 (1536) Start of ETI 660 Chip-8 programs
    |               |
    |               |
    |               |
    |               |
    |               |
    | 0x200 to 0xFFF|
    |     Chip-8    |
    | Program / Data|
    |     Space     |
    |               |
    |               |
    |               |
    +---------------+= 0xFFF (4095) End of Chip-8 RAM

如上图所示,Chip8 的内存是4-KB的大小。每一个内存单元大小为8-bit,即1-byte。编号从0x0000开始,最后一个是0x0FFF,从上往下,编号依次变大。一共可以分为4096(4 * 1024) 个内存单元。内存的地址大小可以使用12-bit大小。 地址的描述通常使用三位的十六进制表示。即 0x200,0xFFF 等。 1

  • 0x000 ~ 0x1FF: chip8 系统内存空间,模拟器则用这部分内容存储 Chip8 的Font数据。 2

font是0~f的的数据数字数据,每一个数字有 5-bytes 大小,8x5 个像素。如:

+---------------------+-----------------+--------------------+-------------------+  
| Symbol  | Address   | Sprite          | Binary             | Hex               |  
+=====================+=================+====================+===================+  
| 0       | 0x050     | ****            | 11110000           | 0xF0              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | ****            | 11110000           | 0xF0              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 1       | 0x055     |   *             | 00100000           | 0x20              |  
|         |           |  **             | 01100000           | 0x60              |  
|         |           |   *             | 00100000           | 0x20              |  
|         |           |   *             | 00100000           | 0x20              |  
|         |           |  ***            | 01110000           | 0x70              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 2       | 0x05A     | ****            | 11110000           | 0xF0              |  
|         |           |    *            | 00010000           | 0x10              |  
|         |           | ****            | 11110000           | 0xF0              |  
|         |           | *               | 10000000           | 0x80              |  
|         |           | ****            | 11110000           | 0xF0              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 3       | 0x05F     | ****            | 11110000           | 0xF0              |  
|         |           |    *            | 00010000           | 0x10              |  
|         |           | ****            | 11110000           | 0xF0              |  
|         |           |    *            | 00010000           | 0x10              |  
|         |           | ****            | 11110000           | 0xF0              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 4       | 0x064     | *  *            | 10010000           | 0x90              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | ****            | 11110000           | 0xF0              |  
|         |           |    *            | 00010000           | 0x10              |  
|         |           |    *            | 00010000           | 0x10              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 5       | 0x069     | ****            | 11110000           | 0xF0              |  
|         |           | *               | 10000000           | 0x80              |  
|         |           | ****            | 11110000           | 0xF0              |  
|         |           |    *            | 00010000           | 0x10              |  
|         |           | ****            | 11110000           | 0xF0              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 6       | 0x06E     | ****            | 11110000           | 0xF0              |  
|         |           | *               | 10000000           | 0x80              |  
|         |           | ****            | 11110000           | 0xF0              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | ****            | 11110000           | 0xF0              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 7       | 0x073     | ****            | 11110000           | 0xF0              |  
|         |           |    *            | 00010000           | 0x10              |  
|         |           |   *             | 00100000           | 0x20              |  
|         |           |  *              | 01000000           | 0x40              |  
|         |           |  *              | 01000000           | 0x40              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 8       | 0x078     | ****            | 11110000           | 0xF0              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | ****            | 11110000           | 0xF0              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | ****            | 11110000           | 0xF0              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 9       | 0x07D     | ****            | 11110000           | 0xF0              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | ****            | 11110000           | 0xF0              |  
|         |           |    *            | 00010000           | 0x10              |  
|         |           | ****            | 11110000           | 0xF0              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 10      | 0x082     | ****            | 11110000           | 0xF0              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | ****            | 11110000           | 0xF0              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | *  *            | 10010000           | 0x90              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 11      | 0x087     | ***             | 11100000           | 0xE0              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | ***             | 11100000           | 0xE0              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | ***             | 11100000           | 0xE0              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 12      | 0x08C     | ****            | 11110000           | 0xF0              |  
|         |           | *               | 10000000           | 0x80              |  
|         |           | *               | 10000000           | 0x80              |  
|         |           | *               | 10000000           | 0x80              |  
|         |           | ****            | 11110000           | 0xF0              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 13      | 0x091     | ***             | 11100000           | 0xE0              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | *  *            | 10010000           | 0x90              |  
|         |           | ***             | 11100000           | 0xE0              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 14      | 0x096     | ****            | 11110000           | 0xF0              |  
|         |           | *               | 10000000           | 0x80              |  
|         |           | ****            | 11110000           | 0xF0              |  
|         |           | *               | 10000000           | 0x80              |  
|         |           | ****            | 11110000           | 0xF0              |  
+---------+-----------+-----------------+--------------------+-------------------+  
| 15      | 0x09B     | ****            | 11110000           | 0xF0              |  
|         |           | *               | 10000000           | 0x80              |  
|         |           | ****            | 11110000           | 0xF0              |  
|         |           | *               | 10000000           | 0x80              |  
|         |           | *               | 10000000           | 0x80              |  
+---------+-----------+-----------------+--------------------+-------------------+
  • 0x200 ~ 0xE9F: chip8 程序可以随意使用的空间。实际上,0x200~0xFFF 都可以作为程序使用,现在的程序也很少有用到比0xE9F 还大的地址。

  • 0xEA0 ~ 0xEFF: 保留给栈以及其他内部应用。实现模拟器的时候,可以独立使用一个数据结构来作为栈,因此这一块内存可以给程序使用。

  • 0xF00 ~ 0xFFF: 保留给屏幕显示使用的 buffer。Chip8 的分辨率是 64 * 32,每一个 bit 一个像素。一共 256 个字节,对应内存地址为0xF00 ~ 0xFFF。与栈一样,可以独立使用一个数据结构来作为屏幕显示用到buffer。不需要使用这一部分内存。

下面是内存的代码实现:

class src.tutorial.03.2_memory_define.Memory[source]
# 字体数据
_FONTSET = [0xF0, 0x90, 0x90, 0x90, 0xF0,  # 0
            0x20, 0x60, 0x20, 0x20, 0x70,  # 1
            0xF0, 0x10, 0xF0, 0x80, 0xF0,  # 2
            0xF0, 0x10, 0xF0, 0x10, 0xF0,  # 3
            0x90, 0x90, 0xF0, 0x10, 0x10,  # 4
            0xF0, 0x80, 0xF0, 0x10, 0xF0,  # 5
            0xF0, 0x80, 0xF0, 0x90, 0xF0,  # 6
            0xF0, 0x10, 0x20, 0x40, 0x40,  # 7
            0xF0, 0x90, 0xF0, 0x90, 0xF0,  # 8
            0xF0, 0x90, 0xF0, 0x10, 0xF0,  # 9
            0xF0, 0x90, 0xF0, 0x90, 0x90,  # A
            0xE0, 0x90, 0xE0, 0x90, 0xE0,  # B
            0xF0, 0x80, 0x80, 0x80, 0xF0,  # C
            0xE0, 0x90, 0x90, 0x90, 0xE0,  # D
            0xF0, 0x80, 0xF0, 0x80, 0xF0,  # E
            0xF0, 0x80, 0xF0, 0x80, 0x80]  # F


class Memory:

    def __init__(self):
        # 4KB memory
        self._ram = [0] * 4 * 1024
        # stack
        self._stack = []
        for i in range(len(_FONTSET)):
            self._ram[i] = _FONTSET[i]

    def stack_pop(self):
        return self._stack.pop()

    def stack_push(self, x: int):
        return self._stack.append(x)

CPU

Chip8 的CPU主要用寄存器(register), 定时器(timer),按键缓存(key_pressed_buf),屏幕缓存(screen_buf)组成。

寄存器

  • 通用寄存器:16个 8-bit 的通用寄存器,记为V0 ~ VF,其中VF用来表示 flag 标记。可以用一个 16 长度数组表示,数组的每一个元素是一个寄存器,每个元素的类型是 uint8,当然 python 可以统一使用 int 表示

  • 程序计数器:一个 16-bit 的程序计数器,即PC寄存器。初始值为0x200。Chip8 的内存是4k,实际上PC的最大值是 12-bit。

  • 索引寄存器:一个 16-bit 的地址索引寄存器。

  • 栈顶寄存器:8-bit 的栈顶寄存器,指向当前栈顶。因为 Memory 实现使用了python的list接口,直接使用 append 入栈和 pop 出栈,这个寄存器可以忽略。

计时器

两个 8-bit 的定时器,一个 delay timer, 一个 sound timer,都按照 60hz 的频率递减直到 0。

按键缓存

使用一个大小 16 的列表作为按键缓存,映射了 Chip8 的按键,按下的键其值为 1,未按的键是 0。键盘按键的映射留在 Keyboard 组件实现的时候再介绍。

屏幕缓存

传统的 Chip8 使用内存的 0xF00 ~ 0xFFF 用作屏幕缓存。这里使用一个二维列表,作为 CPU 一个属性字段。

下面是 CPU 模块的定义代码实现:

class src.tutorial.03.3_cpu_define.CPU[source]
class CPU:
    def __init__(self):
        self.memory = Memory()
        # 8 bits register
        self._reg_V = [0] * 16

        # 16 bits register
        self._reg_PC = 0x200
        self._reg_I = 0

        self.draw_flag = False
        self._screen_buf = [[0 for _ in range(640)] for _ in range(320)]

        self._keys_pressed_buf = [0] * 16

        self._delay_timer = 0
        self._sound_timer = 0

    def cycle(self):
        pass

Display

用来显示的 Display 模块相对简单,它本身需要初始化 pygame 和 canvas 对象。然后提供绘图和渲染的方法。具体的方法实现留到下一节再介绍。当前只需要知道 Display 的初始化即可。

class src.tutorial.03.4_display_define.Display[source]
class Display:
    def __init__(self):
        pygame.init()
        pygame.display.set_caption(TITLE)
        self.canvas = pygame.display.set_mode((SCREEN_WIDTH * ZOOM, SCREEN_HEIGHT * ZOOM))
        self.canvas.fill(BG_COLOR)
        pygame.display.update()

Display 类进行 pygame 的初始化,设置canvas。

Keyboard

Keyboard 模块和 Display 模块类似,自身的没有属性,提供了pygame键盘处理的绑定方法而已。

键盘映射,左边是 Chip8 的按键,右边是键盘的按键。

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
class src.tutorial.03.5_keyboard_define.Keyboard[source]

_KEYS = {
    pygame.K_1: 0x1, pygame.K_2: 0x2, pygame.K_3: 0x3, pygame.K_4: 0xC,
    pygame.K_q: 0x4, pygame.K_w: 0x5, pygame.K_e: 0x6, pygame.K_r: 0xD,
    pygame.K_a: 0x7, pygame.K_s: 0x8, pygame.K_d: 0x9, pygame.K_f: 0xE,
    pygame.K_z: 0xA, pygame.K_x: 0x0, pygame.K_c: 0xB, pygame.K_v: 0xF
}

class Keyboard:
    def poll_event(self):
        pass

总结

CHIP8 模拟器定义为 Machine 类,它由 CPU,Memory,Keyboard 和 Display 几个部分组成。这也是冯诺依曼结构的基本组成。

模拟器的CPU由内存和寄存器组成,根据 Chip8 的 SPEC 定义了一些寄存器。

栈和视频缓存并没有使用内存对象,而是 CPU 内部独立的数据结构,这样实现比较方便,原理一致。

整个模拟器的工作流和 pygame 绘制图像的方式类似。也就是在主循环中进行 CPU 的 cycle 执行,处理键盘事件和绘制刷新屏幕。

至此,一个简单的模拟器(虚拟机)的工作原理介绍完毕。接下来就是针对 Chip8 的具体 SPEC,one step by step 实现完整的功能。


1

Chip8 的地址范围是0~4KB,使用12-bit的大小即可覆盖,可是编程语言没有12-bit大小数据结构,通常地址类型是一个2-byte大小的结构。如 golang 的 uint16 或 rust 中的 u16。

2

更多的font数据可以参考font