使用C++实现FC红白机模拟器 Cartridge 与 Mapper(实现篇)

news/2024/12/2 16:39:58/

(继上篇:原理篇,下:实现篇)

2. Cartridge 与 Mapper的实现

首先我们在QT中创建两个类,Cartridge 与 Mapper类:

  • Cartridge 类负责加载和解析ROM,因为CPU和PPU的内存映射都有指向卡带的部分(如果你忘了请看上篇:原理篇),因此需要分别提供CPU和PPU的读写接口。
  • Mapper 类负责地址空间与ROM的实际映射关系。

2.1 Cartridge 类实现

在Qt中创建Cartridge类并生成cartridge.h与cartridge.cpp两个文件。

cartridge.h内容如下:

#ifndef CARTRIDGE_H
#define CARTRIDGE_H#include "stdint.h" //需包含STDINT.h头文件才能支持uint_8这些类型typedef struct{uint8_t prgrom_count;uint8_t chrrom_count;uint8_t * prgrom; //程序镜像指针uint32_t prgrom_size;uint8_t * chrrom;//图像资源指针uint32_t  chrrom_size;bool is_vmirroring;	// 是否Vertical Mirroring(否则为水平)bool is_fourscreen;	// 是否FourScreenbool has_battery_backed;	// 是否有SRAM(电池供电的)bool has_trainer; //是否有Trainer部分
}rominfo_t;typedef struct {uint8_t nes[4];uint8_t prg_bank_count;uint8_t chr_bank_count;uint8_t flag1;uint8_t flag2;}nesheader_t;class Mapper; //这里没有直接包含mapper.hclass Cartridge
{Mapper* mapper; //mapper指针rominfo_t rominfo; //rom信息
public:Cartridge();//加载并解析ROMbool loadRom(char * rom,int size);//提供CPU读的接口uint8_t ReadViaCpu(uint16_t address);//提供PPU读的接口uint8_t ReadViaPPU(uint16_t address);//提供CPU写的接口void WriteViaCpu(uint16_t address,uint8_t data);//提供PPU写的接口void WriteViaPPU(uint16_t address, uint8_t data);
};#endif // CARTRIDGE_H

小提示:

这是首次出现代码,因此为了方便理解我把头文件完整的贴了出来。后面的代码中则只出现主要代码,像宏定义、引入头文件这些则会省略。

需要解释的东西都已经写在了注释,这里只特殊强调两个地方: 

一个是这里声明了两个结构体:rominfo_tnesheader_t

  • rominfo_t :存放解析后的rom信息,如uint8_t prgrom_count表示PRG ROM的数量,而uint8_t * prgrom则是指向PRG ROM的指针。其他几个成员可以对照注释理解。
  • nesheader_t :这个则表示的是ROM文件头部信息的16字节。方便我们解析ROM头使用。

另一个则是class Cartridge前面有一个前置定义:class Mapper。为什么我们不直接引用mapper.h的头文件呢?这个我们在实现mapper的时候会说到。总之这里姑且这么写。

其他的成员变量和方法可以参看注释。

cartridge.cpp主要内容如下:

首先是根据上面讲过的.nes文件格式去解析rom信息:

bool Cartridge::loadRom(char *rom, int size)
{//rom如果小于16字节则一定是错误的文件if(size < 16){return false;}nesheader_t * header = (nesheader_t*)rom;//验证文件头,检测文件前四个字节是否是NES,否则为错误文件if(!(header->nes[0] == 'N'&& header->nes[1] == 'E'&& header->nes[2] == 'S'&& header->nes[3] == 0x1a)){return false;}uint8_t map = header->flag1 >> 4; //获取Mapper第四位map |= (header->flag2 & 0xf0); //获取Mapper高四位// 获取程序镜像块数量rominfo.prgrom_count = header->prg_bank_count;// 获取图像镜像块数量rominfo.chrrom_count = header->chr_bank_count;//PRG ROM大小 = 数量 * 16KBrominfo.prgrom_size = rominfo.prgrom_count * 16 * 1024;//CHR ROM大小 = 数量 * 8 KBrominfo.chrrom_size = rominfo.chrrom_count *  8 * 1024;//动态内存分配方便后面使用rominfo.prgrom = new uint8_t[rominfo.prgrom_size];rominfo.chrrom = new uint8_t[rominfo.chrrom_size];memcpy(rominfo.prgrom,rom + 16,rominfo.prgrom_size);memcpy(rominfo.chrrom,rom + 16 + rominfo.prgrom_size ,rominfo.chrrom_size);//杂项设置rominfo.is_vmirroring = (header->flag1) & 0x1;rominfo.has_battery_backed = (header->flag1 >> 1) & 0x1;rominfo.has_trainer = (header->flag1 >> 2) & 0x1;rominfo.is_fourscreen = (header->flag1 >> 3) & 0x1;qDebug("MAPPER %d,PRG BANK COUNT %d,CHR BANK COUNT %d\n",map,rominfo.prgrom_count,rominfo.chrrom_count);this->mapper = new Mapper(&rominfo);return true;
}

需要注意的是函数return前面的 this->mapper = new Mapper(&rominfo);这里实例化了我们稍后会首先的Mapper,不要忘记这一步。正常来说应该是根据rom头文件中记录的mapper号实例化不同的Mapper。不过这里简单起见我们暂时直接实例化Mapper,后续再修改。

然后就是对CPU和PPU提供的读写接口,因为地址空间和rom的映射由Mapper负责,所以我们直接调用Mapper的接口即可。

uint8_t Cartridge::ReadViaCpu(uint16_t address)
{return this->mapper->ReadViaCpu(address);
}uint8_t Cartridge::ReadViaPPU(uint16_t address)
{return this->mapper->ReadViaPPU(address);
}void Cartridge::WriteViaCpu(uint16_t address, uint8_t data)
{this->mapper->WriteViaCpu(address,data);
}void Cartridge::WriteViaPPU(uint16_t address, uint8_t data)
{this->mapper->WriteViaPPU(address,data);
}

2.2 Mapper的实现

同样是创建一个Mapper类,分别生成mapper.h和mapper.cpp

mapper.h内容如下:

#ifndef MAPPER_H
#define MAPPER_H
#include "stdint.h"
#include "cartridge.h"
class Mapper
{rominfo_t * rominfo;
public:Mapper(rominfo_t * rominfo);uint8_t ReadViaCpu(uint16_t address);uint8_t ReadViaPPU(uint16_t address);void WriteViaCpu(uint16_t address,uint8_t data);void WriteViaPPU(uint16_t address, uint8_t data);
};#endif // MAPPER_H

需要注意的是,因为初始化的时候需要传入rominfo_t结构体,因此引用了cartridge.h。这也是为什么cartridge.h中没有直接包含mapper.h而使用了前置定义。因为如果不这么做就会造成循环包含,编译出错!当然你可以把rominfo_t结构体定义在一个单独的头文件中。

mapper.cpp内容如下:

#include "mapper.h"Mapper::Mapper(rominfo_t *rom):rominfo(rom)
{}/*** CPU读取PRG ROM 地址空间:0x8000-0xFFFF* @brief Mapper::ReadViaCpu* @param address* @return*/
uint8_t Mapper::ReadViaCpu(uint16_t address)
{if(address >= 0x8000){//如果PRG ROM只有一个,则//0xc000~0xFFFF地址是0x8000~0xbFFF的镜像if(rominfo->prgrom_count == 1){address -= 0x4000;}return this->rominfo->prgrom[address - 0x8000];}return 0;
}/*** CPU写入PRG ROM 地址空间:0x8000-0xFFFF* @brief Mapper::WriteViaCpu* @param address* @param data*/
void Mapper::WriteViaCpu(uint16_t address, uint8_t data)
{//实际上通常这里不会有写入,因此可以不实现if(address >= 0x8000){//如果PRG ROM只有一个,则//0xc000~0xFFFF地址是0x8000~0xbFFF的镜像if(rominfo->prgrom_count == 1){address -= 0x4000;}this->rominfo->prgrom[address - 0x8000] = data;}
}/*** PPU 读取CHR ROM 地址空间:0x0000-0x1FFF* @brief Mapper::ReadViaPPU* @param address* @return*/
uint8_t Mapper::ReadViaPPU(uint16_t address)
{return this->rominfo->chrrom[address];
}/*** PPU 写入CHR ROM 地址空间:0x0000-0x1FFF* @brief Mapper::WriteViaPPU* @param address* @param data*/
void Mapper::WriteViaPPU(uint16_t address, uint8_t data)
{this->rominfo->chrrom[address] = data;
}

内容比较简单不过多赘述,主要别忘记构造方法中初始化rominfo

2.3 调用

修改mainwindows.cpp

MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);//文件操作std::fstream fs;fs.open("D:\\Qt\\Project\\STUFC\\roms\\nestest.nes",std::ios::binary|std::ios::in);if(!fs.is_open()){QMessageBox::critical(this,"ERROR","rom open failed",QMessageBox::Ok);return;}//内部指针移动到文件尾部fs.seekg(0,fs.end);//获取文件的长度int romsize = fs.tellg();//重新把内部指针移动到开始的位置fs.seekg(0, fs.beg);//开辟一个缓冲区char* rommem = new char[romsize];//读取全部内容到缓冲区内fs.read(rommem,romsize);//读完及时关闭fs.close();//实例化并调用Cartridge类Cartridge cartridge;if(!cartridge.loadRom(rommem,romsize)){QMessageBox::critical(this,"ERROR","rom load failed",QMessageBox::Ok);}//释放缓冲区delete [] rommem;
}

我为了图方便直接在构造方法中调用了,您可以可以选择其他地方。例如按钮点击信号的槽函数中。

这里首先打开了测试ROM文件:nestest.nes,然后读取他的全部并传给我们之前实现的Cartridge 类的loadRom方法。

//...省略...
Cartridge cartridge;
if(!cartridge.loadRom(rommem,romsize))
{//...省略....
}
//...省略...

至此我们这部分就完全实现了。遗憾的是,除了打印一些ROM信息外,我们暂时无法看到任何现象。这多少令人遗憾,但是我们已经卖出了重要的一步。加油。

3. 本章完整代码:

待上传....

【小提示】

文中有到的测试ROM可以百度网盘中下载:

https://pan.baidu.com/s/1ZrlJUlbGcOs4CDalehkXnw 

提取码:3qg1


http://www.ppmy.cn/news/165217.html

相关文章

【游戏开发实战】重温红白机经典FC游戏,顺便教你快速搭建2D游戏关卡(Tilemap | 场景 | 地图)

文章目录 一、前言&#xff1a;童年的回忆二、本文Unity制作的效果三、情怀来了&#xff0c;想玩FC游戏1、下载FC模拟器&#xff0c;推荐nestopia2、启动nestopia3、下载FC卡带包&#xff1a;76合1&#xff08;76in1&#xff09;4、载入卡带&#xff08;.nes&#xff09;5、设置…

nes 红白机模拟器 第1篇

对比了很多种&#xff0c;开源的 NES 模拟器 VirtuaNES , nestopia , FakeNES , FCEUX , InfoNES , LiteNES 最后决定使用 LiteNES 进行移值&#xff0c;它是由 mynes 移值而来。LiteNES 对 mynes 代码进行整理兼容了 C99 标准&#xff0c;编译时无警告。 https://github.com/N…

使用C++实现FC红白机模拟器 Cartridge 与 Mapper(原理篇)

1. 认识nes文件 我们既然是模拟&#xff0c;就不可能使用实体的卡带硬件。那我们如何获取游戏文件呢&#xff1f;好在已经有人为我们准备好了&#xff08;心怀感恩&#xff09;。 .nes文件是NES&#xff08;FC&#xff09;的rom文件&#xff0c;关于它的来龙去脉这里就不做详细…

童年经典,任天堂switch红白机游戏

相信很多90后的小伙伴应该玩过任天堂的游戏&#xff0c;比如魂斗罗&#xff0c;影子传说等&#xff0c;如图&#xff1a; 今天小编就给大家带来了这么一个红白机的JavaScript版本&#xff0c;我们可以直接在浏览器中玩&#xff0c;当然你也可以在模拟器中玩&#xff0c;这里…

大数据教程【05.01】--Python 数据分析简介

更多信息请关注WX搜索GZH&#xff1a;XiaoBaiGPT Python数据分析简介 本教程将介绍如何使用Python进行大数据分析。Python是一种功能强大且易于使用的编程语言&#xff0c;具备丰富的数据分析库和工具。在本教程中&#xff0c;我们将涵盖以下主题&#xff1a; 数据分析准备工作…

2023年上半年系统规划与管理师上午真题及答案解析

1.香农用概率来定量描述信息的公式如下&#xff0c;其中H(x)表示X的( )&#xff0c;Pi是( )出现第i种状态的( )。 A.信息熵 事件 概率 B.总熵 单位 概率 C.信息熵 单位 概率 D.总熵 单位 度量 2.信息传输模型中&#xff0c;( )负责信息的向外传播&#xff0c;( )负责…

代码随想录算法训练营15期 Day 10 | 理论基础、232.用栈实现队列、225. 用队列实现栈

理论基础 队列是先进先出&#xff0c;栈是先进后出。 四个问题&#xff1a; C中stack 是容器么&#xff1f;我们使用的stack是属于哪个版本的STL&#xff1f;我们使用的STL中stack是如何实现的&#xff1f;stack 提供迭代器来遍历stack空间么&#xff1f; 栈和队列是STL&am…

硬盘录像机nvr装硬盘操作

今天装维一台新的硬盘录像机&#xff0c;海康威视牌子&#xff0c;上电后就一直叫&#xff0c;拆开来看&#xff0c;原来还没装硬盘&#xff0c;手头有一台旧的nvr&#xff0c;领导叫我拆下来换 首先拧下螺丝&#xff0c;注意把螺丝按位置顺序放好&#xff0c;方便拧回去&…