API
之前讲过 NES 有 CPU 和 PPU 两条总线,总线使 CPU 或 PPU 具备了与其他模块通信的能力,所以设计 CPU 之前首先需要设计 CPU 总线,好在它并不复杂,不管是 CPU 还是 PPU 总线,只需要 读 和 写 两个接口
export interface IBus {
writeByte(address: uint16, data: uint8): void;
writeWord(address: uint16, data: uint16): void;
readByte(address: uint16): uint8;
readWord(address: uint16): uint16;
}
这里设计区分了 byte 和 word 主要是为了调用的时候方便,其实 word 的 api 完全可以用 byte 的 api 进行封装,比如:
public readWord(address: uint16): uint16 {
return (this.readByte(address + 1) << 8 | this.readByte(address)) & 0xFFFF;
}
实现
在 NES 模拟器开发教程 01 - NES 系统结构 中,已经介绍过了 CPU 内存映射。CPU BUS 的实现,就是根据内存映射去读写不同的硬件
export class CPUBus implements IBus {
public cartridge: ICartridge;
private readonly ram = new Uint8Array(2048);
public writeByte(address: uint16, data: uint8): void {
if (address < 0x2000) {
// RAM
this.ram[address & 0x07FF] = data;
} else if (address < 0x6000) {
// IO Registers, 暂时不实现
} else {
// Cartridge
this.cartridge.mapper.write(address, data);
}
}
public writeWord(address: uint16, data: uint16): void {
this.writeByte(address, data & 0xFF);
this.writeByte(address + 1, (data >> 8) & 0xFF)
}
public readByte(address: uint16): uint8 {
if (address < 0x2000) {
// RAM
return this.ram[address & 0x07FF];
} else if (address < 0x6000) {
// IO Registers, 暂时不实现
return 0;
} else {
// ROM
return this.cartridge.mapper.read(address);
}
}
public readWord(address: uint16): uint16 {
return (this.readByte(address + 1) << 8 | this.readByte(address)) & 0xFFFF;
}
}
初始化
Cartridge 和 CPU BUS 都实现后,需要在 Emulator 构造函数中初始化:
const cartridge = new Cartridge(nesData, new Uint8Array(8192));
const cpuBus = new CPUBus();
cpuBus.cartridge = cartridge; // 将 Cartridge 和 CPU BUS 关联起来
这样初始化之后,就可以通过 CPU BUS 读取总线上的任意数据了,例如读取 RESET 中断向量指向的地址:
const resetAddress = cpuBus.readWord(0xFFFA);