1. CPU 基本原理
CPU 的本質(zhì)叠殷,只有兩件事:
- 什么時(shí)候執(zhí)行什么指令
- 什么時(shí)候讀寫(xiě)什么地址的數(shù)據(jù)
CPU 有一個(gè)時(shí)鐘作為輸入源确镊,該時(shí)鐘實(shí)際上只是一個(gè)頻率很高的脈沖波拨与,一般幾 M 到 幾 GHZ瓦胎。傳統(tǒng)的 CPU 會(huì)在一個(gè)到多個(gè)時(shí)鐘期間執(zhí)行完一條指令,然后再執(zhí)行下一條指令衷快。如果某一時(shí)刻產(chǎn)生了中斷宙橱,CPU 會(huì)讀取中斷向量表對(duì)應(yīng)中斷地址,等到當(dāng)前指令執(zhí)行完后切換到該地址處繼續(xù)執(zhí)行
2. NES CPU
NES CPU 為 6502 指令集蘸拔,型號(hào)為 RP2A03师郑,CPU 時(shí)鐘 1.79 MHz,CPU 內(nèi)存映射已經(jīng)在 NES 模擬器開(kāi)發(fā)教程 01 講過(guò)了都伪,這里就不再贅述了
2.1 寄存器
2A03 有 6 個(gè)寄存器:A呕乎,X,Y陨晶,PC猬仁,SP,P先誉,除了 PC 為 16bit 以外湿刽,其他全都是 8bit
A
通常作為累加器X,Y
通常作為循環(huán)計(jì)數(shù)器PC
程序計(jì)數(shù)器褐耳,記錄下一條指令地址SP
堆棧寄存器诈闺,其值為 0x00 ~ 0xFF,對(duì)應(yīng)著 CPU 總線上的 0x100 ~ 0x1FF-
P
標(biāo)志寄存器比較麻煩铃芦,它實(shí)際上只有 6bit雅镊,但是我們可以看成 8bitBIT 名稱(chēng) 含義 0 C 進(jìn)位標(biāo)志,如果計(jì)算結(jié)果產(chǎn)生進(jìn)位刃滓,則置 1 1 Z 零標(biāo)志仁烹,如果結(jié)算結(jié)果為 0,則置 1 2 I 中斷去使能標(biāo)志咧虎,置 1 則可屏蔽掉 IRQ 中斷 3 D 十進(jìn)制模式卓缰,未使用 4 B BRK,后面解釋 5 U 未使用,后面解釋 6 V 溢出標(biāo)志征唬,如果結(jié)算結(jié)果產(chǎn)生了溢出捌显,則置 1 7 N 負(fù)標(biāo)志,如果計(jì)算結(jié)果為負(fù)总寒,則置 1 剛說(shuō)過(guò)標(biāo)志寄存器只有 6 bit扶歪,這是因?yàn)?B 和 U 并不是實(shí)際位,只不過(guò)某些指令執(zhí)行后偿乖,標(biāo)志位 push 到 stack 的時(shí)候击罪,會(huì)附加上這兩位以區(qū)分中斷是由 BRK 觸發(fā)還是 IRQ 觸發(fā)哲嘲,下面是詳細(xì)解釋
指令或中斷 U 和 B 的值 push 之后對(duì) P 的影響 PHP 指令 11 無(wú) BRK 指令 11 I 置 1 IRQ 中斷 10 I 置 1 MNI 中斷 10 I 置 1 可能光看表格也看不懂贪薪,下面用偽代碼舉個(gè)例子
function brk() { // 保存 PC push(PC & 0xFF); push(PC >> 8 & 0xFF); // 保存 P push(P | Flag.B | Flag.U); // 設(shè)置 P P |= Flag.I; // 從中斷向量表獲取對(duì)應(yīng)中斷地址 PC = ... }
2.2 中斷
2A03 支持 3 種中斷:
- RESET
復(fù)位中斷,RESET 按鈕按下后或者系統(tǒng)剛上電時(shí)產(chǎn)生 - NMI
不可屏蔽中斷眠副,該中斷不能通過(guò) P 的 I 標(biāo)志屏蔽画切,所以它一定能觸發(fā)。比如 PPU 在進(jìn)入 VBlank 時(shí)就會(huì)產(chǎn)生 NMI 中斷 - IRQ
可屏蔽中斷囱怕,如果 P 的 I 標(biāo)志置 1霍弹,則可以屏蔽該中斷,同時(shí)也可以通過(guò) BRK 指令由軟件自行觸發(fā)
產(chǎn)生中斷時(shí)娃弓,CPU 會(huì)將 PC 和 P 壓棧典格,之后 CPU 讀取中斷向量表對(duì)應(yīng)地址,賦給 PC台丛,同時(shí)設(shè)置相應(yīng)的標(biāo)志位耍缴。當(dāng)程序執(zhí)行 RTI(中斷返回) 后,CPU 將 P 和 PC 出棧挽霉,恢復(fù) P 和 PC防嗡,從中斷產(chǎn)生前的地址處繼續(xù)執(zhí)行
中斷可以理解為一個(gè)函數(shù)調(diào)用,區(qū)別在于該函數(shù)可以通過(guò)其他硬件隨時(shí)通過(guò)中斷來(lái)調(diào)用
中斷向量表:
中斷向量表位于 0xFFFA ~ 0xFFFF侠坎,共 6 字節(jié)蚁趁,分別對(duì)應(yīng) 3 個(gè)中斷:
- 0xFFFA, 0xFFFB: NMI 中斷地址
- 0xFFFC, 0xFFFD: RESET 中斷地址
- 0xFFFE, 0xFFFF: IRQ 中斷地址
2.3 指令集
指令集可以參考這兩個(gè)地址:
指令陣列如圖:
里面包含了 56 種官方指令和一些非官方指令
每個(gè)方塊包含了指令,尋址模式实胸,執(zhí)行周期他嫡,例如:
表示:LDY 指令,立即尋址庐完,執(zhí)行完需要 2 個(gè) CPU 時(shí)鐘
有些方塊執(zhí)行周期后面有個(gè)
*
號(hào)钢属,這說(shuō)明該指令在某些情況下需要額外增加 1 ~ 2 個(gè)時(shí)鐘才能完成,具體等介紹完尋址模式再解釋
2.4 尋址模式
2A03 支持 13 種尋址模式:
尋址模式 | 含義 | 數(shù)據(jù)長(zhǎng)度 | 例子 | 說(shuō)明 |
---|---|---|---|---|
Implicit | 特殊指令的尋址方式 | 0 | CLC | 清除 C 標(biāo)志 |
Accumulator | 累加器 A 尋址 | 0 | LSR A | A 右移一位 |
Immediate | 指定一個(gè)字節(jié)的數(shù)據(jù) | 1 | ADC #$1 | A 增加 1 |
Zero Page | 指定 Zero Page 地址 | 1 | LDA $00 | 將 0x0000 的值寫(xiě)入 A |
Zero Page,X | 指定 Zero Page 地址加上 X | 1 | STY $10,X | 將 0x0010 + X 地址上的值寫(xiě)入 Y |
Zero Page,Y | 指定 Zero Page 地址加上 Y | 1 | LDX $10,Y | 將 0x0010 + X 地址上的值寫(xiě)入 X |
Relative | 相對(duì)尋址 | 1 | BEQ LABEL | 如果 Z 標(biāo)志置位則跳轉(zhuǎn)到 LABEL 所在地址假褪,跳轉(zhuǎn)范圍為當(dāng)前 PC 的 -128 ~ 127 |
Absolute | 絕對(duì)尋址 | 2 | JMP $1234 | 跳轉(zhuǎn)到地址 0x1234 |
Absolute,X | 絕對(duì)尋址加上 X | 2 | STA $3000,X | 將 0x3000 + X 地址值寫(xiě)入 A |
Absolute,Y | 絕對(duì)尋址加上 Y | 2 | STA $3000,Y | 將 0x3000 + Y 地址值寫(xiě)入 A |
Indirect | 間接尋址(只有 JMP 使用) | 2 | JMP ($FFFC) | 跳轉(zhuǎn)到 0xFFFC 地址上的值表示的地址處 |
Indexed Indirect | 變址間接尋址 | 1 | LDA ($40,X) | 首先 X 的值加上 0x40署咽,得到一個(gè)地址,再以此地址上的值作為一個(gè)新地址,將新地址上的值寫(xiě)入 A |
Indirect Indexed | 間接變址尋址 | 1 | LDA ($40),Y | 首先獲取 0x40 處存儲(chǔ)的值宁否,將該值與 Y 相加窒升,得到一個(gè)新地址,然后將該地址上的值寫(xiě)入 A |
2.5 額外的時(shí)鐘
前面提到過(guò)在指令陣列圖中帶 *
的需要額外的時(shí)鐘慕匠,有兩種情況會(huì)額外增加時(shí)鐘
- 分支指令進(jìn)行跳轉(zhuǎn)時(shí)
分支指令比如 BNE饱须,BEQ 這類(lèi)指令,如果檢測(cè)條件為真台谊,這時(shí)需要額外增加 1 個(gè)時(shí)鐘 - 跨 Page 訪問(wèn)
新地址和舊地址如果 Page 不一樣蓉媳,即(newAddr & 0xFF00) !== (oldAddr & 0xFF00)
,則需要額外增加一個(gè)時(shí)鐘锅铅。例如0x1234
與0x12FF
為同一 Page酪呻,但是與0x1334
為不同 Page
以上兩種情況可以同時(shí)存在,所以一條指令可能會(huì)額外增加 1 ~ 2 個(gè)時(shí)鐘
2.6 BUG
NES 硬件存在一些 BUG盐须,這里講講間接尋址時(shí)的 BUG玩荠,因?yàn)橛行y(cè)試 ROM 會(huì)測(cè)試該行為
間接尋址時(shí),設(shè)地址為 x贼邓,page 起始地址為 n(即 n = x & 0xFF00)阶冈,若 x 低 8 位為 0xFF,則尋址地址高位為 n 上的值塑径,低位為 x 上的值女坑,一句話概括,就是地址無(wú)法跨 Page
下面舉個(gè)例子:
Address: 0x0200 0x0201 ... 0x02FF
Value: 0x01 0x02 ... 0x03
針對(duì)上述內(nèi)存统舀,執(zhí)行 JMP (0x02FF)
匆骗,則會(huì)跳轉(zhuǎn)到 0x0103 處
正常情況下,執(zhí)行 JMP (0x0200)
绑咱,則會(huì)跳轉(zhuǎn)到 0x0201 處
3. API 設(shè)計(jì)
CPU 工作時(shí)绰筛,以時(shí)鐘作為輸入源,每個(gè)幾個(gè)時(shí)鐘執(zhí)行一條指令描融,同時(shí)有三個(gè)中斷的輸入源:RESET铝噩,NMI,IRQ窿克,所以 CPU 一共只需要 4 個(gè)方法:
export interface ICPU {
clock(): void;
reset(): void;
irq(): void;
nmi(): void;
}
CPU 有 6 個(gè)寄存器骏庸,還需要定義寄存器接口:
export enum Flags {
C = 1 << 0, // Carry
Z = 1 << 1, // Zero
I = 1 << 2, // Disable interrupt
D = 1 << 3, // Decimal Mode ( unused in nes )
B = 1 << 4, // Break
U = 1 << 5, // Unused ( always 1 )
V = 1 << 6, // Overflow
N = 1 << 7, // Negative
}
export interface IRegisters {
readonly PC: uint16;
readonly SP: uint8;
readonly P: uint8;
readonly A: uint8;
readonly X: uint8;
readonly Y: uint8;
}
4. 實(shí)現(xiàn)
CPU 對(duì)外獲取數(shù)據(jù)的通道為 CPU BUS,所需 CPU 類(lèi)中需要存儲(chǔ) CPU BUS 實(shí)例
CPU 執(zhí)行指令所需周期數(shù)不等年叮,所以需要 deferCycles 表示 CPU 還需要多少個(gè) clock 才能執(zhí)行下一條指令
下面以最簡(jiǎn)單的 JMP 舉一個(gè)例子:
enum InterruptVector {
NMI = 0xFFFA,
RESET = 0xFFFC,
IRQ = 0xFFFE,
}
export class CPU implements ICPU {
public bus: IBus;
private deferCycles = 0;
private readonly registers = new Registers();
public reset(): void {
this.registers.A = 0;
this.registers.X = 0;
this.registers.Y = 0;
this.registers.P = 0;
this.registers.SP = 0xfd;
this.registers.PC = this.bus.readWord(InterruptVector.RESET);
this.deferCycles = 8;
this.clocks = 0;
}
public clock(): void {
if (this.deferCycles === 0) {
this.step();
}
this.deferCycles--;
}
public irq(): void {
if (this.isFlagSet(Flags.I)) {
return;
}
this.pushWord(this.registers.PC);
this.pushByte((this.registers.P | Flags.U) & ~Flags.B);
this.setFlag(Flags.I, true);
this.registers.PC = this.bus.readWord(InterruptVector.IRQ);
this.deferCycles += 7;
}
public nmi(): void {
// 和 IRQ 大體相同具被,這里就不重復(fù)寫(xiě)了
}
private step(): void {
const opcode = this.bus.readByte(this.registers.PC++);
switch (opcode) {
case 0x4C:
// JMP abs 3
const address = this.bus.readWord(this.registers.PC);
this.registers.PC = address;
this.deferCycles += 3;
break;
default:
throw new Error(`Invalid opcode ${opcode}`);
}
}
}
用 switch case 雖然在編譯器優(yōu)化的情況下效率不會(huì)降低,但是代碼看起來(lái)不簡(jiǎn)潔只损,這里為了解釋起來(lái)簡(jiǎn)單采用了 switch case一姿,實(shí)際上項(xiàng)目中最好用數(shù)組或?qū)ο蟠鎯?chǔ)映射關(guān)系
5. TIPS
CPU 介紹得差不多了七咧,其他指令也都千篇一律,參考 6502 指令集就能寫(xiě)出來(lái)了叮叹,實(shí)在不清楚的參考下其他模擬器代碼就行
最后提醒下有個(gè)測(cè)試文件對(duì)于 CPU 開(kāi)發(fā)非常有幫助:
下載地址:http://www.qmtpro.com/~nes/misc/
下載這 2 個(gè)文件:nestest.log
和 nestest.nes
開(kāi)發(fā) CPU 時(shí)艾栋,先將 PC 設(shè)置為 0xC000,再讓 CPU 運(yùn)行蛉顽,每運(yùn)行一條指令后蝗砾,和它的 nestest.log 對(duì)照一下各個(gè)寄存器狀態(tài),這樣能按照它的 log 順序逐條開(kāi)發(fā)出正確的指令携冤,比一口氣開(kāi)發(fā)完再去 debug 好得多