這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。本文由 伯樂線上 - JackalHu 翻譯,toolate 校稿。未經許可,禁止轉載!
英文出處:fogleman。歡迎加入翻譯小組。
譯註:Family Computer(簡稱 FC)是任天堂(Nintendo)公司發行的家用遊戲主機。日版 FC 機身以紅色和白色為主,因此在華人圈中又有“紅白機”的俗稱;歐美版 FC 在歐美則稱 Nintendo Entertainment System(簡稱 NES)。
最近我編寫了一個 FC 模擬器。製作這樣一個模擬器主要是出於興趣以及為了從中學習 FC 的工作原理。在這個過程中我學到了很多有趣的知識,於是寫下這篇文章同諸位分享我所學到的內容。由於相關的文檔已經有很多了,所以這裡我只打算講述一些有趣的特性。請注意,接下來都將是些技術方面的內容。
圖1 我的模擬器可以將畫面錄製成 GIF。這是我正在玩《大金剛》(Donkey Kong)的畫面。
CPU
FC 使用 MOS 6502(主頻1.79MHz)作為其CPU。6502 是一枚誕生於 1975 年(距今已有 40 年之久了)的 8位微處理器。在當時這款晶片非常流行,不僅應用於 FC,還被廣泛應用於雅達利 2600 & 800、Apple I & II、Commodore 64、VIC-20、BBC Micro等機器上。事實上,直到今天6502的修訂版(65C02)還依然在生產。
6502 的寄存器相對較少,只有寄存器 A、 X 和 Y ,而且它們都是專用寄存器。儘管如此,其指令卻有多種定址模式。這其中包括一種稱為“零頁”(Zero Page)的定址模式,使開發人員可以訪問記憶體中最初的256個字($0000~ $00FF)。6502 的作業碼佔用的程式記憶體較少,執行時花費的 CPU 週期也較短。這樣理解, 開發人員可以把零頁上的 256 個儲存單元看作是 256 個寄存器。
6502 中沒有乘法和除法指令,當然也沒有浮點數運算指令。雖然有 BCD 碼模式,但是在 FC 版的6502中,可能是由於專利問題該模式被禁用了。
譯註:Binary-Coded Decimal,簡稱BCD,中國大陸稱BCD碼或二-十進位編碼,是一種十進位的數字編碼形式。在這種編碼下,每個十進位數字用一串單獨的二進位位元來儲存表示。通常 4 個位元表示 1 個十進位數。
6502 還具有一塊不帶溢出檢測的 256 位元組的棧空間。
6502 擁有 151 條指令(理論上有 256 條指令)。剩餘的 105 條都是非法或沒有文檔的指令,多數會使導致處理器崩潰。但是其中也有一些可能會碰巧產生某種作用,於是大部分這樣的指令也會有與其作用相應的名稱。
6502 至少有一個已知的硬體上的缺陷,例如間接跳轉指令的缺陷在於,當 JMP 指令的運算元為形如 $xxFF 的地址時就無法正常工作。因為當從這樣的地址讀出 2 位元組的資料時,該指令無法將低位元組 FF 加 1 後(FF -> 00)產生的進位加到高位元組上。例如,當從 $10FF 讀出2位元組的資料時,讀取的其實是 $10FF 和 $1000 中的資料,而不是 $10FF 和 $1100 中的資料。
記憶體映射
6502 擁有 16 位地址空間,定址能力為 64 KB。但是 FC 實際只有 2 KB的 RAM(Internal RAM),對應的位址範圍是 $0000~$0799。而剩餘的地址空間則用於訪問 PPU、 APU、遊戲卡以及輸入裝置等。
6502 上有些地址匯流排的引腳並沒有布線,所以有很大的一塊記憶體空間實際上都映射到了之前的空間。例如 RAM 中的 $1000~$17FF 就映射到了 $0000~$07FF,這意味著向 $1000 寫資料等價於向 $0000 寫資料。
圖2 “IT’S DANGEROUS TO GO ALONE! TAKE THIS.”(《塞爾達傳說》中的遊戲對白)
PPU(圖形處理器)
PPU 為 FC 產生視頻輸出。與 CPU 不同,PPU 晶片是為 FC 定製的,其運行頻率是 CPU 的 3 倍。渲染時 PPU 在每個周期輸出1個像素。
PPU 能夠渲染遊戲中的背景層和最多 64 個子畫面(Sprite)。子畫面可以由 8 x 8 或 8 x 16 像素構成。而背景則既可以延水平(X軸)方向捲動,又可以延豎直(Y軸)方向捲動。並且 PPU 還支援一種稱為微調(Fine)的捲動模式,即每次只捲動 1 像素。這種捲動模式在當年可是非常了不起的技術。
背景和子畫面都是由 8 x 8 像素的圖形塊(Tile)構成的,而圖形塊是定義在遊戲卡 ROM 中的 Pattern Table 裡的。Pattern Table 中的圖形塊僅指定了其所用顏色中的最後 2 位元,剩餘的 2 位元來自 Attribute Table。Nametable 則指定了圖形塊在背景上的位置。總之,這一切看起來都要比今天的標準複雜得多,所以我不得不和合作者解釋說“這不是簡單的位元影像”。
背景的解析度為 32 x 30 = 960 像素,由 8 x 8 像素的圖形塊構成。背景捲動的實現方法是再額外渲染多幅 32 x 30 像素的背景,且每幅背景都加上一個位移量。如果同時沿 X 軸和 Y 軸捲動背景,那麼最多可以有 4 幅背景處於可見狀態。但是 FC 只支援 2 幅背景,因此遊戲中經常使用不同的鏡像模式(Mirroring Mode)來實現水平鏡像或豎直鏡像。
PPU 包含 256 位元組的 OAM(Object Attribute Memory)用於儲存全部 64 個子畫面的屬性。屬性包括子畫面的 X 和 Y 座標、對應的圖形塊編號以及一組標誌位。在這組標誌位中,有 2 位元用於指定子畫面的顏色,還有用於指定子畫面是顯示在背景層之前還是之後,是否允許沿水平和/或豎直方向翻轉子畫面的標誌位。FC 支援 DMA 複製,可以快速地將 256 位元組從 CPU 可定址的某段記憶體(譯註:通常是 $0200 – $02FF)填充到整個 OAM。像這樣直接存取比手工逐位元組拷貝大約快 3 倍左右。
雖然 PPU 支援 64 個卡通圖形,但是在一條掃描線(Scan Line)上只能顯示 8 個子畫面。當一條掃描線上有過多的子畫面時,PPU 的溢出(Overflow)標誌位將被置位,程式可以依此做出相應的處理。這也就是當畫面中有很多的子畫面時,這些子畫面會發生閃爍的原因。另外,由於一個硬體上的缺陷,會導致溢出標誌位有時不能正常工作。
很多遊戲會使用一種叫做 mid-frame 的技術,使 PPU 可以在螢幕的一部分做一件事而在另一部分做另一件事。這項技術經常用於分屏滾動畫面或重新整理分數條。這需要精確的時間掐算以及對每條指令所需 CPU 週期的詳細瞭解。實作類別似這樣的功能將會加大編寫模擬器的難度。
PPU 具有一個原始形態的碰撞檢測機制。如果第 1 個(編號為0的)子畫面和背景相交,那麼一個標誌位將會被置位,表示“子畫面0 發生了碰撞”。這種碰撞在每一幀只會發生一次。
FC 具有一個內建的 54 色調色盤,遊戲只能使用這裡面的顏色。這些顏色不是 RGB 顏色,基本上只會向電視輸出特定的色度(Chroma)和亮度(Luminance)訊號。
圖3 FC的調色盤。
APU(音頻處理器)
APU 支援 5 個聲道,包括 2 個方波聲道,1 個三角波聲道,1 個雜訊聲道和 1 個增量調製聲道(DMC)。
遊戲程式需要向指定的寄存器(已映射到記憶體)寫入資料以驅動這些聲道發出聲音。
方波聲道支援對頻率和時值的控制,以及頻率掃描(Frequency Sweep)和音量包絡(Volume Envelope)。
雜訊聲道可以利用線性反饋移位(Linear Feedback Shift)寄存器產生偽隨機的雜訊。
增量調製聲道(DMC)可以播放記憶體中的聲音樣本。例如在《超級馬里奧3》中金屬鼓的敲擊聲以及《忍者神龜3》中的語音“cowabunga”使用的都是DMC。
圖4 打氣球遊戲
記憶體映射器
預留給遊戲卡的地址空間是有限的,遊戲卡的程式記憶體(Program Memory)被限制在 32 KB,角色記憶體(Character Memory)被限制在 8 KB。為了突破這種限制,人們發明了記憶體映射器(Mapper)。
記憶體映射器是遊戲卡中的一個硬體,具有儲存體空間切換(Bank Switching)的功能,以將新的程式或角色記憶體引入到可定址的記憶體空間。程式可以通過向指向記憶體映射器的特定的地址寫入資料來控制儲存體空間的切換。
不同的遊戲卡實現了不同的儲存體空間切換方案,所以會有十幾種不同的記憶體映射器。既然模擬器要類比 FC 的硬體,也就必須能夠類比遊戲卡的 記憶體映射器。儘管如此,實際上 90% 的 FC 遊戲使用的都是六種最常見的記憶體映射器中的一種。
ROM檔案
一個副檔名為 .nes 的 ROM 檔案包含遊戲卡中的一個或多個程式記憶體 Bank 和角色記憶體 Bank。除此之外還有一個簡單的頭部用於說明遊戲中使用了哪種 Mapper 和視頻鏡像模式,以及是否存在帶蓄電池後備電源的 RAM。
結尾
學習 FC 很有意思,當時的人們能夠用如此有限的硬體完成這樣一款遊戲機給我留下了深刻的印象。接下來我都想開始編寫一個 8 位元風格的遊戲了。
我用 Go 語言編寫了我的模擬器,用 OpenGL 和 GLFW 處理視頻,PortAudio 處理音頻。模擬器的代碼都放到了 GitHub 上,歡迎諸位下載:https://github.com/fogleman/nes
圖5 我的最愛:《超級馬里奧3》
瞭解更多
- NES Documentation (PDF)
- NES Reference Guide (Wiki)
- 6502 CPU Reference
關於作者: JackalHu
熱愛編程,關注設計模式,致力於提升軟體開發的品質。新浪微博:@Jackal-Hu