基本指令¶
屏幕绘制 一节,我们通过指定屏幕二维数组 screen_buf 在屏幕显示了 IBM
的图案。本节的目标就是使用 Chip8 加载 ibm 的 rom 文件,通过 CPU 的解析执行 Chip8 指令,来显示 IBM 的图像。
二进制¶
chip8 的 rom 文件都是二进制文件。使用 python 可以很方便的读取。下面是代码:
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
方法
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 方式绘图。1 如0x22a ~ 0x238
是15-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 方法实现指令的执行。下面是这几条指令的代码。
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 的大小固定(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 属性,表示此时主循环需要绘制一帧图形。
实现上面的指令之后,还需要处理一下键盘事件
,给键盘事件增加退出监听,不然我们的模拟器会被卡住。
def poll_event(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
之后,屏幕就会输出 IBM 的图案。
总结¶
本节的主要内容是了解 Chip8 CPU 循环的主要工作流。同时实现 SPEC 上描述的几条基本指令。比较重要的是绘图指令DXYN
。掌握Chip8图像绘制的Sprite
方法。
接下来的任务就很简单了,就是根据 SPEC,将剩余的指令实现完成,模拟器就基本完成了。