Loading

从零开始的nes模拟器-[7] 总线

参考:

包含模块

前面在讲cpu的时候有提到总线的概念,这里我详细讲解总线的具体功能和实现代码。

首先,我们要回顾一下Nes的硬件布局。

image-20220213011234825

这里实际上还缺少了DMA (详见PPU精灵渲染的博文)

上面图中两条绿色的线就是 Nes 的 “总线” , 一条总线是 cpu 的 , 一条是 ppu 的。

我们之前在实现ppu代码的时候,已经把ppu的总线以及总线上对应的模块信息集成到 ppu 类里面了。

所以这个博文主要讲的是实现 cpu 的总线。

因为只是实现cpu的总线,所以我们只要根据图上的模块组装,然后按照cpu的内存布局写对应接口就好。

代码

总线包含的模块 , 以及cpu和其它设备交流的寄存器

class Bus
{

    .....
public: // Devices on Main Bus

	// The 6502 derived processor
	cpu_6502 cpu;	
	// The 2C02 Picture Processing Unit
	cpu_2C02 ppu;
	// The Cartridge or "GamePak"
	std::shared_ptr<Cartridge> cart;
	// 2KB of RAM
	uint8_t cpuRam[2048];
	// Controllers 手柄之类的
	uint8_t controller[2];
    // 这个暂时还没有实现... 先占个位
    apu_2A03 apu;
    

private:
	// Internal cache of controller state
	uint8_t controller_state[2];

private:
	// DMA 相关, 详见ppu 精灵渲染那个博客
	uint8_t dma_page = 0x00;
	uint8_t dma_addr = 0x00;
	uint8_t dma_data = 0x00;

	bool dma_dummy = true;

	bool dma_transfer = false;

   ...
};

实际上上面很多模块和寄存器前面的博客都出现过的。

初始化模块

加一些接口来初始化ppu和卡带

class bus{
public: // 系统接口
    // 将卡带智能指针对象连接到内部总线
    void insertCartridge(const std::shared_ptr<Cartridge> &cartridge);
    // 重置系统
    void reset();
}

初始化ppu和整体的变量

void Bus::insertCartridge(const std::shared_ptr<Cartridge> &cartridge)
{
    // 插入卡带,
    // 将卡带连接到bus和ppu
    this->cart = cartridge;
    ppu.ConnectCartridge(cartridge);
}


void Bus::reset()
{
    cart->reset();
    cpu.reset();
    ppu.reset();
    nSystemClockCounter = 0;
    dma_page = 0x00;
    dma_addr = 0x00;
    dma_data = 0x00;
    dma_dummy = true;
    dma_transfer = false;
}

cpu读写通道

添加两个函数让cpu可以通过这两个函数来和各个模块交流。可以说,这两个函数是整个总线类的精髓。

class bus{ 
public:
    // 总线中负责cpu的读写
    void cpuWrite(uint16_t addr, uint8_t data);
    uint8_t cpuRead(uint16_t addr, bool bReadOnly = false);
}

这里加一个参数bool bReadOnly ,是为了调试用的

这两个函数的实现是参照内存布局实现的

void Bus::cpuWrite(uint16_t addr, uint8_t data)
{
    if (cart->cpuWrite(addr, data))
    {
        // cartridge进行首次判断
        // cartridge可以根據需要將內容映射到其他任何地址
        // 通过这种方式,可以扩展硬件外设,比如枪。
    }
    else if (addr >= 0x0000 && addr <= 0x1FFF)
    {
        // 系统内存地址
        // 只有一共8kb,但是只有2kb可以用
        // 其他地址是2kb的镜像,使用&0x07ff运算,只使用2kb
        cpuRam[addr & 0x07FF] = data;
    }
    else if (addr >= 0x2000 && addr <= 0x3FFF)
    {
        // PPU地址
        // ppu只有8个寄存器,在0x2000 -
        // 0x3fff这个范围内,都是这8个寄存器的重复镜像数据
        // 所以直接使用&0x0007获取正确地址即可
        ppu.cpuWrite(addr & 0x0007, data);
    }
    else if ((addr >= 0x4000 && addr <= 0x4013) || addr == 0x4015 || addr == 0x4017)
    {
        // APU地址
        // apu的几个寄存器地址 0x4000 - 0x4013 0x4015 0x4017
        apu.cpuWrite(addr, data);
    }
    else if (addr == 0x4014)
    {
        // 0x4014地址是DMA传送开始标志
        // 当此地址被写入数据时,说明发生DMA传送
        // CPU周期将停止,将OAM寄存器中的数据传送到PPU中,64个精灵,直到传送结束
        dma_page = data;
        dma_addr = 0x00;
        dma_transfer = true;
    }
    else if (addr >= 0x4016 && addr <= 0x4017)
    {
        // 控制寄存器地址
        // 0x4016 - 0x4017是控制寄存器地址,cpu通过读取此地址中的数据
        // 获取控制命令
        controller_state[addr & 0x0001] = controller[addr & 0x0001];
    }
}

uint8_t Bus::cpuRead(uint16_t addr, bool bReadOnly)
{
    uint8_t data = 0x00;
    if (cart->cpuRead(addr, data))
    {
        // Cartridge 地址
    }
    else if (addr >= 0x0000 && addr <= 0x1FFF)
    {
        // 系统内存地址RAM, 每2kb都是镜像  2kb = 0x07FF
        data = cpuRam[addr & 0x07FF];
    }
    else if (addr >= 0x2000 && addr <= 0x3FFF)
    {
        // PPU地址 每8个字节都是镜像数据
        data = ppu.cpuRead(addr & 0x0007, bReadOnly);
    }
    else if (addr == 0x4015)
    {
        // APU读寄存器
        data = apu.cpuRead(addr);
    }
    else if (addr >= 0x4016 && addr <= 0x4017)
    {
        // 读取控制命令数据
        data = (controller_state[addr & 0x0001] & 0x80) > 0;
        controller_state[addr & 0x0001] <<= 1;
    }

    return data;
}

时钟

cpu总线可以同时访问 ppu 和 cpu ,所以可以顺便在这个类里面组织时序。

class bus{

private:
    // A count of how many clocks have passed
	uint32_t nSystemClockCounter = 0;
public:
     // Clocks the system - a single whole systme tick
	void clock();
}

时序里面的各个部分前面的博客都已经提及过了,这里只不过是再整体展示一下。

bool Bus::clock()
{

    // ppu时钟通常用来决定整个模拟器速度
    ppu.clock();

    // APU时钟
    apu.clock();

    // CPU的时钟周期比PPU和APU的慢3倍,也就是说,PPU和APU每执行3次,CPU执行一次
    // 这是NES模拟器中规定的
    // 使用nSystemClockCounter来记录ppu的时钟周期. 除3取余即可得到cpu时钟
    if (nSystemClockCounter % 3 == 0)
    {
        // 判断是否发生DMA传送(将OAM数据传送到PPU中)
        if (dma_transfer)
        {
            // TODO(tiansongyu): 可能存在cpu的周期bug
            // 这里虽然在cpu的周期中,但实际cpu不进行clock,
            // 此时DMA占用cpu周期
            // 通常占用513或514个周期
            // (等待写入完成时为 1 个等待状态周期,如果在奇数 CPU 周期中为 + 1,则为
            // 256 个交替读 / 写周期。) dma_dummy的默认是true
            if (dma_dummy)
            {
                // 执行周期必须是偶数,需要等待一个cpu周期
                if (nSystemClockCounter % 2 == 1)
                {
                    // DMA开始传送
                    dma_dummy = false;
                }
            }
            else
            {
                if (nSystemClockCounter % 2 == 0)
                {
                    // 偶数周期从cpu中读取数据,
                    // dma_page存储着 0x(dma_page)xx数据
                    // xx通常从0开始 ,0xFF结束,
                    // 64个精灵,每个精灵4个字节,所以是256个字节,也就是0xFF大小
                    // dma_page是地址的高位,dma_addr是地址的低位
                    // dma_data即为从cpu中读取的数据
                    dma_data = cpuRead(dma_page << 8 | dma_addr);
                }
                else
                {
                    // 奇数周期,将数据写到PPU中
                    ppu.pOAM[dma_addr] = dma_data;
                    // 需要增加dma的地址偏移
                    dma_addr++;
                    // dma_addr是一个字节的数据
                    // 当dma_addr大于0xFF时,会产生溢出
                    // dma_addr = 0 说明此时DMA传送停止
                    // 将标志寄存器更改
                    if (dma_addr == 0x00)
                    {
                        dma_transfer = false;
                        dma_dummy = true;
                    }
                }
            }
        }
        else
        {
            // 没有发生DMA传送时,CPU时钟正常进行。
            cpu.clock();
        }
    }

补充

总线应该还包含apu模块的内容,等我把apu实现了再填坑

posted @ 2022-02-15 15:17  CHZarles  阅读(421)  评论(0)    收藏  举报