Loading

从零开始的nes模拟器-[2] 卡带和Mapper

Nes-卡带

nes游戏机本身是不附带游戏的,如果想玩某款游戏,就要买对应的游戏卡带。然后再把卡带插到机器上,才能玩。电脑不能插卡带,但是我们可以通过.nes文件来加载游戏。

nes文件本质上只是从卡带上 dump 下来的信息,所以我们可以通过读取nes文件的信息来模拟一个卡带。

Nes文件格式

ref: NES 模拟器开发教程 02 - NES 文件结构

INES 分为下列四个区域

Header
前 16 个字节,包含了 ROM 相关的所有信息

Trainer
如果 Header 中 Trainer 的 flag 为 1,则此区域为 512 字节,否则为 0。这块区域的作用这里有讨论:http://forums.nesdev.com/viewtopic.php?t=3657,总之我们没有必要模拟它,忽略掉就好了

PRG
这里存放了 NES 程序数据,即 CPU 总线上 0x8000 - 0xFFFF 的数据,具体大小在 Header 中给出

CHR
这里存放了 NES 图像数据,即 PPU 总线上 0x0000 - 0x2000 的数据,具体大小在 Header 中给出

Header 总共 16 字节,每个字节定义如下:

  • 0 - 3:NES
    这里是恒定的 4E 45 53 1A,对应着 ASCII 码的 'NES␚',可以用来检测是否为 NES 文件

  • 4:PRG 块数量,一块大小为 16KB

  • 5:CHR 块数量,一块大小为 8KB

  • 6:Flag

    76543210
    ||||||||
    |||||||+- Mirroring: 0: 水平镜像(PPU 章节再介绍)
    |||||||              1: 垂直镜像(PPU 章节再介绍)
    ||||||+-- 1: 卡带上有没有带电池的 SRAM
    |||||+--- 1: Trainer 标志
    ||||+---- 1: 4-Screen 模式(PPU 章节再介绍)
    ++++----- Mapper 号的低 4 bit
    
  • 7:Flag

    该 Flag 只有高 4 bit 有用,其他位暂时不需要了解

    76543210
    ||||||||
    |||||||+- VS Unisystem,不需要了解
    ||||||+-- PlayChoice-10,不需要了解
    ||||++--- 如果为 2,代表 NES 2.0 格式,不需要了解
    ++++----- Mapper 号的高 4 bit
    
  • 8-15:不常用,不需要了解

从 Header 中可以看出,我们只需要其中的 PRG,CHR,Mapper,Mirror,Trainer 等信息,所以解析也很简单

API 卡带( 部分 )

看完上面header的解析之后,我们知道了模拟卡带需要的信息。


// 墨盒对外暴露的接口只有连接cpu 和 连接ppu 的接口
// 墨盒内部存储的信息有 Program Memory 和 Pattern Memory (又称 character memory)
class Cartridge
{
public:
    explicit Cartridge(const std::string& sFileName);
    ~Cartridge();
    
public:// 辅助函数,判断帧是否渲染完成
    bool ImageValid();
private:
    bool bImageValid = false;

private:
    
    // 存储mirror 方式 ,  这个 enum 定义在 mapper.h 
    MIRROR hw_mirror = HORIZONTAL;

    // 记录 nes 文件信息
    uint8_t nMapperID = 0; // Mapper 类型
    uint8_t nPRGBanks = 0; // 程序的 bank 数
    uint8_t nCHRBanks = 0; // CHR rom bank 数

    // 用不定长数组存储 progrom rom 和 charatic rom
    std::vector<uint8_t> vPRGMemory;
    std::vector<uint8_t> vCHRMemory;
}

Mapper

上面只是讲解了卡带信息的主要部分。如果要模拟构建一个完整的卡带,还要了解一下Mapper的简单原理。

Mapper 并不位于主机中,而位于卡带上。每一张卡带都对应了一种 Mapper,在 NES 1.0 格式中,可以表示多达 256 种 Mapper

每种 Mapper 行为都不一样,比如有的 Mapper 增加了音频芯片提高音频性能,有的 Mapper 增加了寄存器扩展程序大小,有的还有中断计数的功能

比如 Mapper2,增加了 Bank 选择寄存器,以控制不同的 Bank 映射到 0x8000 - 0xFFFF,这样就扩充了游戏容量

首先回顾一下cpuppu ( ppu会在后面的博文详细介绍 ) 的内存布局 (左cpu , 右ppu)。

简单来说就是当cpu 和 ppu 访问到 红框地址范围的数据时候,它们实际访问的是卡带里面的数据。

而mapper相当于一个“隔离层" ,cpu/ppu访问这些地址的时候, 并不直接取数据,而是告知mapper要访问这个地址,然后mapper会在内部执行一下处理(不同的mapper有不用的处理,cpu/ppu是不知道mapper的行为的),帮助cpu/ppu得到想要的目标数值。

API Mapper ( 部分 )

这里只写了mapper的基本接口(如果是玩坦克大战,这几个接口也够了),后续可以丰富细节。

这里写成虚基类可以方面实现mapper多态。

class Mapper
{
  public:
    Mapper(uint8_t prgBanks, uint8_t chrBanks);
    ~Mapper();

  public:
      // 将 CPU 总线地址转换为 PRG ROM 偏移量
    virtual bool cpuMapRead(uint16_t addr, uint32_t& mapped_addr, uint8_t& data) = 0;
    virtual bool cpuMapWrite(uint16_t addr, uint32_t& mapped_addr, uint8_t data = 0) = 0;


    // 将 PPU 总线地址转换为 CHR ROM 偏移量
    virtual bool ppuMapRead(uint16_t addr, uint32_t& mapped_addr) = 0;
    virtual bool ppuMapWrite(uint16_t addr, uint32_t& mapped_addr) = 0;
    
 
 protected:
    // 许多映射器都需要这两个变量, 这些信息可以通过读取卡带提取
    // 这两个量通过构造函数初始化
	uint8_t nPRGBanks = 0;
	uint8_t nCHRBanks = 0;
};

API 卡带 ( 部分 )

构建了mapper后,我们把mapper放进卡带类里面

class Cartridge
{

    //指向mapper对象的指针
	std::shared_ptr<Mapper> pMapper;
    
    
public:
    // 获取加载的mapper对象
    std::shared_ptr<Mapper> GetMapper();
    
public:
	
	// 连接 Cpu 总线的
	bool cpuRead(uint16_t addr, uint8_t &data);
	bool cpuWrite(uint16_t addr, uint8_t data);

	
	// 连接 PPU 总线的
	bool ppuRead(uint16_t addr, uint8_t &data);
	bool ppuWrite(uint16_t addr, uint8_t data);
	
}

补充: 卡带构造函数

上面并没有包含完整的卡带类的信息( 因此我在后续讲解 ppu 会继续补充)

最后附上卡带类的构造函数,这个构造函数是完整的。通过阅读这个构造函数可以更清晰地知道怎么通过读取nes文件来模拟卡带信息

Cartridge::Cartridge(const std::string& sFileName) {

	// NES游戏文件的文件头 一共16字节
	// ref: https://www.jianshu.com/p/994f1663475a
	//一个临时变量
	struct sHeader {
		char name[4]; // 4E 45 53 1A,对应着 ASCII 码的 'NES␚',可以用来检测是否为 NES 文件
		uint8_t prg_rom_chunks; //一块chunk的大小为 16KB
		uint8_t chr_rom_chunks; //一块chunk大小为 8KB

		uint8_t mapper1; // flag 位
		/* mapper1 各个信息位的含义
		76543210
		||||||||
		|||||||+- Mirroring: 0: 水平镜像(PPU 章节再介绍)
		|||||||              1: 垂直镜像(PPU 章节再介绍)
		||||||+-- 1: 卡带上有没有带电池的 SRAM
		|||||+--- 1: Trainer 标志
		||||+---- 1: 4-Screen 模式(PPU 章节再介绍)
		++++----- Mapper 号的低 4 bit
		*/

		
		uint8_t mapper2;
		/*
		mapper2 各个位含义
		76543210
		||||||||
		|||||||+- VS Unisystem,不需要了解
		||||||+-- PlayChoice-10,不需要了解
		||||++--- 如果为 2,代表 NES 2.0 格式,不需要了解
		++++----- Mapper 号的高 4 bit
		*/
		uint8_t prg_ram_size;
		uint8_t tv_system1;
		uint8_t tv_system2;
		char unused[5];
	}header;


	bImageValid = false;

	if (nCHRBanks == 0)
	{
		// Create CHR RAM
		vCHRMemory.resize(8192);
	}
	else
	{
		// Allocate for ROM
		vCHRMemory.resize(nCHRBanks * 8192);
	}

	//以二进制形式打开文件
	std::ifstream ifs;
	ifs.open(sFileName, std::ifstream::binary);
	if (ifs.is_open()) {
		// Read file header 读文件头
		ifs.read((char*)&header, sizeof(sHeader));

		// 紧接着的512byte 是 training information ,没有什么用,直接忽略了
		if (header.mapper1 & 0x04)
			ifs.seekg(512, std::ios_base::cur);

		//计算出 mapper id
		nMapperID = ((header.mapper2 >> 4) << 4) | (header.mapper1 >> 4);
		mirror = (header.mapper1 & 0x01) ? VERTICAL : HORIZONTAL;
		//根据不同的nes文件类型做不同的操作
		uint8_t nFileType = 1;

		if (nFileType == 0) {

		}

		
		// 这种是最常见的
		if (nFileType == 1) {
			
			//读入程序数据
			nPRGBanks = header.prg_rom_chunks;
			// 计算有程序有多大
			vPRGMemory.resize(nPRGBanks * 16384); // 16 * 1024 = 16384
			// 读入 vector
			ifs.read((char*)vPRGMemory.data(), vPRGMemory.size());


			//读入 pattern 数据,同上
			nCHRBanks = header.chr_rom_chunks;
			vCHRMemory.resize(nCHRBanks * 8192); // 8 * 1024 = 8192 
			ifs.read((char*)vCHRMemory.data(), vCHRMemory.size());

		}

		if (nFileType == 2)
		{

		}

		switch (nMapperID) {

		case 0: pMapper = std::make_shared<Mapper_000>(nPRGBanks, nCHRBanks); break;
		//case   2: pMapper = std::make_shared<Mapper_002>(nPRGBanks, nCHRBanks); break;
		//case   3: pMapper = std::make_shared<Mapper_003>(nPRGBanks, nCHRBanks); break;
		//case  66: pMapper = std::make_shared<Mapper_066>(nPRGBanks, nCHRBanks); break;

		}

		ifs.close();
	}

}
posted @ 2022-01-20 16:35  CHZarles  阅读(1147)  评论(0编辑  收藏  举报