模拟器架构¶
Chip8简介 介绍了 Chip8 的历史以及其基本组成。本文在此基础上,针对 Chip8 的各个组件的开始编码,构建 Chip8 模拟器的基本骨架。 接下来再根据 SPEC,把各个组件的功能实现完成。Chip8 模拟器就大功告成了。
组成结构¶
从结构图看到 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。不需要使用这一部分内存。
下面是内存的代码实现:
# 字体数据
_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 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 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
_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 实现完整的功能。