介绍
CHIP-8 是一种解释型语言,设计之初就是为了编写简单的小游戏。我猜是作者嫌老机器的汇编语言太复杂繁琐,从而自己设计了一门汇编语言,并且摆脱硬件的束缚,在模拟器上运行。其实这个思想和 Java 等基于虚拟机的高级语言也是类似的,提供方便程序员编写的指令集,在硬件之上空架一层虚拟机,实现 “Write Once, Run Everywhere”。
我们的目的是在python中实现一个chip8虚拟机
CHIP8 虚拟机的结构
和现代的计算机一样,虚拟起也有内存,CPU,寄存器等计算机的“基础零件”:
- Memory:CHIP-8 最多有 4096 字节的内存
CHIP-8 解释器本身占用这些机器上的前 512 字节内存空间。因此,为原始系统编写的大多数程序都从内存位置 512 (0x200) 开始,并且不会访问位置 512 (0x200) 以下的任何内存。最上面的 256 个字节 (0xF00-0xFFF) 保留用于显示刷新,下面的 96 个字节 (0xEA0-0xEFF) 保留用于调用堆栈、内部使用和其他变量。
- Program Counter:16 位的 PC,记录当前程序指令运行的内存位置,因为需要访问最多 4K 的内存(0xFFF)
- Stack:16 位地址的堆栈,用于调用函数和返回。栈调用深度最初设计位 12 层,可以自行调整。
Registers:
- 16 个 8 位数据寄存器(data register),名为 V0 至 VF。 VF 寄存器兼作某些指令的标志;因此,应该避免这种情况。在加法运算中,VF 是进位标志,而在减法运算中,VF 是“无借位”标志。在绘制指令中,VF 在像素冲突时设置。
- 一个 16 位索引寄存器(index register),用于记录内存地址
Timers
- 8 位延迟定时器,以 60 Hz(每秒 60 次)的速率递减,直至达到 0
- 8 位声音定时器,当其值非零时,会发出蜂鸣声。
- Display:64 x 32 像素(或 128 x 64 对于 SUPER-CHIP)单色,即黑或白
- Inputs:16 个输入键,与前 16 个十六进制值匹配:0 到 F。
上面就是 CHIP-8 虚拟机的全部“硬件”了,接下来就是实现用软件模拟硬件。
导入模块
import pygame
import sys
import random
import os
我们需要实现图形界面、系统操作、随机数生成器和CHIP8指令集
寄存器
class Register:
def __init__(self, bits):
self.value = 0
self.bits = bits
def checkCarry(self):
hexValue = hex(self.value)[2:]
if len(hexValue) > self.bits / 4:
self.value = int(hexValue[-int(self.bits / 4):], 16)
return 1
return 0
def checkBorrow(self):
if self.value < 0:
self.value = abs(self.value)
return 0
return 1
def readValue(self):
return hex(self.value)
def setValue(self, value):
self.value = value
Register
类表示一个寄存器,包含寄存器的值和位数。checkCarry
和 checkBorrow
方法用于检查进位和借位。
延迟计时器
CHIP-8 有两个独立的定时器寄存器:延迟定时器和声音定时器。 大小为 1 个字节,只要它们的值大于 0,它们就应该每秒减少 60 次(即 60 Hz ),并且与执行指令的速度无关。 也就是说无论代码怎么执行,即使进入了死循环,定时器也需要以 60 Hz 的频率运行。
class DelayTimer:
def __init__(self):
self.timer = 0
def countDown(self):
if self.timer > 0:
self.timer -= 1
def setTimer(self, value):
self.timer = value
def readTimer(self):
return self.timer
这样可以设计一个最基础的延迟计时器,包含计时器的值和倒计时方法。
蜂鸣器
class SoundTimer(DelayTimer):
def __init__(self):
DelayTimer.__init__(self)
def beep(self):
if self.timer > 1:
os.system('play --no-show-progress --null --channels 1 synth %s triangle %f' % (self.timer / 60, 440))
self.timer = 0
SoundTimer
类继承自 DelayTimer
,可以利用系统延时器播放声音。
Stack 类
chip8虚拟机提供了一个栈机,我们可以使用python的队列来实现
class Stack:
def __init__(self):
self.stack = []
def push(self, value):
self.stack.append(value)
def pop(self):
return self.stack.pop()
Stack
类表示一个堆栈,包含入栈和出栈方法。
Emulator 类
Emulator
初始化内存、寄存器、堆栈、计时器、键盘映射和显示屏。 我们之前导入的pygame
这会儿可以用于图形界面
chip8使用的是点阵字体,我们可以用16进制数,每一位来表示一个像素
class Emulator:
def __init__(self):
self.Memory = [0x0] * 4096
fonts = [
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
]
for i in range(len(fonts)):
self.Memory[i] = fonts[i]
self.Registers = [Register(8) for _ in range(16)]
self.IRegister = Register(16)
self.ProgramCounter = 0x200
self.stack = Stack()
self.delayTimer = DelayTimer()
self.soundTimer = SoundTimer()
pygame.init()
pygame.time.set_timer(pygame.USEREVENT+1, int(1000 / 60))
self.keys = [False] * 16
self.keyDict = {
49 : 1, 50 : 2, 51 : 3, 52 : 0xc,
113 : 4, 119 : 5, 101 : 6, 114 : 0xd,
97 : 7, 115 : 8, 100 : 9, 102 : 0xe,
122 : 0xa, 120 : 0, 99 : 0xb, 118 : 0xf
}
self.grid = [[0] * 64 for _ in range(32)]
self.emptyGrid = self.grid[:]
self.zeroColor = [0, 0, 50]
self.oneColor = [255, 255, 255]
self.size = 10
width = 64
height = 32
self.screen = pygame.display.set_mode([width * self.size, height * self.size])
self.screen.fill(self.oneColor)
pygame.display.flip()
execOpcode 方法
cpu最重要的就是指令集了,chip8的指令集非常精简,一共只有35条指令,execOpcode
方法根据操作码执行相应的指令。
def execOpcode(self, opcode):
if opcode[0] == '0':
if opcode[1] != '0':
print("ROM attempts to run RCA 1802 program at <0x" + opcode[1:] + '>')
else:
if opcode == '00e0':
self.clear()
elif opcode == '00ee':
self.ProgramCounter = self.stack.pop()
elif opcode[0] == '1':
self.ProgramCounter = int(opcode[1:], 16) - 2
elif opcode[0] == '2':
self.stack.push(self.ProgramCounter)
self.ProgramCounter = int(opcode[1:], 16) - 2
elif opcode[0] == '3':
vNum = int(opcode[1], 16)
targetNum = int(opcode[2:], 16)
if self.Registers[vNum].value == targetNum:
self.ProgramCounter += 2
elif opcode[0] == '4':
vNum = int(opcode[1], 16)
targetNum = int(opcode[2:], 16)
if self.Registers[vNum].value != targetNum:
self.ProgramCounter += 2
elif opcode[0] == '5':
v1 = int(opcode[1], 16)
v2 = int(opcode[2], 16)
if self.Registers[v1].value == self.Registers[v2].value:
self.ProgramCounter += 2
elif opcode[0] == '6':
vNum = int(opcode[1], 16)
targetNum = int(opcode[2:], 16)
self.Registers[vNum].value = targetNum
elif opcode[0] == '7':
vNum = int(opcode[1], 16)
targetNum = int(opcode[2:], 16)
self.Registers[vNum].value += targetNum
self.Registers[vNum].checkCarry()
elif opcode[0] == '8':
if opcode[3] == '0':
v1 = int(opcode[1], 16)
v2 = int(opcode[2], 16)
self.Registers[v1].value = self.Registers[v2].value
elif opcode[3] == '1':
v1 = int(opcode[1], 16)
v2 = int(opcode[2], 16)
self.Registers[v1].value = self.Registers[v1].value | self.Registers[v2].value
elif opcode[3] == '2':
v1 = int(opcode[1], 16)
v2 = int(opcode[2], 16)
self.Registers[v1].value = self.Registers[v1].value & self.Registers[v2].value
elif opcode[3] == '3':
v1 = int(opcode[1], 16)
v2 = int(opcode[2], 16)
self.Registers[v1].value = self.Registers[v1].value ^ self.Registers[v2].value
elif opcode[3] == '4':
v1 = int(opcode[1], 16)
v2 = int(opcode[2], 16)
self.Registers[v1].value += self.Registers[v2].value
self.Registers[0xf].value = self.Registers[v1].checkCarry()
elif opcode[3] == '5':
v1 = int(opcode[1], 16)
v2 = int(opcode[2], 16)
self.Registers[v1].value -= self.Registers[v2].value
self.Registers[0xf].value = self.Registers[v1].checkBorrow()
elif opcode[3] == '6':
v1 = int(opcode[1], 16)
leastBit = int(bin(self.Registers[v1].value)[-1])
self.Registers[v1].value = self.Registers[v1].value >> 1
self.Registers[0xf].value = leastBit
elif opcode[3] == '7':
v1 = int(opcode[1], 16)
v2 = int(opcode[2], 16)
self.Registers[v1].value = self.Registers[v2].value - self.Registers[v1].value
self.Registers[0xf].value = self.Registers[v1].checkBorrow()
elif opcode[3] == 'e':
v1 = int(opcode[1], 16)
mostBit = int(bin(self.Registers[v1].value)[2])
self.Registers[v1].value = self.Registers[v1].value << 1
self.Registers[0xf].value = mostBit
elif opcode[0] == '9':
v1 = int(opcode[1], 16)
v2 = int(opcode[2], 16)
if self.Registers[v1].value != self.Registers[v2].value:
self.ProgramCounter += 2
elif opcode[0] == 'a':
addr = int(opcode[1:], 16)
self.IRegister.value = addr
elif opcode[0] == 'b':
addr = int(opcode[1:], 16)
self.ProgramCounter = self.Registers[0].value + addr - 2
elif opcode[0] == 'c':
vNum = int(opcode[1], 16)
targetNum = int(opcode[2:], 16)
rand = random.randint(0, 255)
self.Registers[vNum].value = targetNum & rand
elif opcode[0] == 'd':
Vx = int(opcode[1], 16)
Vy = int(opcode[2], 16)
N = int(opcode[3], 16)
addr = self.IRegister.value
sprite = self.Memory[addr: addr + N]
for i in range(len(sprite)):
if type(sprite[i]) == str:
sprite[i] = int(sprite[i], 16)
if self.draw(self.Registers[Vx].value, self.Registers[Vy].value, sprite):
self.Registers[0xf].value = 1
else:
self.Registers[0xf].value = 0
elif opcode[0] == 'e':
if opcode[2:] == '9e':
Vx = int(opcode[1], 16)
key = self.Registers[Vx].value
if self.keys[key]:
self.ProgramCounter += 2
elif opcode[2:] == 'a1':
Vx = int(opcode[1], 16)
key = self.Registers[Vx].value
if not self.keys[key]:
self.ProgramCounter += 2
elif opcode[0] == 'f':
if opcode[2:] == '07':
Vx = int(opcode[1], 16)
self.Registers[Vx].value = self.delayTimer.readTimer()
elif opcode[2:] == '0a':
Vx = int(opcode[1], 16)
key = None
while True:
self.keyHandler()
isKeyDown = False
for i in range(len(self.keys)):
if self.keys[i]:
key = i
isKeyDown = True
if isKeyDown:
break
self.Registers[Vx].value = key
elif opcode[2:] == '15':
Vx = int(opcode[1], 16)
value = self.Registers[Vx].value
self.delayTimer.setTimer(value)
elif opcode[2:] == '18':
Vx = int(opcode[1], 16)
value = self.Registers[Vx].value
self.soundTimer.setTimer(value)
elif opcode[2:] == '1e':
Vx = int(opcode[1], 16)
self.IRegister.value += self.Registers[Vx].value
elif opcode[2:] == '29':
Vx = int(opcode[1], 16)
value = self.Registers[Vx].value
self.IRegister.value = value * 5
elif opcode[2:] == '33':
Vx = int(opcode[1], 16)
value = str(self.Registers[Vx].value)
fillNum = 3 - len(value)
value = '0' * fillNum + value
for i in range(len(value)):
self.Memory[self.IRegister.value + i] = int(value[i])
elif opcode[2:] == '55':
Vx = int(opcode[1], 16)
for i in range(0, Vx + 1):
self.Memory[self.IRegister.value + i] = self.Registers[i].value
elif opcode[2:] == '65':
Vx = int(opcode[1], 16)
for i in range(0, Vx + 1):
self.Registers[i].value = self.Memory[self.IRegister.value + i]
self.ProgramCounter += 2
图形显示配置
def execution(self):
index = self.ProgramCounter
high = self.hexHandler(self.Memory[index])
low = self.hexHandler(self.Memory[index + 1])
opcode = high + low
self.execOpcode(opcode)
def draw(self, Vx, Vy, sprite):
collision = False
spriteBits = []
for i in sprite:
binary = bin(i)
line = list(binary[2:])
fillNum = 8 - len(line)
line = ['0'] * fillNum + line
spriteBits.append(line)
for i in range(len(spriteBits)):
for j in range(8):
try:
if self.grid[Vy + i][Vx + j] == 1 and int(spriteBits[i][j]) == 1:
collision = True
self.grid[Vy + i][Vx + j] = self.grid[Vy + i][Vx + j] ^ int(spriteBits[i][j])
except:
continue
return collision
def clear(self):
for i in range(len(self.grid)):
for j in range(len(self.grid[0])):
self.grid[i][j] = 0
def readProg(self, filename):
rom = self.convertProg(filename)
offset = int('0x200', 16)
for i in rom:
self.Memory[offset] = i
offset += 1
def convertProg(self, filename):
rom = []
with open(filename, 'rb') as f:
wholeProgram = f.read()
for i in wholeProgram:
opcode = i
rom.append(opcode)
return rom
def hexHandler(self, Num):
newHex = hex(Num)[2:]
if len(newHex) == 1:
newHex = '0' + newHex
return newHex
def keyHandler(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.USEREVENT+1:
self.delayTimer.countDown()
elif event.type == pygame.KEYDOWN:
try:
targetKey = self.keyDict[event.key]
self.keys[targetKey] = True
except: pass
elif event.type == pygame.KEYUP:
try:
targetKey = self.keyDict[event.key]
self.keys[targetKey] = False
except: pass
def mainLoop(self):
clock = pygame.time.Clock()
while True:
clock.tick(300)
self.keyHandler()
self.soundTimer.beep()
self.execution()
self.display()
def display(self):
for i in range(0, len(self.grid)):
for j in range(0, len(self.grid[0])):
cellColor = self.zeroColor
if self.grid[i][j] == 1:
cell