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

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

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;	// 是否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_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大小 = 数量 * 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);
}

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

你可能感兴趣的:(c++,qt,FC,红白机)