我向你介紹:GB Interceptor。它是一個適配器,連接在未修改的Game Boy和盒式磁帶之間,并通過USB提供游戲視頻流。
B站鏈接:https://www.bilibili.com/video/BV1h8411K7c4/
YouTube鏈接:https://www.youtube.com/watch?v=6mOJtrFnawk
上面的視頻應該能讓你很好地了解它的功能、工作原理以及它的局限性。本文將詳細介紹其工作原理的技術細節。如果您對如何訂購和構建自己的GB Interceptor感興趣,請查看github(https://github.com/Staacks/gbinterceptor)和訪問訂購和構建視頻(https://www.youtube.com/watch?v=Lg92tVkEE98)。
我們為什么需要這個?
解釋我為什么開發和建造GB Interceptor的最好方法是解釋我試圖用它解決的問題。幾個月前,一位俄羅斯方塊愛好者就這個問題與我取得了聯系:一場在線俄羅斯方塊錦標賽,參賽者在比賽中展示了他們的游戲。
今天,Game Boy的視頻流并沒有什么異常。模擬器可以很容易地做到這一點,而現代Game Boy變體,如Analogue Pocket,提供可以捕獲的HDMI輸出。還有一些MOD可以將HDMI添加到Game Boy的原始硬件中,因此從Game Boys獲取視頻流是一個長期以來需要被解決的挑戰。
在俄羅斯方塊比賽中做這件事的一個不同尋常的細節是,玩家必須依靠他們在個人Game Boys上訓練的肌肉記憶。將他們換成不熟悉的現代設備或模擬器將嚴重阻礙他們的競爭能力。此外,你可以想象,一場比賽要求每個參賽者先把他們心愛的Game Boys通過mod做視頻流支持改造,這樣的比賽不會受到歡迎。
因此,我們需要一種方法,在不修改正在播放的游戲的情況下,從未經修改的Game Boys獲取視頻。理想的形式是任何人都可以使用,而無需復雜的軟件或額外的硬件,如HDMI抓取器。
工作原理的基本概念
最后,在Game Boy上沒有mod的情況下,唯一可以訪問帶有游戲數據的連接器是cartridge插槽。畢竟,整個游戲數據都要經過那里。因此,我們的想法是創建一個適配器,將cartridge直接連接到Game Boy,并且只添加攔截傳輸數據副本的功能。
GB Interceptor連接到筆記本電腦,該筆記本電腦以VLC顯示其視頻流。
然而,這意味著我們無法隨機訪問感興趣的數據,也無法在RAM中看到Game Boy的CPU從cartridge的原始指令中合成的數據。特別是,我們看不到視頻RAM,但這非常不錯,因為它將包含在屏幕上繪制圖像所需的所有內容。我們需要創建自己的VRAM副本。
為此,我必須編寫一個仿真器,從cartridge總線向其提供數據。為此,我使用了rp2040(樹莓派Pi Pico的微控制器),并將其內核劃分為Game Boy的兩個主要處理部分。一個內核模擬CPU以重新創建VRAM副本,另一個內核則模擬Game Boy的圖形單元PPU4。
CPU仿真實際上是這里最棘手的部分,因為它必須跟上以大約1MHz的速率推出事件的內存總線。如果PPU仿真落后,它會導致像flicker一樣的短暫glitch,但如果CPU仿真落后,最終會錯過內存總線上的事件。不僅RAM的模擬副本可能永遠不同步,而且仿真器甚至無法解釋后續指令。總線上的事件并不總是下一條指令,因為Game Boy的CPU可能需要幾個周期來執行某些指令,而其他指令則在一個周期內完成。因此,仿真器必須跟蹤在某個特定指令之后,在某個事件再次被視為指令之前,需要忽略多少個周期。如果我們只錯過了其中的一個,就幾乎不可能再次得到正確的答案。
這加上在32位CPU上模擬8位CPU的開銷,使rp2040有必要從其默認的125 MHz超頻到225 MHz。rp2040通常可以輕松處理這個問題,但我還是很想看看是否可以提高我的代碼的效率。
由于PPU仿真并不是那么關鍵,實際上在Game Boy的vblank期間,當沒有繪制圖像時,它會定期獲得一些空閑時間,因此它還可以處理USB通信。
硬件
實現這一功能的實際硬件是一個樹莓派Pico,帶有一些總線收發器,將其GPIO端口連接到cartridge總線。從該總線的32個引腳中,兩個用于+5V和接地,一個用于模擬音頻,一個用來控制Game Boy的復位狀態。其他28個引腳連接到rp2040,因此rp2040可以訪問16個地址引腳、8個數據引腳和4個總線控制引腳時鐘、讀取、寫入和芯片選擇。由于這些都使用5V邏輯,我使用的WiFi Game Boy cartridge(https://there.oughta.be/a/wifi-game-boy-cartridge)已經支持將信號轉換為rp2040的3.3V。
還有兩個GPIO未使用。一個觀察用于+5V線上的電壓,以檢查Game Boy是否打開,另一個控制狀態LED并讀取模式按鈕。
GB Interceptor的PCB。
其余的cartridge基于樹莓派rp2040的最小硬件設計示例(https://datasheets.raspberrypi.com/rp2040/hardware-design-with-rp2040.pdf)。這包括一個振蕩器、閃存、一個電壓轉換器和一個USB端口,我用Type C替換了它。
一個樹莓派 Pico連接到Game Boy cartridge的內存總線的轉換器就差不多了。原理圖和PCB設計可以在項目的github存儲庫(https://github.com/Staacks/gbinterceptor/tree/main/pcb)中找到。
實現
真正讓GB Interceptor工作的是它的軟件,當然也可以在github(https://github.com/Staacks/gbinterceptor/tree/main/firmware)上找到。在下文中,我將介紹下它的一些細節。
USB video class
GB Interceptor使用TinyUSB的USB video class實現流式傳輸生成的圖像,因此理論上不需要驅動程序,它應該只顯示為網絡攝像頭。從理論上講。不幸的是,這只能在Linux上按預期工作,我可以在VLC、OBS、Zoom或ffmpeg中直接使用GB Interceptor。在Windows和Android上,許多應用程序似乎在視頻流的格式上有問題。例如,在Windows上,VLC(盡管在Linux上工作)提示沒有找到合適的格式,而OBS在不需要任何設置或驅動程序的情況下工作得很好。在Windows上,這是一個好消息,因為您可以使用OBS作為虛擬網絡攝像頭,將GB Interceptor流轉發給任何對格式挑剔的軟件。在github上可以找到一個測試過的主機軟件列表(https://github.com/Staacks/gbinterceptor/wiki/Host-software-compatibility)。
不幸的是,在撰寫本文時,我無法在MacOS上獲得任何視頻,我還不知道為什么。由于某些原因,它甚至不會觸發TinyUSB來啟用視頻流,因此我并不完全相信它是這種格式。記住,我還沒有在MacOS上做過很多測試,TinyUSB中的 video class實現是最近才開始的,也是實驗性的,我希望將來能解決這個問題。即使我不能讓 video class在這里工作,也應該可以通過USB總線上的UART傳送圖像,并使用簡單的Python腳本將其轉換為系統上的視頻流。您可以在github上查看此Issue的當前狀態(https://github.com/Staacks/gbinterceptor/issues/1)。
那么,這種不尋常的格式是什么?很明顯,這是從Game Boy 160x144像素的分辨率開始的,我可以想象,這可能會讓一些期待現代1080p流的軟件感到驚訝。但是,當我們研究rp2040的全速USB端口及其對TinyUSB實現的同步傳輸的影響所產生的限制時,它會變得更加復雜。這種組合意味著此端點的最大緩沖區大小為1023字節,由于同步傳輸每1ms發生一次,因此每秒可獲得1023000字節。
如果我們只看Game Boy的原始圖像,這就綽綽有余了。Game Boy的“顏色深度”為2位,因此一個圖像幀為5760字節。大約每秒60幀,我們只需要345600字節,這就是為什么如果其他所有的都失敗了,我認為自定義UART協議是MacOS上一個有趣的替代方案。
然而,我們不希望需要驅動程序或其他軟件。我們想要的是一種能正常工作的格式,但遺憾的是,目前還沒有被廣泛接受的2位彩色格式。相反,有很多壓縮格式,我們沒有足夠的計算能力,還有一些被認為是廣泛支持的未壓縮彩色格式,其中大多數使用16位像素。相反,我們使用了一種據稱也被廣泛支持的稍微更高效的格式:每像素12位的NV12。12位由亮度(灰度亮度)的每像素8位和顏色信息的四個像素共享的16位(因此每像素多4位)組成。
好消息是,整個幀的顏色數據存儲在最后,因此我們可以將其設置為灰色或綠色,并可以忽略它。事實上,我們可以將之前的數據視為具有8位灰度數據的簡單160x144像素緩沖區,這對于我們的目的來說或多或少是理想的。
當然,壞消息是,它所占用的數據仍然是原始2位圖像所需數據的6倍。我們的每秒1023000字節現在限制在29fps。
因此,總的來說,我們有一個分辨率為160x144的29fps NV12流。并非所有這些視頻會議工具都支持。
順便說一句,雖然GB Interceptor因此只推出了29fps,但它仍然以60fps的速度在內部工作,并混合這些幀以模擬舊LCD的延遲。它只是在USB總線調用時推出最新的混合幀。
可編程IO
現在,在我解釋了如何從GB Interceptor中獲取結果之后,讓我們來談談另一端:如何將cartridge總線上的通信連接到rp2040。
當我試著在我的WiFi Game Boy Cartridge用ESP8266監聽一個事件時,非常痛苦。中斷太慢,而讓CPU 輪詢的方式觀察時鐘線也不可行。rp2040有一個訣竅:可編程IO。這些是簡單的狀態機,可以直接訪問GPIO引腳以及CPU的FIFO緩沖區。
我們需要做的就是等待時鐘線變低,然后同時讀取連接到Game Boy內存總線的剩余27個GPIO引腳,并將結果寫入FIFO。為此,我們只需要一個PIO,它只執行四條指令:
wait1pin28;WaitforCLKtogohigh
wait 0 pin 28 ;Wait for falling flank of CLK
mov isr pins ;Read all GPIO pins to the input shift register
push;PushtheISRtotheFIFO
這樣,CPU很方便就可以從FIFO中提取這些事件中的一個,并將其打包成一個32位整數。
仿真器部分
現在我們看看如何處理這些事件。我希望你對Game Boy的工作原理有一個基本的了解。對于那些不熟悉Game Boy開發的人,我總是推薦Michael Steil的《Ultimate Game Boy Talk》(https://www.youtube.com/watch?v=HyzD8pNlpwI)。
如上所述,基本思想是rp2040的一個內核解釋傳入的總線事件,使其遵循與Game Boy的CPU相同的指令。也就是說,它模擬Game Boy CPU,以便重新創建VRAM(和OAM)的精確副本。然后,第二個內核充當PPU,并從VRAM副本中渲染圖像。這主要是一個基本Game Boy模擬器的實現,但我想談談(或寫一下)一些不同之處。
條件跳轉和IO
首先,在這個場景中,有幾個事情變得簡單得多。想想程序計數器和條件跳轉。我們不必執行這些。真正的Game Boy無論如何都會獲取下一條指令。無論它是遞增PC的下一條指令,還是跳到完全不同的地址,都無關緊要。真正的Game Boy將獲取下一條指令,我們不必擔心指令來自何處。
這解決了一個看似最大的問題:我們看不到任何硬件I/O寄存器。特別是,我們看不到來自游戲板的輸入!如果我們看不到玩家的輸入,我們應該如何模擬游戲?好吧,幾乎所有存在的代碼都會比較游戲板輸入以檢查按下了哪個按鈕,并對按鈕觸發的代碼進行條件跳轉。我們的模擬器將簡單地遵循這些相同的指令,而不必關心它是否由按下按鈕觸發。
你可以說GB Interceptor是一個模擬器。
只有當來自I/O寄存器的數據最終到達VRAM時,這才成為問題。想象一下,將gamepad的值添加到一個基地址,以計算顯示D-Pad當前狀態的圖像的tile
索引。CPU將獲得獲取游戲板寄存器值的指令,并向其添加一個數字,我們的仿真器將不知道該操作的正確結果。然后將此結果寫入VRAM,我們不知道該位置中的內容。
然而,這些應僅適用于較小的視覺差異。我不知道有哪個例子是用gamepad I/O完成的,但我有一個DIV寄存器的例子。在俄羅斯方塊中,它被用作隨機數的來源,大多數時候它通過條件跳轉來分支代碼,以選擇下一個不同的塊,或者在游戲模式B中生成初始的垃圾塊堆。這也決定了模式B中垃圾堆的一個塊是空的還是滿的,所以我們也得到了相同的垃圾堆布局。但這些垃圾塊也有一種隨機的視覺樣式,它不是基于分支代碼,而是添加到基本tile索引中的隨機數。
結果是,我們在GB Interceptor上看到了相同的垃圾堆棧布局,但各個塊的外觀不同。這是無害的,只有當你將圖像與Game Boys屏幕進行比較時,你才會注意到。
左:原始Game Boy屏幕在俄羅斯方塊模式B下的圖像。右:與GB Interceptor渲染的場景相同。方塊的布局是相同的,但各個塊有不同的設計。
只有當整個準備好的數據流從一個I/O寄存器寫入VRAM時,我們才會遇到真正的麻煩。我所知道的(而且我能想到的)唯一的例子是連接電纜。在這里,我們可以看到B模式方塊的相同示例,但在俄羅斯方塊的雙人模式中。問題是,兩個玩家應該擁有相同的方塊樣式。因此,首先啟動游戲的Game Boy將生成方塊并通過鏈接電纜將其發送給第二個堆棧。第二個將數據直接寫入VRAM,沒有任何檢查或條件跳轉,我們看不到任何內容。
俄羅斯方塊為雙人模式。左:首先啟動帶有GB Interceptor的Game Boy Color,方塊呈現為1人模式。右:另一個Game Boy首先啟動,我們無法看到方塊,因為它是通過鏈接電纜接收的。
因此,在雙人俄羅斯方塊中,如果是在Game Boy中首先開始游戲,GB Interceptor可以正常工作(除了各個區塊的不同視覺風格),但如果是在第二個Game Boy中,它會產生無法使用的輸出。
時鐘、DIV寄存器和暫停指令
說到DIV寄存器,這實際上是一個我們可以模擬的I/O寄存器。由于我們從Game Boy獲得了準確的時鐘,我們可以計算模擬寄存器與真實寄存器同步,而不會有任何偏離的危險。只有兩個問題:
1.初始值是未知的-至少對我來說是未知的。當執行盒帶中的代碼時,DIV寄存器的狀態取決于Game Boy模型,在某些情況下,它還取決于該模型引導序列期間的用戶交互。例如,如果在引導序列期間更改Game Boy color的顏色模式,DIV寄存器將在開始時具有不同的值。我不確定Interceptor在引導序列期間是否在總線上看到足夠的動作來彌補這一點,但我也不完全排除這種情況。
2.當Game Boy進入暫停狀態時,我們失去了參考時鐘,對于大多數游戲,每幀至少發生一次。在這里,rp2040的時鐘必須精確接管,如果我們有更多的計算空間,這應該是可能的。(比如,如果有人可以優化我的代碼)
問題是,我們實際上測量了在實際游戲開始之前的引導序列中,每個Game Boy時鐘周期出現了多少rp2040時鐘周期。在這里,我們可以觀察數千個周期,并且應該能夠從rp2040中獲得非常精確的替代時鐘。不幸的是,出于性能原因,我只使用兩個時鐘的整數比,通常為每個Game Boy時鐘對應225 rp2040時鐘。這意味著在暫停狀態期間,舍入誤差將導致每100個周期大約一個周期的誤差。
所以,也許我們可以進行分數時鐘計數,但目前,因為它只影響div寄存器,無論如何我都無法正確初始化,所以這沒有實現。
同步CPU和PPU
當我們正在將模擬器與真實Game Boy同步時……我們當然也需要將PPU與真實GameBoy同步。否則,任何需要更改VRAM中間幀的效果都會導致故障,至少在VRAM中隨機更新數據時,我們會看到一些撕裂效果。
問題是,在內存總線上找不到PPU的蹤跡。我們必須通過游戲的行為來推斷PPU的狀態,這也必須與PPU同步——至少要知道它何時可以寫入VRAM。這里的大問題是,游戲可以使用許多不同的方式來做到這一點。
最常見的方法是vsync中斷。大多數游戲只是讓Game Boy在達到vsync時觸發一個中斷,我們可以看到這個中斷的代碼何時被執行,因此我們可以簡單地調整我們自己的仿真PPU的定時,以便在同一時刻進入vsync。
不幸的是,有很多其他選擇可以做到這一點。對于需要擠出更多VRAM訪問權限的游戲(例如在Donkey Kong Land中實現),另一個常見的方法是以緊密循環的方式讀取LY寄存器,并定期將其與特定的行號進行比較。條件跳轉返回到LY讀取,直到到達正確的行,并且代碼僅超出條件跳轉。幸運的是,開發人員可以在未到達時通過jump指令來節省幾個周期,所以許多游戲都是這樣做的,這允許在Interceptor中簡單檢測這些緊密的循環。
然而,會有不同方法的游戲(比如我的Wifi cartridge),在檢測到這些其他方法之前,GB Interceptor的輸出會出現故障。
檢測中斷
哦,雖然中斷是同步PPU的福音,但它們最初并不容易檢測。我們需要跟蹤每一條指令,以及Game Boy為每條指令需要多少周期,以確定內存總線上的哪個事件將是下一條指令。Game Boy在執行過程中跳到另一個點,并花費幾個額外的周期來執行,這在這里并沒有什么幫助。
看看《The Legend of Zelda - Link’s Awakening》在原版Game Boy上的第一次vsync中斷:
AddressDataInstruction
01a2 fb EI
01a3 c3 JP a16
01a4 bd
01a5 03
81a5 71
03bd 3e IRQ
82bd 01
82bd 01
dffe 81
dffd a5
0040 c3 JP a16
0041 25
0042 05
804224
當忽略中斷時,我們會錯誤地將第7行中的0x3e解釋為操作碼。要或多或少地確定我們正在看到中斷,唯一的方法是實現GB Interceptor,使其在當前事件被誤解為指令之前讀取幾個周期以識別中斷,而實際上它只是內存總線上的垃圾,而CPU需要一點時間進入中斷。
幸運的是,Game Boy在中斷時跳轉到幾個固定地址,所以我們要注意這些地址。但由于這些地址理論上也可以從常規代碼中調用,因此我們混合了更多的指示符,特別是堆棧指針的行為。在中斷調用期間,當前PC被推到堆棧上,因此SP寄存器被遞減兩次,Game Boy寫入兩個遞減的地址。通常這些地址并不指向屬于盒帶的地址,但這些地址在內存總線上仍然可見,因此這增加了我們檢測中斷的信心。
唯一的問題是,Game Boy實際上不需要一直這樣做,也不需要在內存總線上顯示SP地址,因為沒有任何游戲cartridge關心這些操作。因此,我們可以在這里看到不同設備之間的一些差異并不奇怪。以下僅為原始GameBoy(DMG)、GameBoy Color和Analogue Pocket的中斷調用:
DMG GBC Pocket
Address Data Address Data Address Data
03bd 3e 03bd 3e 03bd 3e
82bd 01 83be 00 dfff 00
82bd 01 dfff 00 dffe 01
dffe 81 dffe 80 dffd 9b
dffd a5 dffd 00 0040 c3
0040 c3 0040 c3 0040 c3 < Next instruction
如果我們仔細觀察,我們會發現一些細微的差異:DMG在遞減SP地址之前也會顯示SP地址,GBC只顯示它實際寫入的兩個遞減地址,Pocket則會提前一個周期執行此操作。考慮到所有這些情況,當然會降低中斷檢測的可靠性,并且目前它無法與Pocket的變體一起正常工作。
意圖和構建說明
我認為這些是實現過程中最有趣的部分。如果你已經讀到了這里,那你是一個真正的8bit極客!
如果你想了解更多細節,你現在必須深入github上的代碼(https://github.com/Staacks/gbinterceptor),在那里你還可以找到硬件設計文件和案例材料。我希望在代碼和硬件設計方面都會有一些社區貢獻,所以如果這篇文章發表后幾個月過去了,這也將主要發生在github上。
如果你想建立自己的GB Interceptor,你也應該觀看訂購和構建視頻。
B站鏈接:https://www.bilibili.com/video/BV17G4y1y7Yg/
Youtube鏈接:https://youtu.be/Lg92tVkEE98
我希望你喜歡這個項目!
致謝
如果沒有許多人在我之前研究、測試和推動Game Boy,并且(最重要的是,也是我自己寫這些文章的原因)記錄了他們的工作,這個項目就不可能存在。以下是我最重要的一些資源:
1,gbdev.io(https://gbdev.io/),尤其是它的Pan Docs是我了解Game Boy工作原理的主要來源。
2,在Joonas Javanainen的網頁上(https://gekkio.fi/)可以找到許多硬件細節和一些復雜的細節。
3,雖然有很多網站都有Game Boy的opcode表,但我發現Megan Sullivan的網站是最方便的(https://meganesulli.com/blog/game-boy-opcodes/),我一直都在訪問它。
審核編輯 :李倩
-
GB
+關注
關注
0文章
102瀏覽量
45336 -
適配器
+關注
關注
8文章
1932瀏覽量
67914 -
模擬器
+關注
關注
2文章
867瀏覽量
43165
原文標題:GB Interceptor:值得擁有的Game Boy 捕捉器
文章出處:【微信號:Arm軟件開發者,微信公眾號:Arm軟件開發者】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論