(继上篇:原理篇,下:实现篇)
首先我们在QT中创建两个类,Cartridge 与 Mapper类:
在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; // 是否FourScreen
bool 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.h
class Cartridge
{
Mapper* mapper; //mapper指针
rominfo_t rominfo; //rom信息
public:
Cartridge();
//加载并解析ROM
bool 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 。
另一个则是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大小 = 数量 * 16KB
rominfo.prgrom_size = rominfo.prgrom_count * 16 * 1024;
//CHR ROM大小 = 数量 * 8 KB
rominfo.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);
}
同样是创建一个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!
修改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信息外,我们暂时无法看到任何现象。这多少令人遗憾,但是我们已经卖出了重要的一步。加油。
待上传....
【小提示】
文中有到的测试ROM可以百度网盘中下载:
https://pan.baidu.com/s/1ZrlJUlbGcOs4CDalehkXnw
提取码:3qg1