基础
所有计算机程序本质上都是保存到文件的数字
-
ROM
- 只读存储器,制造时硬编码的内存类型
- 使用终端命令查看十六进制
xxd rom | head
- 左侧 00000000: → 文件偏移地址
- 22f6 6b0c 6c3f 6d0c ... → 16 进制字节
- ".k.l?m.......n. → ASCII 解析(仅可读字符显示)
-
寄存器
- 专门指定的存储单个字节的场所,用于指令的使用,类似于RAM
- 有专属名称,在执行操作码时可以直接使用
- 如果想在RAM中使用一个值,必须首先将其复制到寄存器
- chip-8有十六个寄存器,命名从V0到VF(十六进制0-15)
- 示例:8YX4 首尾的8--4来匹配操作码,XY为寄存器地址,如8124是8--4操作的V1和V2,操作完成后会替换前面的寄存器数据,本例是替换V1
- chip-8还有一些额外的寄存器
- PC:程序计数器
- 持有正在处理的指令的索引,用来跟踪确定程序执行的位置
- 所有的操作码都是两个字节
- PC:程序计数器
-
RAM
- 由于是模拟器项目,虽然不受物理上的制约,但RAM仍然以物理上的标准进行复现也就是4KB1
- 另外,由于物理系统的惯例,游戏ROM的加载偏移了512字节,首位是0x200,而之前的空间被分配给了Chip-8本身
项目实践的要点记录
按照教程CHIP-8's BOOK实现一个适用于桌面和网页浏览器的模拟器
-
工程配置
- 在工程目录内部,生成独立crate作为模拟器核心库,用来提供给预设的前端服务
- 使用
[workspace]
配置多个crate互相配合的项目 cargo init chip8_core --lib
- 适用 --lib的标志初始化为库文件
cargo init desktop
- 使用
- 在工程目录内部,生成独立crate作为模拟器核心库,用来提供给预设的前端服务
-
基础实现
- CPU
- 在chip8_core lib中实现cpu模拟的工作
- 模拟 loop: fetch-decode-execute
- program counter 使用u16的原因是CHIP-8 的 地址空间是 12 位(0x000–0xFFF,通过二进制位来寻址 $2^{12}=4096$ ),但 CPU 通常按 16位 处理地址
- CPU Stack
- 堆栈和RAM不同,只能通过LIFO(后进先出)原则进行读写
- 不通用,唯一允许使用堆栈的情况是进入或退出子程序时
- Chip-8没有正式规定堆栈的大小
- stack pointer (SP,栈指针)
- RAM
- 4096
- Display
- chip-8使用64x32单色显示屏(每像素一位)
- 不会自动清除屏幕来重绘每一帧,是直接绘制新的精灵,有清除屏幕的指令
- V(Variable) Registers
- chip-8定义十六个8位寄存器,用来提高处理速度,以十六进制编号从V0到VF
- I Registers
- Special Registers
- 时间寄存器和声音寄存器
- CPU
-
实现仿真方法
-
图形库SDL2的注意事项
- macOs
brew install sdl2
export LIBRARY_PATH="$LIBRARY_PATH:$(brew --prefix)/lib"
放入~/.zshenv
或~/.bash_profile
- 使用过程中需要注意绘制过程是成对出现的,以及对
opengl()
是否兼容canvas.clear(); // 清空当前屏幕 draw_screen(&chip8, &mut canvas); // 绘制当前屏幕 canvas.present(); // 更新屏幕
- macOs
-
字体精灵的示意图
- Chip-8中的Sprite宽度为8像素宽
- 字符如图所示大小为8×5
- 二进制的1来代表显示屏的点亮位置,白色即为模拟出的符号1
- 0x20 = 0b0010 0000 十六进制是每四位分离表示0到F,实际的数值计算是$\text{0x}20 = 216^{1} + 016^0 = 32 = 0010 0000$,所以符号1最终的编码是**[0x20, 0x60, 0x20, 0x20, 0x70]**
0b00100000 // 0x20 ( █ ) RAM[I + 0] 0b01100000 // 0x60 ( ██ ) 0b00100000 // 0x20 ( █ ) 0b00100000 // 0x20 ( █ ) 0b01110000 // 0x70 ( ███ )
-
DXYN-绘制指令
- XY:基本数据座标 N:精灵高度
- I寄存器中存储着精灵的数据地址(构造如上)
- 指令实现过程:
- 从0到N从I寄存器中取出精灵的数据(以Y每行的格式存储),再从RAM中取得实际数据
- 然后使用掩码进行逐位(bit)的检测方式
if (pixels & (0b1000_0000 >> x_line)) != 0
- 再通过基本数据座标和位的数值确定出显示逻辑上的XY座标,之后将二维显示座标转换成控制显示状态的一维数组的索引
let idx = x + SCREEN_W * y;
- 最终进行反转
self.screen[idx] ^= true;
- 位运算异或XOR和逻辑取反结果一致,但是执行效率不一样
self.screen[idx] =!self.screen[idx];
-
BCD 格式
- BCD(Binary-Coded Decimal,二进制编码的十进制) 是一种用 4 位二进制 表示 1 位十进制数字 的编码方式
- 将数字按位分割独立保存
-
-
WASM实现
- 快速启动网页服务
- 在index.html文件的目录下执行命令
python3 -m http.server
- 在index.html文件的目录下执行命令
- 打包工具wasm-pack
- 主要问题
- 随机数的兼容性问题
- 在core/Cargo.toml配置中依赖wasm特性的随机数lib
getrandom = { version = "0.3", features = ["wasm_js"] }
- 实现并使用包装后的随机函数
-
fn get_random_u8() -> Result<u8, getrandom::Error> { let mut buf = [0u8; 1]; getrandom::fill(&mut buf)?; Ok(u8::from_ne_bytes(buf)) }
-
- 编译时需要注明flags
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' wasm-pack build --target web
- 在core/Cargo.toml配置中依赖wasm特性的随机数lib
- 开发调试遇到的问题
- 需要注意所有权的移动问题会导致Error: null pointer passed to Rust
- 由fn(mut self)缺少&导致
- 由于是跨平台的项目,调试依赖于Log,配置log库
-
[dependencies] cfg-if = "0.1" log = "^0.4" console_log = { version = "1", features = ["color"] } [lib] crate-type = ["cdylib", "rlib"]
-
- 需要注意所有权的移动问题会导致Error: null pointer passed to Rust
- 随机数的兼容性问题
- 快速启动网页服务
1
所以Chip-8游戏最大只支持4KB,超过这个大小就无法完全加载