(继上篇:原理篇,下:实现篇)
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_t和nesheader_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