在Go中编写NES仿真器

2020-05-18 08:10:25

我已经决定在GO中编写我自己的NES模拟器。我知道多年来已经编写了许许多多的NES仿真器(至少有一个是Ingo编写的),但是在使用了很多年之后,我一直想尝试编写我自己的仿真器。另外,这给了我一个很好的理由,让我更多地在Gosome上编程。我计划随着模拟器的进展每隔一段时间发一篇帖子。希望我中途不会失去兴趣!我将把该项目的源代码放到这个GitHub存储库中。

我的第一个任务是为NES使用的CPU编写模拟器Themos 6502。NTSC NES中使用的6502芯片的运行频率为1.789773 Mhz,即1,789,773个周期/秒。NES的6502不支持十进制模式,这意味着一些指令不需要支持,这是很好的。

MOS 6502是一个具有16位地址的8位处理器(小端,因此它希望每个16位地址的最低有效字节首先存储在内存中)。它没有I/O线,因此任何I/O寄存器都必须映射到16位地址空间。可以在这里和这里找到6502指令集的完整列表。

索引寄存器1&;2(X&;Y)-寄存器X和Y用于间接寻址,也用作计数器/索引。某些指令使用X来使用堆栈保存/恢复P的值。

堆栈指针(SP)-存储堆栈顶部的最低有效字节。6502的堆栈被硬连接到占用$0100-$01ff,SP在加电时初始化为$ff。如果SP的值是84美元,则堆栈的顶部位于0184美元。当值被上推时,堆栈的顶部在内存中向下移动,而值被弹出时,堆栈的顶部在内存中向下移动。

程序计数器(PC)-6502上唯一的16位寄存器,PC指向要执行的下一条指令。

处理器状态(P)-P中的位指示最后的算术和逻辑指令的结果,并指示中止/中断指令是否刚刚执行。

$0000-$00ff-由零页寻址指令使用。使用零页寻址的指令只需要8位地址参数。假设地址的最重要的8位是$00。这样做是为了节省内存,因为地址需要一半的空间。

实现CPU只需创建CPU内部结构和输入/输出线的表示,然后编写实现6502指令集的函数。

内存可以只是一个65,536(16位地址总线,所以2^16个地址)元素uint8数组。读/写内存仅获取和设置数组中的元素。现在,我将使用一个非常简单的BasicMemory类型来模拟6502的RAM:

类型内存接口{Reset()FETCH(地址uint16)(值uint8)store(地址uint16,值uint8)(OldValue Uint8)}类型基本内存[65536]uint8。

为了处理由NES完成的内存映射,我需要创建一个NSMemory类型,该类型实现内存接口,但其读取和存储函数理解NES的内存布局。具体地说,许多存储器范围或者被镜像到其他存储器范围,存储器被映射到PPU(图片处理单元)和APU(音频处理单元)的寄存器,或者被映射到实际的NES盒式磁带。有关详细信息,请参阅此处。

CPU类型存储6502的寄存器和指令表以及时钟输入和到存储器的链接:

类型状态uint8const(C status=1<;<;iota//进位标志Z//零标志I//中断禁用D//十进制模式B//Break命令_//-未使用-V//溢出标志N//负标志)类型寄存器结构{A uint8//累加器X uint8//索引寄存器X Y uint8//索引寄存器Y P状态//处理器状态SP uint8//堆栈指针PC uint16//PROGRAM。

仿真器的获取/执行周期在PC寄存器中存储的地址获取指令,在其指令表中查找操作码,然后执行它。每条指令应负责适当地修改堆栈/寄存器/存储器,以及针对参数的数量适当地递增PC寄存器(或者在分支指令的情况下使用参数值)。每条指令还需要确定它应该使用多少个时钟周期,因为一些指令根据它们的参数占用不同数量的时钟周期。

func(CPU*CPU)EXECUTE(){//FETCH操作码:=OpCode(cpu.memory y.fetch(cpu.registers.pc))inst,ok:=cpu.directions[opcode]if!OK{fmt.Printf(";No该类opcode 0x%x\n";,opcode)os.Exit(1)}//EXECUTE,exec()返回周期数:=inst.exec(CPU)//count Cycle

实施过程中的一个棘手问题是时间问题。为了使6502与NES的其他组件(如PPU和APU)正确交互,它必须在特定的时间内执行指令,并与主时钟保持同步。根据6502规范,执行每条指令需要一定数量的时钟周期。由于可以想当然地认为现代机器将能够比Areal6502芯片更快地执行每条指令,因此仿真器将需要对CPU进行节流以确保其不会执行得太快。我计划研究GO的时间包,特别是TickerData类型,以实现6502使用的时钟信号。这绝对是我最担心的部分。

我已经编写了基本架构,但到目前为止我只实现了LDA指令。在实现了指令集的其余部分之后,我将需要编写一些单元测试来确保一切正常工作。这应该会让我有机会试用Go的单元测试框架,特别是Go 1.2的新测试覆盖特性(这里有关于该特性的精彩博客文章)。

我一直在使用以下站点来帮助实现6502,并了解NES的内部结构。

由Disqus提供支持的评论