首先列上参考的资料来源
算是最全的FC相关资料的网站,能在这里找到所有的FC硬件信息以及逻辑(个人认为查找信息可以,但是不适合用来上手,信息过于详细)
我能找到的最全、最详细的中文模拟器开发日志,本文基于此,对其中没有详细说的细节或者需要注意的地方进行补充
-
FC模拟器字节顺序
FC模拟器使用的CPU,8位2A03 NMOS处理器,是6502的改版,该CPU虽然是8位,但是地址空间是16位,所以有字节序概念,且字节序是小端模式。
该字节顺序主要影响rom文件的读取,以及CPU指令读取16bit地址。
-
PC寄存器启动以及重置时的状态
从CPU地址的0xFFFC读取低字节,0xFFFD读取高字节,组成16bit地址,存入PC寄存器中。
其他的寄存器初始状态可以从wiki中找到。
-
CPU指令执行需要的CPU周期数
在初期开发的时候,就要预留好统计各指令执行所需要的CPU周期数,用于各模块执行的同步。
-
图像渲染模块(PPU)上手
FC主要适应两种显示类型,PAL和NTSC,其中NTSC的显示频率约等于60帧,建议按照NTSC做。
FC的图像绘制单元是像素,绘制顺序是一行一行,一个坐标点一个坐标点的画。
PPU中有一个包含64个颜色的写死在硬件中的调色板,不同硬件类型的FC,颜色上的差异,主要就是由此产生的。
PPU的地址0x3F00-0x3F1F中存储的,就是上面提到的调色板的索引值。程序是可以修改这段地址中的值,从而改变屏幕上的颜色的。其中低16字节用于背景,高16字节用于精灵。
背景绘制大体流程:
- 当前屏幕xy坐标+屏幕滚动偏移坐标---->Nametable中对应位置的值(即Pattern table的索引)---->Pattern table中数据组合获取当前坐标的颜色信息低两位。
- 当前屏幕xy坐标+屏幕滚动偏移坐标---->上一步Nametable后紧随的Attribute table对应位置的值---->获取当前坐标的颜色信息高两位
- 上面两步组合成4bit数据---->PPU地址0x3F00-0x3F0F背景调色板---->PPU中固定的64个RGB颜色,绘制像素点
复杂点说,是根据当前像素的xy坐标加上屏幕滚动信息(最开始实现时可不考虑滚动),从Nametable中获取8*8图块、即Pattern table的索引,然后根据Pattern table中的信息,计算出8*8图块中每个像素调色板索引的索引的低两位。另外再从紧随Nametable的Attribute table中,取得该8*8图块所有像素的调色板索引的索引高两位。将这些信息对应组合,获取8*8图块每个像素的4bit调色板索引的索引,从上面提到的低16字节获取调色板索引,再获取调色板实际的RGB信息,最终绘制在屏幕上。
精灵绘制大体流程:
- 从oam中取出4个字节,获取精灵左上角xy坐标
- Byte 1中获取Pattern table的索引---->Pattern table数据组合获取颜色信息低两位
- Byte 2中获取颜色信息高两位
- 上面两步组合成4bit数据---->PPU地址0x3F10-0x3F1F精灵调色板---->PPU中固定的64个RGB颜色,绘制像素点
复杂点说(以8*8精灵为例),首先从oam中确认当前精灵的xy坐标,然后再根据PPUCTRL中bit3确认Pattern tables,oam的Byte 1确认Pattern tables的下标,获取精灵的全部8*8像素调色板索引的索引低两位,再通过oam的Byte 2低两位,获取像素调色板索引的索引高两位。这样就和背景一样,获取到4比特调色板索引的索引,再去上面提到的高16字节,获取调色板索引值,最终获取RGB信息。
绘制精灵时,如果调色板索引的索引低两位都是0,则表示该像素点透明。
在实现屏幕滚动(scrolling)时,最好按照wiki中描述的那样,实现PPU内部的寄存器,vtxw,这样方便做地址映射。
初期最好以行为单位绘制图像,每画完一行,就执行341.5个CPU时钟周期,这就是简单的时钟同步。最精准的时钟同步是,每画一个像素点,就执行3个CPU时钟周期。
画完240行之后,再执行341.5个PPU时钟周期,置vblank为1,再根据PPUCTRL中的bit7确认是否执行NMI中断,然后执行341.5*20个时钟周期,清除vblank标志,最后再执行一次341.5个(奇数帧340.5个)时钟周期。这样就完成一整帧的工作,即wiki中说的0-261行共262行扫描。
-
APU声音模块
如果使用SDL作为底层实现,需要根据wiki上面写的方式,通过查表或者公式计算进行混音,得到的结果是浮点数,
APU中使用最多的一个基本模块,就是divider,他的最重要的特点,当触发他的时候,它先检查当前值是否为0,为0则发出信号,不为0则递减1。所以wiki中会说,设置divider的值为P,实际上它输出信号的周期为P+1。这个是永远都不会变的,在查看wiki的时候,如果他写的是设置周期为P+1,我们在实现的时候一定要记住,保存的变量为P。我个人就是被wiki中sweep的描述误导,方波声音总是不对,再加上查看了好几个模拟器的实现代码都是设置+1,可是时钟发信号的地方都是1变0就发出了。
三角波静音的时候,是持续输出最后一个值,而不是输出0,否则会听到很清晰的“BABA”爆破音。
最后是建议使用blip_buffer库作为混音库,该库是wiki上最初提供APU信息的人编写的,这是地址http://www.slack.net/~ant/