NES 模擬器開(kāi)發(fā)教程 07 - CPU

1. CPU 基本原理

CPU 的本質(zhì)叠殷,只有兩件事:

  1. 什么時(shí)候執(zhí)行什么指令
  2. 什么時(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雅镊,但是我們可以看成 8bit

    BIT 名稱(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è)地址:

指令陣列如圖:


image.png

里面包含了 56 種官方指令和一些非官方指令

每個(gè)方塊包含了指令,尋址模式实胸,執(zhí)行周期他嫡,例如:

image.png

表示: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í)鐘

  1. 分支指令進(jìn)行跳轉(zhuǎn)時(shí)
    分支指令比如 BNE饱须,BEQ 這類(lèi)指令,如果檢測(cè)條件為真台谊,這時(shí)需要額外增加 1 個(gè)時(shí)鐘
  2. 跨 Page 訪問(wèn)
    新地址和舊地址如果 Page 不一樣蓉媳,即 (newAddr & 0xFF00) !== (oldAddr & 0xFF00),則需要額外增加一個(gè)時(shí)鐘锅铅。例如 0x12340x12FF 為同一 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.lognestest.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 好得多

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末悼粮,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子曾棕,更是在濱河造成了極大的恐慌扣猫,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件睁蕾,死亡現(xiàn)場(chǎng)離奇詭異苞笨,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)子眶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)序芦,“玉大人臭杰,你說(shuō)我怎么就攤上這事⊙柚校” “怎么了渴杆?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)宪塔。 經(jīng)常有香客問(wèn)我磁奖,道長(zhǎng),這世上最難降的妖魔是什么某筐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任比搭,我火速辦了婚禮,結(jié)果婚禮上南誊,老公的妹妹穿的比我還像新娘身诺。我一直安慰自己,他們只是感情好抄囚,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布霉赡。 她就那樣靜靜地躺著,像睡著了一般幔托。 火紅的嫁衣襯著肌膚如雪穴亏。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,268評(píng)論 1 309
  • 那天,我揣著相機(jī)與錄音嗓化,去河邊找鬼锅劝。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蟆湖,可吹牛的內(nèi)容都是我干的故爵。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼隅津,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼诬垂!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起伦仍,我...
    開(kāi)封第一講書(shū)人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤结窘,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后充蓝,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體隧枫,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年谓苟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了官脓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涝焙,死狀恐怖卑笨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情仑撞,我是刑警寧澤赤兴,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站隧哮,受9級(jí)特大地震影響桶良,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沮翔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一陨帆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鉴竭,春花似錦歧譬、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至璧眠,卻和暖如春缩焦,著一層夾襖步出監(jiān)牢的瞬間读虏,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工袁滥, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盖桥,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓题翻,卻偏偏與公主長(zhǎng)得像揩徊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子嵌赠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容