我用 Go 語言做了一個紅白機模擬器


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 <addr>
指令的操作數為形如 $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》