基本指令

屏幕绘制 一节,我们通过指定屏幕二维数组 screen_buf 在屏幕显示了 IBM 的图案。本节的目标就是使用 Chip8 加载 ibm 的 rom 文件,通过 CPU 的解析执行 Chip8 指令,来显示 IBM 的图像。

二进制

chip8 的 rom 文件都是二进制文件。使用 python 可以很方便的读取。下面是代码:

src.tutorial.06.1_load_rom.load_file(filepath: str) Optional[List[int]][source]
def load_file(filepath: str) -> Optional[List[int]]:
    try:
        data = []
        with open(filepath, "rb") as f:
            file_bytes = f.read()
            for i in range(len(file_bytes)):
                data.append(int(file_bytes[i]))
    except Exception as e:
        print(f"Error: {e}")
        sys.exit()
    return data

Machine 类也增加load_rom方法

Machine.load_rom(rom_file: str)[source]
class Machine:

    def load_rom(self, rom_file: str):
        data = load_file(rom_file)
        self.cpu.load_rom(data)

Memory 类增加读取 rom 到ram中

src.tutorial.06.1_load_rom.Memory.write(self, data: List[int])
    def write(self, data: List[int]):
        for i in range(len(data)):
            self._ram[START_ADDR + i] = int(data[i])

chip8用户程序的内存地址是0x200开始的,因此读取rom的数据也从0x200开始。

IBM

使用十六进制编辑器可以打开IBM.bin的rom文件,然后看到其数据为 :

0x00, 0xe0, 0xa2, 0x2a, 0x60, 0x0c, 0x61, 0x08, 0xd0, 0x1f, 0x70, 0x09, 0xa2, 0x39, 0xd0, 0x1f,
0xa2, 0x48, 0x70, 0x08, 0xd0, 0x1f, 0x70, 0x04, 0xa2, 0x57, 0xd0, 0x1f, 0x70, 0x08, 0xa2, 0x66,
0xd0, 0x1f, 0x70, 0x08, 0xa2, 0x75, 0xd0, 0x1f, 0x12, 0x28, 0xff, 0x00, 0xff, 0x00, 0x3c, 0x00,
0x3c, 0x00, 0x3c, 0x00, 0x3c, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00, 0xff, 0x00, 0x38, 0x00, 0x3f,
0x00, 0x3f, 0x00, 0x38, 0x00, 0xff, 0x00, 0xff, 0x80, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0x80, 0x00,
0x80, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0x80, 0xf8, 0x00, 0xfc, 0x00, 0x3e, 0x00, 0x3f, 0x00, 0x3b,
0x00, 0x39, 0x00, 0xf8, 0x00, 0xf8, 0x03, 0x00, 0x07, 0x00, 0x0f, 0x00, 0xbf, 0x00, 0xfb, 0x00,
0xf3, 0x00, 0xe3, 0x00, 0x43, 0xe0, 0x00, 0xe0, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80,
0x00, 0xe0, 0x00, 0xe0

反汇编为如下的指令:

start:
             0x0200     CLS
             0x0202     LD I, lbl_0x022a
             0x0204     LD V0, 0x0c
             0x0206     LD V1, 0x08
             0x0208     DRW V0, V1, 0x0f
             0x020a     ADD V0, 0x09
             0x020c     LD I, lbl_0x0239
             0x020e     DRW V0, V1, 0x0f
             0x0210     LD I, lbl_0x0248
             0x0212     ADD V0, 0x08
             0x0214     DRW V0, V1, 0x0f
             0x0216     ADD V0, 0x04
             0x0218     LD I, lbl_0x0257
             0x021a     DRW V0, V1, 0x0f
             0x021c     ADD V0, 0x08
             0x021e     LD I, lbl_0x0266
             0x0220     DRW V0, V1, 0x0f
             0x0222     ADD V0, 0x08
             0x0224     LD I, lbl_0x0275
             0x0226     DRW V0, V1, 0x0f
lbl_0x0228:
             0x0228     JP lbl_0x0228
lbl_0x022a:
             0x022a     DB 0xff    ; 11111111
             0x022b     DB 0x00    ;
             0x022c     DB 0xff    ; 11111111
             0x022d     DB 0x00    ;
             0x022e     DB 0x3c    ;   1111
             0x022f     DB 0x00    ;
             0x0230     DB 0x3c    ;   1111
             0x0231     DB 0x00    ;
             0x0232     DB 0x3c    ;   1111
             0x0233     DB 0x00    ;
             0x0234     DB 0x3c    ;   1111
             0x0235     DB 0x00    ;
             0x0236     DB 0xff    ; 11111111
             0x0237     DB 0x00    ;
             0x0238     DB 0xff    ; 11111111
lbl_0x0239:
             0x0239     DB 0xff    ; 11111111
             0x023a     DB 0x00    ;
             0x023b     DB 0xff    ; 11111111
             0x023c     DB 0x00    ;
             0x023d     DB 0x38    ;   111
             0x023e     DB 0x00    ;
             0x023f     DB 0x3f    ;   111111
             0x0240     DB 0x00    ;
             0x0241     DB 0x3f    ;   111111
             0x0242     DB 0x00    ;
             0x0243     DB 0x38    ;   111
             0x0244     DB 0x00    ;
             0x0245     DB 0xff    ; 11111111
             0x0246     DB 0x00    ;
             0x0247     DB 0xff    ; 11111111
lbl_0x0248:
             0x0248     DB 0x80    ; 1
             0x0249     DB 0x00    ;
             0x024a     DB 0xe0    ; 111
             0x024b     DB 0x00    ;
             0x024c     DB 0xe0    ; 111
             0x024d     DB 0x00    ;
             0x024e     DB 0x80    ; 1
             0x024f     DB 0x00    ;
             0x0250     DB 0x80    ; 1
             0x0251     DB 0x00    ;
             0x0252     DB 0xe0    ; 111
             0x0253     DB 0x00    ;
             0x0254     DB 0xe0    ; 111
             0x0255     DB 0x00    ;
             0x0256     DB 0x80    ; 1
lbl_0x0257:
             0x0257     DB 0xf8    ; 11111
             0x0258     DB 0x00    ;
             0x0259     DB 0xfc    ; 111111
             0x025a     DB 0x00    ;
             0x025b     DB 0x3e    ;   11111
             0x025c     DB 0x00    ;
             0x025d     DB 0x3f    ;   111111
             0x025e     DB 0x00    ;
             0x025f     DB 0x3b    ;   111 11
             0x0260     DB 0x00    ;
             0x0261     DB 0x39    ;   111  1
             0x0262     DB 0x00    ;
             0x0263     DB 0xf8    ; 11111
             0x0264     DB 0x00    ;
             0x0265     DB 0xf8    ; 11111
lbl_0x0266:
             0x0266     DB 0x03    ;       11
             0x0267     DB 0x00    ;
             0x0268     DB 0x07    ;      111
             0x0269     DB 0x00    ;
             0x026a     DB 0x0f    ;     1111
             0x026b     DB 0x00    ;
             0x026c     DB 0xbf    ; 1 111111
             0x026d     DB 0x00    ;
             0x026e     DB 0xfb    ; 11111 11
             0x026f     DB 0x00    ;
             0x0270     DB 0xf3    ; 1111  11
             0x0271     DB 0x00    ;
             0x0272     DB 0xe3    ; 111   11
             0x0273     DB 0x00    ;
             0x0274     DB 0x43    ;  1    11
lbl_0x0275:
             0x0275     DB 0xe0    ; 111
             0x0276     DB 0x00    ;
             0x0277     DB 0xe0    ; 111
             0x0278     DB 0x00    ;
             0x0279     DB 0x80    ; 1
             0x027a     DB 0x00    ;
             0x027b     DB 0x80    ; 1
             0x027c     DB 0x00    ;
             0x027d     DB 0x80    ; 1
             0x027e     DB 0x00    ;
             0x027f     DB 0x80    ; 1
             0x0280     DB 0x00    ;
             0x0281     DB 0xe0    ; 111
             0x0282     DB 0x00    ;
             0x0283     DB 0xe0    ; 111

地址 0x0200 ~ 0x0228 是 rom 的指令,0x22a ~ 0x283 是数据。 从数据可以看到,chip8 以 sprite 方式绘图。10x22a ~ 0x23815-byte的sprite。 Sprite绘图方式将在后面介绍。

由此可见,想要打印 IBM 的突然,只需要实现下面几个指令:

  • 00E0: 清屏 CLS

  • 1NNN: 跳转 JP NNN

  • 6XKK: 设置寄存器 LD Vx, KK

  • 7XKK: 将值添加到寄存器 ADD Vx, KK

  • ANNN: 设置地址寄存器 LD I, NNN

  • DXYN: 显示/绘制 DRW Vx, Vy, N

这些指令放在 CPU.execute 方法实现。这是一个巨大的if elif 语句。2

指令执行

CPU.execute 方法实现指令的执行。下面是这几条指令的代码。

CPU.execute()[source]
    def execute(self):

        if self.opcode == 0x0000:
            # 00E0
            # CLS
            # clear the display
            if self._IR.kk == 0x00E0:
                self._screen_buf = self.reset_screen()
                self.draw_flag = True

        # 1NNN
        # JP addr
        # jump to location nnn
        elif self.opcode == 0x1000:
            addr = self.nnn
            self._reg_PC = addr


        # 6XKK
        # LD Vx, byte
        # Vx = kk
        elif self.opcode == 0x6000:
            x = self.x
            kk = self.kk
            self._reg_V[x] = kk


        # 7XKK
        # ADD Vx, byte
        # Vx = Vx + kk
        elif self.opcode == 0x7000:
            x = self.x
            kk = self.kk
            self._reg_V[x] += kk
            self._reg_V[x] &= 0xff

        # ANNN
        # LD I, addr
        # I = nnn
        elif self.opcode == 0xA000:
            addr = self.nnn
            self._reg_I = addr

        # DXYN
        # DRW Vx, Vy, n
        elif self.opcode == 0xD000:
            n = self.flag
            x = self.x
            y = self.y

            vx = self._reg_V[x]
            vy = self._reg_V[y]

            self._reg_V[0xF] = 0
            for yy in range(n):
                sys_byte = self._memory.ram[self._reg_I + yy]
                for xx in range(8):
                    x_cord = vx + xx
                    y_cord = vy + yy
                    if x_cord < SCREEN_WIDTH and y_cord < SCREEN_HEIGHT:
                        sys_bit = (sys_byte >> (7 - xx)) & 0x01
                        if (self._screen_buf[y_cord][x_cord] & sys_bit) == 1:
                            self._reg_V[0xF] = 1

                        self._screen_buf[y_cord][x_cord] ^= sys_bit

            self.draw_flag = True

下面针对这些代码进行解析。

00E0,清屏的指令,只需要将视频缓存二维数组 screen_buf 恢复成默认,即所有元素都是 0 值。

1NNN 跳转指令,decode 指令得到 NNN,这是跳转的地址。然后将 PC 寄存器的值指向这个地址。即程序的执行流将会从跳转地址开始 fetch 下一条指令。类似高级语言的goto语句。

6XKK 设置寄存器 Vx 的值为 KK。decode可以得到 x 和 KK 的值,直接设置 reg_V 对应的值即可。类似高级语言的赋值语句 Vx = KK

7XKK 将寄存器Vx的值加上 KK,然后再存储回 Vx。类似高级语言的 += 语句。Vx = Vx + KK。通用寄存器的大小是 1-byte,加法操作会溢出。溢出之后需要抹去高位,Vx 会和 0xFF 做(&)操作。但是这条指令不需要设置标记位(reg_V[0xF])。

ANNN 设置地址寄存器I的值为 NNN。这条指令与1NNN 类似。都是设置寄存器的值为一个地址。不同在于这条指令修改的是索引寄存器。

DXYN 这一条绘图指令。Chip8 绘图使用 Sprite 方式。Sprite 即一个区域块。如下图所示,IBM logo的图案,由6个 sprite 组成。每一个 spite 是 8 * 15 的像素块,由不同的颜色标识。其中字母I正好是一个 sprite。字母B由 蓝色和绿色的两个 sprite 组成。其中绿色的 spite 和组成M的黄色的 sprite 有重叠。因为重叠的地方绿色 sprite 没有像素,这样的覆盖也不会影响最终的视觉效果。

sprite

sprite 的大小固定(15*8),因此绘制 sprite 的时候,只需要指定起点的坐标即可。例如I的起点坐标是(12, 8),即存储在 Vx 和 Vy 的值。以此类推,字母 B 的蓝色 sprite 的起点是(12+9, 8)。

尽管 sprite 的大小是固定的,但是 sprite 上的图案可以自由绘制,不同的 sprite 块可以叠加以绘制最终效果。就像字母B和字母M重叠的部分。

对于指令 DXYN 而言,(x, y) 的值分别为 Vx 和 Vy 的值。N 表示从地址寄存器 reg_I 读取 N-byte 的数据。这些数据用二进制进行表示。0 表示不需要绘图,1 表示需要绘制。通过解析数据的二进制位,写到视频缓存二维数组 screen_buf 中。再交给 display 去绘图。

执行改指令的代码会先 decode Vx 和 Vy,然后取 [reg_I, reg_I + N) 这一段空间的数据。 再针对这些数据进行遍历,求出当前 sprite 的起点坐标(x_cord, y_cord)。

如果当前点的值(sys_bit)与需要绘制的bit的操作为1,则需要设置标记寄存器 reg_V[0xF]的值为1。最后该值与sys_bit做异或(^)操作。

指令完成之前,设置一个 CPU 的 draw_flag 属性,表示此时主循环需要绘制一帧图形。

实现上面的指令之后,还需要处理一下键盘事件,给键盘事件增加退出监听,不然我们的模拟器会被卡住。

Keyboard.poll_event()[source]
    def poll_event(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
src.tutorial.06.3_run_rom.main()[source]

之后,屏幕就会输出 IBM 的图案。

ibm-logo

总结

本节的主要内容是了解 Chip8 CPU 循环的主要工作流。同时实现 SPEC 上描述的几条基本指令。比较重要的是绘图指令DXYN。掌握Chip8图像绘制的Sprite方法。

接下来的任务就很简单了,就是根据 SPEC,将剩余的指令实现完成,模拟器就基本完成了。


1

Sprite 绘图方式一直很流行

2

Python3.10提供了match语句,对于这种应用场景实现会更优雅。rust 的match pattern也十分合适。