API
之前講過 NES 有 CPU 和 PPU 兩條總線,總線使 CPU 或 PPU 具備了與其他模塊通信的能力,所以設(shè)計 CPU 之前首先需要設(shè)計 CPU 總線,好在它并不復(fù)雜,不管是 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;
}
這里設(shè)計區(qū)分了 byte 和 word 主要是為了調(diào)用的時候方便,其實 word 的 api 完全可以用 byte 的 api 進行封裝,比如:
public readWord(address: uint16): uint16 {
return (this.readByte(address + 1) << 8 | this.readByte(address)) & 0xFFFF;
}
實現(xiàn)
在 NES 模擬器開發(fā)教程 01 - NES 系統(tǒng)結(jié)構(gòu) 中,已經(jīng)介紹過了 CPU 內(nèi)存映射。CPU BUS 的實現(xiàn),就是根據(jù)內(nèi)存映射去讀寫不同的硬件
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, 暫時不實現(xiàn)
} 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, 暫時不實現(xiàn)
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 都實現(xiàn)后,需要在 Emulator 構(gòu)造函數(shù)中初始化:
const cartridge = new Cartridge(nesData, new Uint8Array(8192));
const cpuBus = new CPUBus();
cpuBus.cartridge = cartridge; // 將 Cartridge 和 CPU BUS 關(guān)聯(lián)起來
這樣初始化之后,就可以通過 CPU BUS 讀取總線上的任意數(shù)據(jù)了,例如讀取 RESET 中斷向量指向的地址:
const resetAddress = cpuBus.readWord(0xFFFA);