首页
统计
留言板
关于
Search
1
Rust 包装 objc Block
166 阅读
2
Flutter 调用 Rust 生成的 共享/静态 库
157 阅读
3
02. Rust 内存管理 Copy & Clone(上)
157 阅读
4
用 Rust 开发 iOS 应用(粗糙版)
104 阅读
5
纯 C 写个 iOS App(误)
99 阅读
默认分类
Rust
Apple
iOS
Swift
Android
emulator
NES
登录
/
注册
Search
标签搜索
Rust
iOS
NES
Swift
Android
杂项
limit
累计撰写
18
篇文章
累计收到
0
条评论
首页
栏目
默认分类
Rust
Apple
iOS
Swift
Android
emulator
NES
页面
统计
留言板
关于
搜索到
3
篇与
的结果
2021-03-22
0b0010-抽象bit操作
提出问题这趟没屁话。接上文,我们发觉一桩事,修改状态寄存器的时候,我们不够抽象,譬如我想处理 Zero Flag,我需要通过位运算符去操作一个数,每次写二进制,很难受,现在来改得抽象一些解决问题我们发觉状态寄存器有 C、Z、I、D、B、U、V、N 这些状态,那就定义一个类型来描述它们。状态描述状态含义Carry Flag进位标志,操作后导致结果的第 7 位溢出或者 0 位下溢,就设置Zero Flag零标志,如果操作结果为零,就设置。Interrupt Disable中断禁用标志,设置后 CPU 将不处理设备的中断,除非执行清除中断禁用。Decimal Mode十进制模式,CPU 在进行加减时遵守二进制编码的十进制(BCD)算术规则。BBRK 相关U中断相关,这个要配合 B 标志使用Overflow Flag溢出标志,结果出现溢出时设置。Negative Flag负标志,操作结果的第 7 位设置为 1,就要设置。关于中断这里先不讲,直接讲概念没用,后面有实际场景来补充更好理解。struct Flag: OptionSet { static let C = Flag(rawValue: 0b0000_0001) // 1 << 0 static let Z = Flag(rawValue: 0b0000_0010) // 1 << 1 static let I = Flag(rawValue: 0b0000_0100) // 1 << 2 static let D = Flag(rawValue: 0b0000_1000) // 1 << 3 static let B = Flag(rawValue: 0b0001_0000) // 1 << 4 static let U = Flag(rawValue: 0b0010_0000) // 1 << 5 static let V = Flag(rawValue: 0b0100_0000) // 1 << 6 static let N = Flag(rawValue: 0b1000_0000) // 1 << 7 internal var rawValue: UInt8 = 0 init(rawValue flag: UInt8) { rawValue = flag } func bits() -> UInt8 { rawValue } }这样我就把这些状态抽象出来了,如果不想用 rawValue 这个命名,可以把 OptionSet 去掉,反正对应着现在的内容写就可以,使用是一样的效果,OptionSet 是一个协议,用来约束我们的结构体。然后我们要做得就是把行为抽象,根据我们目前已有的代码,我们需要两个函数,当然我也自作主张地添加了一个 set 方法用来处理分支的情况extension Flag { mutating func insert(other: Flag) { rawValue |= other.bits() } mutating func remove(other: Flag) { rawValue &= (~other.bits()) } mutating func set(other: Flag, condition: Bool) { if condition { insert(other) } else { remove(other) } } }修改我们得把 CPU 的 status 改成我们定义的 Flag 结构体class CPU { // ... var status: Flag = Flag(rawValue: 0b0010_0100) // ... } extension CPU { func reset() { // ... status = Flag(rawValue: 0b0010_0100) // ... } }我们的仿真代码也要改一下extension CPU { func interpret(program: [UInt8]) { pc = 0 while true { let code = program[Int(pc)] pc += 1 switch code { // ... case 0xa9: let param = program[Int(pc)] pc += 1 a = param // 处理 Zero Flag status.set(other: .Z, condition: a == 0) // 处理 Negative Flag status.set(other: .N, condition: a >> 7 == 1) default: break } } } }测试由于我们把实现修改嘞,所以我们还要把测试代码改一下,看看效果func testSth() { let cpu = CPU() cpu.interpret(program: [0xa9, 0x00, 0x00]) assert(cpu.status.bits() & 0b0000_0010 == 0b10) cpu.reset() cpu.interpret(program: [0xa9, 0x05, 0x00]) assert(cpu.a == 0x05) assert(cpu.status.bits() & 0b0000_0010 == 0) assert(cpu.status.bits() & 0b1000_0000 == 0) }Good Job,现在只要一两行代码就把之前的操作涵盖了,后面仿真其他指令的时候就更容易嘞,心智负担不会太重。
2021年03月22日
36 阅读
0 评论
0 点赞
2021-03-21
0b0001-开始 emulate CPU
日常讲屁话继续我们的日常水文,这节我们来仿真一个 CPU,其实主要工作就是仿真一个 CPU 的指令。我也看过一些别人的好文讲得是从一个 NES 的 ROM 出发,一步一步仿真 NES,不过我觉得那样不太好讲故事。6502 指令开始仿真 CPU 之前,先祭出这个网站,这个网站把 6502 的指令基本都描述了一下(6502 Reference)通过点击对应的指令,可以锚点到对应的介绍。我们可以看到图上有很多指令,其实 NES 有很多非官方指令,不过不要慌,我们把这些指令仿真先,后续考虑处理非官方的一些指令。描述 CPU现在先写个 CPU 的 class/struct,Swift 的 class 是引用类型,而 struct 是值类型,这里看个人的喜好,我直接用 class 来声明。class CPU { var a: UInt8 = 0 // 累加寄存器 a var x: UInt8 = 0 // 变址寄存器 x var y: UInt8 = 0 // 变址寄存器 y var status: UInt8 = 0 // 状态寄存器 var sp: UInt8 = 0 // 堆栈指针 var pc: UInt16 = 0 // 程序计数器 }寄存器 a 是比较常用的,主要用于读写数据,进行一些逻辑运算寄存器 x 跟 a 差不多,但是它可以更方便 +/- 1,主要用于数据传输、运算等操作寄存器 y 性质跟 x 类似状态寄存器主要保存指令执行时的状态信息,这块有多个状态,分别是 C、Z、I、D、B、V、N,后面写代码会用到堆栈的指针指向一块栈内存这个后面实现的时候展开程序计数器,其实可以理解成一个数组的下标索引,它的寻址范围可以从 0x0000 到 0xFFFF,CPU 根据 pc 找到对应的存储单元,pc 可以自动加 1,CPU 每次读取一条指令,pc 就会自动加 1,当然我们也可以让它跳转返回来改变执行顺序BRK现在把 CPU 做为一个整体,用它来读取“程序”,首先实现一下 BRK 指令,通过上面那个网址查表,我们发现 BRK 是 $00(也就是 0x00,$ 表示 16 进制),它的 Addressing Mode(地址模式) 是 Implied,先不管那么多,直接执行到 BRK 让它跳出循环,如果要真实仿真 BRK 指令,需要把中断的模式都仿真完整,图简单直接 return 吧。extension CPU { func interpret(program: [UInt8]) { pc = 0 while true { let code = program[Int(pc)] pc += 1 switch code { case 0x00: return default: break } } } }目前来看,这好无聊。LDA我们再来把另一个 LDA 实现一下,查表得到 LDA 有多个地址模式,Immediate,Zero Page,Zero Page X,Absolute X……一个一个来,先把快速模式(Immediate)解决掉,这个 Opcode 是 0xa9,此外还要修改 status,这里分别对 Zero Flag 跟 Negative Flag 有影响(Zero Flag 就是上面讲到得 Z,Negative Flag 就是上面讲到的 N)。extension CPU { func interpret(program: [UInt8]) { pc = 0 while true { let code = program[Int(pc)] pc += 1 switch code { // ... case 0xa9: let param = program[Int(pc)] pc += 1 a = param // 处理 Zero Flag if a == 0 { status |= 0b0000_0010 } else { status &= (~0b0000_0010) } // 处理 Negative Flag if a >> 7 == 1 { status |= 0b1000_0000 } else { status &= (~0b1000_0000) } default: break } } } }现在已经把 Immediate 模式的 LDA 处理脱,还有其他 0xa5、0xb5……这些,尝试动手处理一下。测试用例由于我建项目的时候,启用了测试 target,可以写点代码测试一下我们实现的指令,为了方便测试,我们实现一个 reset 函数extension CPU { func reset() { a = 0 x = 0 y = 0 status = 0 sp = 0 pc = 0 } }然后在测试 target 里写上测试的函数,跑一下func testSth() { let cpu = CPU() cpu.interpret(program: [0xa9, 0x00, 0x00]) assert(cpu.status & 0b0000_0010 == 0b10) cpu.reset() cpu.interpret(program: [0xa9, 0x05, 0x00]) assert(cpu.a == 0x05) assert(cpu.status & 0b0000_0010 == 0) assert(cpu.status & 0b1000_0000 == 0) }现在我们已经稍微了解了一挨挨仿真指令的处理方式,后面一节我们继续来优化当前的这些代码。代码会再实现完 CPU 后放到 gayhub 上。
2021年03月21日
51 阅读
0 评论
0 点赞
2021-02-23
0b0000-写个 NES emulator
日常讲屁话吃得饱,打算写个 NES emulator,语言使用 Swift(一边看文档一边学),目标是做个 macOS App,如果有人有想法,把它移植到 iOS 上也可以。macOS App 开发也是临时看文档学,代码质量估计不高,先这样吧。NES:是 FC 主机欧美地区发售的称呼。1985年10月,NES 正式在西方国家推出,除了名称由 Family Computer 改为 Nintendo Entertainment System,造型设计也大幅改变。我们 80/90 后都玩过的小霸王就是内置了一个 NES,比较出名的游戏如超级马里奥,我们的大目标就是可以仿真运行一个超级马里奥初代。设备结构开发一个 NES emulator 之前,我们来过一遍 NES 的主要组成部分。基本上有这些模块CPU NES 的处理器是 MOS 6502 的衍生产品,譬如 Apple II 的处理器就是它这一系的,这是我在网上找到的一篇关于该处理器的文章 MOS Technology 6502 CPU。我读中学的时期,国内有一些低端学习机(譬如电子词典这类)用得也是这个 CPU,具体可以查一下,此处不赘述。它是一个 8 位的处理器,频率是 1.7897725MHz(通常是 NTSC 机型, PAL 机型下频率只有 1.773447MHz),中断模式有 RESET、NMI、IRQ。PPU 这个全称是 Picture Processing Unit,这玩意是用来处理图形的。它会把图像用 256 * 240 的分辨率输出(受限于 NTSC 上下各 8 行显示不了,其实只能显示 256 * 224),调色盘可显示 48 色 5 个灰阶(现在看起来好像蛮弱的),NES 中图像分 Background 跟 Sprite,通过组合两者来显示完整的图像。如果你尝试开发过 Game Boy 的小游戏, 你会发现这两个概念好眼熟,因为受限于那个年代游戏机平台的硬件机能,很多 2D 游戏机是通过类似方式处理图像的(Sprite 在 Web 前端应用或小游戏开发里也有相应的应用)。RAM NES 的 CPU 跟 PPU 都有 2KiB 大小的内存APU 是一个音频处理器,全称 Audio Processing Unit,这个模块其实集成在 CPU 中, 所以它并不是独立的芯片,可以称它为 p(seudo)APU,它提供了五个通道用来处理音频,分别是两个矩形波通道,一个三角波通道, 一个噪声波通道(譬如爆炸声),一个音频采样通道(主要用于处理背景音)。卡带 由于 NES 没有操作系统,所以内容由卡带提供,每个卡带至少包含两个东西,一个是 CHR ROM 主要用于存储游戏图形的数据,另一个是 PRG ROM,主要是用来存储游戏的指令。现实中卡带插入到机器后,CHR ROM 直接连接到 PPU,PRG 连接到 CPU,NES 后续的机器还用卡带提供其他的扩展,不过目前我们先不太深究这块内容手柄 这就是个输入设备就不细讲了其实这些就是我们的目标,我们要把这些模块都用代码描述出来,由于我们用得是高级语言,所以很多东西都变得简单起来了。创建项目现在来启个新项目,具体怎么创建 macOS 项目就不截图描述了,我就不用 SwiftUI 模板创建项目,因为我只想要显示一个窗口,输入相关也是通过键盘处理,界面上也没有太复杂的 UI,然后把 AppDelegate 类改一下import Cocoa @main class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow? func applicationDidFinishLaunching(_ aNotification: Notification) { window = NSApplication.shared.windows.first window?.title = "NES emulator" window?.setFrame(CGRect(x: 0, y: 0, width: 960, height: 544), display: true) window?.center() } func applicationWillTerminate(_ aNotification: Notification) { } func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { if !flag { for a in sender.windows { if let w = a as NSWindow? { w.makeKeyAndOrderFront(self) } } } return true } }现在第一步已经处理好,后续水文主要专注写 emulator 的逻辑,再把仿真后的内容展示在窗口上。
2021年02月23日
84 阅读
0 评论
0 点赞