--- tags: System Software --- # Game Boy 的硬體設計與運作原理 為設計一個 Game Boy Emulator,我們將探討 Game Boy 中的硬體元件,以及相關的運作機制。 :::danger 有點流水帳的條列內容,敬請見諒... ::: ## Register 每個 register 為 8-bit 結構: * `A` 為 accumulator register 且大多的 instructions 只能藉由 `A` 來運作 * 某些 instructions 會透過一對 register 來操作 16 bit operand,可行的組合有 `BC`, `DE`, `HL` * flags register `F` 紀錄運算的狀態,bit 0 - 3 恆為 0 * Bit 7: 結果為 0 * Bit 6: instruction 為減法 * Bit 5: bit 從第 3 位進位到第 4 位時,例如 `0x0F + 0x01 = 0x10` 會設置 `H` * Bit 4: bit 從第 7 位溢出時,例如 `0xF0 + 0x11 = 0x01` 會設置 `C` * 16-bit stack pointer `SP` * program counter `PC` ## Clock clock 約為 4.19MHZ,1 個 machine cycles 約為 4 個 clock cycles(~1.05MHz) ### Divider and Timer Registers * Gameboy 中會維護一個在每個 clock 中增加的 counter,counter 可以分成 divider 和 timer 兩種 register 揭露 * divider 每 256 個 clock cycles 會增加,而 timer 則根據 config 增加,且當 overflow 時產生 interrupt * divider 和 timer 底下都是一個相同的 16-bit cycle counter,這個 counter 的前 8 個 bit 直接作為 divider register 在 `0xFF04` 揭露,而對 divider 做寫入操作則會重置該 16-bit cycle counter * timer counter 根據 timer control register 設定的 rate 遞增,當 overflow 發生時,會在下一個 delay cycle 設置 interrupt,並且從 timer modulo register 重新載入內容 * 在 delay cycle 嘗試寫入 timer counter 的操作會取消中斷和從 timer modulo register 的重載;而在 timer modulo register 被載入的 cycle 重新寫入 timer modulo register 將不會同時也對 timer counter 做更新 ## Interrupt * `VBlank`、`LCDC`、`TIMA`、`Serial`、`Joypad` 5 種 interrupt 對應 interrupt control registers 中的 bit,可以通過寫入 interrupt flag register (`0xFF0F`)被觸發(基本是由硬體寫入,但由軟體設置也並無不可) * 設置 interrupt enable IO register (`0xFFFF`)則可以關閉 interrupt * 當某個 interrupt 被 disable,對應的 ISR 將不會被執行到,則對應的 interrupt flag register 中的 bit 會持續被設置(除非被手動清除) * CPU 在進入 ISR 時會關閉 interrupt,待處理完後再通過 `reti` 指令重新打開。 * Interrupt 會被依序處理(從 `VBlank` 到 `Joypad`),ISR table 的位置則為從 `0x40` 開始的每 8 個 byte,當 interrupt 被處理完後 interrupt flag register 中對應的 bit 會被重置 ## CPU Instruction * GameBoy 使用的 CPU 型號為 8-bit SHARP LR35902,Intel 8080 可以與 Z80 相容,而 SHARP LR35902 在摻雜 Intel 8080 和 Z80 的部份 feature 的基礎上,又加入某些不同的新 feature ![](https://i.imgur.com/DihWDPO.png =600x) > [The Nintendo® Game Boy™, Part 1: The Intel 8080 and the Zilog Z80.](https://realboyemulator.wordpress.com/2013/01/01/the-nintendo-game-boy-1/) * Instruction 對於高位地址(`0xFF00` - `0xFFFF` 為基底的位址)的存取做優化,可以 encode 成更少的 byte 以減少指令運行的 cycle :::warning 暫不探討 CPU 的 instruction 細節,詳閱 [Cpu Emulation](https://thomas.spurden.name/gameboy/#cpu-emulation) ::: ## Core initialisation ### ROM Loading 卡匣的 ROM 在 `0x104` - `0x14F` 會包含一個 header,其中: * `0x104` - `0x133` 是任天堂的 logo,由 boot ROM 進行檢查以確認版權 * `0x134` - `0x143` 為 ROM 的名稱,比較新的 ROM 會用後 4 個 char 標記製造商的編碼 * `0x147` 記錄卡匣的類型,這使得對應的 memory bank controller 種類需要被 GameBoy 對應 * `0x148` 為卡匣 ROM 的大小 * `0x149` 為卡匣 RAM 的大小(如果存在) * `0x14D` 為 1 個 byte 對 header 的 checksum * `0x14E` - `0x14F` 為對整個 ROM 的 checksun ### Resetting * 進行 reset 時,首先檢查 boot ROM 是否 enabled,如果 enable,則把 PC 指向 `0x0`,則執行 boot ROM 中的指令可以完成啟動需要的初始化,以及對卡匣的 header 檢查 * 如果 boot ROM 不 enable,則將 PC 直接設置到卡匣程式的起點 `0x100`,並且重置其他的 registers、memory、state。 ## Boot ROM > * [Gameboy Bootstrap ROM](https://gbdev.gg8.se/wiki/articles/Gameboy_Bootstrap_ROM) > * [A Look At The Game Boy Bootstrap: Let The Fun Begin!](https://realboyemulator.wordpress.com/2013/01/03/a-look-at-the-game-boy-bootstrap-let-the-fun-begin/) * Boot ROM 是在啟動 GameBoy 時所執行的一小段程式,他會負責設定 RAM / 聲音的資源,顯示開機的畫面與播放 "嗶" 聲,並對卡匣的 ROM 做檢查 ![](https://i.imgur.com/Vdc6vfu.png) > 經典的 GameBoy 開機畫面: [RE: "Game Boy classic boot up "](https://youtu.be/fZy08NsG2FM) * 如果 boot ROM enabled,memory 最前的 256 個 bytes 會被 map 到內部的 ROM。並且此時 PC 應該指向 0x0 因此由 boot ROM 開始執行起。 * 操作 `0xFF50` 可以 disable boot ROM,這將直接允許卡匣的前 256 個 bytes 被直接讀取。此外,一旦關閉 boot ROM 則不能再次重新開啟。 ## Memory subsystem Gamgboy 的 memory map 為: ![](https://i.imgur.com/Qr7b10K.png =500x) > [Memory and Memory-Mapped I/O of the Gameboy — Part 3 of a Series](https://medium.com/@raphaelstaebler/memory-and-memory-mapped-i-o-of-the-gameboy-part-3-of-a-series-37025b40d89b) * `0x0000` - `0x0100` 也作為 Boot ROM 使用 (when enabled) ![](https://i.imgur.com/VDjfztF.jpg) * 物理上其實具有兩個 8K 的 SRAM: video RAM 和 work RAM,兩者連接到不同的 data bus(後者的 data bus 與卡匣共享),因此當 video processor 存取 video RAM 時 CPU 可以同時平行存取卡匣或者 work RAM ## Memory Banking > [Memory Bank Controllers](https://gbdev.gg8.se/wiki/articles/Memory_Bank_Controllers) * 許多卡匣大小的 ROM 都會超過上限的 32 KB,而 RAM 也會超出可用的 8 KB,因此卡匣中會帶有 Memory Bank Controller(MBC) * MBC 可分成許多類型,且可透過存取 ROM 的定址空間來操作。 ## OAM DMA (0xFF46) > [OAM DMA tutorial](https://gbdev.gg8.se/wiki/articles/OAM_DMA_tutorial) * Object Attribute Memory(OAM) 是一段用來紀錄 [sprites](https://en.wikipedia.org/wiki/Sprite_(computer_graphics)) 的記憶體區塊,此區塊在 Gameboy 上有 160-byte 長,而每個 sprites 需要 4 個 bytes 對應,因此 OAM 可以一次容納最多 40 個 sprites * 這 4 個 byte 分別紀錄: * Y location * X location * Tile number * Flags * X 和 Y 對應在遊戲裡的座標位置,第 3 個 byte 表示是 tile map 中的何者,Flags 則用來表示 sprites 的屬性(例如顏色、翻轉等) * 因為 OAM 不能在顯示時直接 access(過於耗時),所以我們透過 [DMA](https://en.wikipedia.org/wiki/Direct_memory_access) 的協助以加速對 OAM 的存取 * 透過向 `0xFF46` 寫入欲存取位置(source address)的高 8 個 bit 以啟動 DMA,DMA 會延遲一個 machine cycle 後啟動 * 當 DMA 啟動,僅有位置 `0xFF80` - `0xFFFE` 被允許存取,其他的 access 都會返回 `0xFF` * 當 DMA 已經在啟動狀態時,再次寫入 `0xFF46` 會延遲一個 cycle 改變 source address * DMA 的執行會與 CPU 平行,每個 machine cycle 中轉換一個 byte ## Video > 對照 [The Ultimate Game Boy Talk: pixel processing unit](https://media.ccc.de/v/33c3-8029-the_ultimate_game_boy_talk#t=1745) 的影片可以更好理解這裡的運作 * Gameboy 的畫面會被顯示在 160x144 pixel 的 LCD 螢幕上,用 2 bits 來表示顏色(3 最暗而 0 最亮),根據 VRAM 中的 8x8 pixel tiles 進行繪圖 * 因此每個 tiles 需要 8x8x2 = 128 bits = 16 bytes,在記憶體中 2 bytes 為一個 line,第一個 bytes 為表示顏色的最低位,第二個 bytes 則為表示顏色的最高位(也就是說,1 個 line 可以表示 8 個 pixel) * tiles 被儲存在 `0x8000` 到 `0x9800` 的記憶體範圍中,這是可以存下 384 個 tiles 的空間,但實際上會被分成 `0x8000` 到`0x9000` 和 `0x8800` 到 `0x9800` 兩段重疊空間,各可存下 256 個 tiles,透過 LCD control 可以設定這些空間該如何被 backgroud 或者 sprites tile 對應 * 索引低位址的 tiles 時 index 是 0~255,而索引高位址的 tiles 時則是 -128~127。 * 兩個各可以索引到 32x32 個 tiles 的 map 在 VRAM 中的 `0x9800` 和 `0x9C00` 位置(對應低與高位置的兩段空間) ### Palettes (0xFF47 - 0xFF49) * Gameboy 中具有 3 個 palettes,每個 palettes 透過 1 個 bytes 去表示,其中每兩個 bit 各可對應一種顏色,根據一個 2-bit input,輸出一個 2-bit output * 因此,tiles 的顏色編碼並不直接等於預設的 order!這讓遊戲可以根據實際的需求設計 palettes 的 mapping。例如讓 00/01 mapping 到黑色的 11 ,讓 10/11 mapping 到白色的 00 * `0xFF47` 為背景的 palettes,`0xFF48` 、`0xFF49` 則為物件的 palettes,根據設定的物件 attribute 取用 * sprites 的顏色編碼為 00 時對應的是 "透明",這也是為甚麼物件有兩個 palettes 的原因之一 * 因此一個物件最多只能使用允許的四種顏色中的三種,而不同的物件可以選擇不同的三種 ### LCD Control (0xFF40) 以下表格為各 bit 對應的相關設定: | Bits (LSB=0) | Function | | ------------ |:---------------------------------------------------- | | 0 | Background & Window enable | | 1 | Object enable | | 2 | Object size (0=8x8, 1=8x16) | | 3 | Background tile map select (0=low, 1=high) | | 4 | Background & Window tile bank select (0=high, 1=low) | | 5 | Window enable | | 6 | Window tile map select (0=low, 1=high) | | 7 | LCD Enable | * `VBlank` 中斷時 LCD enable 只能被設為 0 ### Background and Window ![](https://i.imgur.com/owZN9fT.png) ![](https://i.imgur.com/y2zwTPR.png) * 顯示上,畫面是由 20 x 18 個 tiles 組成,但實際上 VRAM 中可表示的畫面是 32 x 32 個 tiles * 可以透過 Scroll X and Scroll Y registers (`0xFF42` & `0xFF43`) 來捲動,決定展示在畫面上的部份是何區塊 * 則在像馬利歐兄弟這樣的遊戲,可以透過 wrap around 的方法不斷把 offscreen 的畫面更新並捲動呈現 ![](https://i.imgur.com/SklmDds.png) * Window X and Window Y registers `FF4A` & `FF4B` 則可以設定在畫面上的固定顯示,Window 不會隨著畫面捲動而移動,因此可以用來顯示遊戲的得分或生命值資訊等 ### Objects (Sprites) * sprites 總是在 low bank (`0x8000`) 中 * Objects/sprites 由一個或者兩個 tiles 所組成(8x8 或 8x16 個 pixels,根據 LCD Control 的 bit 2 設定) * 至多 40 個 sprites 可以被指定在 OAM 中,每個物件各透過 4 個 bytes 表示,分別代表 X 座標、Y 座標、tile index 跟 attributes * X 座標偏移 -8 個 pixels(換句話說,X = 8 會將物件 tile 的第一個 column 對應畫面的第一個 column),Y 座標則偏移 -16 個 pixels,這使得物件可以僅部份顯示在畫面上 * object attribute: | Bits (0=LSB) | Meaning | | ------------ |:------------------------------------------------------------------------------------------------------------------------------ | | 0-3 | Unused (given meaning on Gameboy Colour) | | 4 | Palette selection (0 = Object Palette 0, 1 = Object Palette 1) | | 5 | X Flip - object image is flipped horizontally | | 6 | Y Flip - object image is flipped veritcally | | 7 | Priority - (0 = object always on top of background & window, 1 = object only on top of colour 0 pixels of window & background) | * 注意第 7 個 bit 的 priority,當此 bit 為 1 時,sprites 的優先度會高於白色的背景,但是低於其他顏色的背景 * 如果使用 8x16 的 tile mode,每個 object 會由在低位址的 tile bank 中相鄰兩個 tile 儲存,則 index 的最低 bit 會被忽略,此時第一個(偶數編號)圖塊構成物件的上半部分,第二個(奇數編號)圖塊則構成物件的下半部分 * 對 8x16 pixel object 做 Y Flip 會是對整個 object 做翻轉 * 重疊的物件的優先度為: X 座標小者優先度最高,具有相同 X 坐標的物件以 OAM 位址小者優先度最高: 如圖,當白框在怪物右邊時,白框會畫在怪物底下,在左邊時則會顯示在其上方 ![](https://i.imgur.com/wnK6bUx.png =300x) ![](https://i.imgur.com/xD362L8.png =260x) * 每個 scaneline 中最多只能畫出 10 個物件,以下圖為例,scanline 中第 11 個以後的物件將不會被顯示 ![](https://i.imgur.com/AlFoiik.png) * 在 Y 座標 =0 或大於 160 (160 是因為 144 + 偏移量 16)的物件會直接被忽略,因為它們已經超出了可顯示的範圍 * 對於 X 座標 = 0 或者 >= 168 的物件,雖然不可視,但仍然參與該 scanline 的物件數量計算 ### LCD Y & Y Compare (0xFF44 & 0xFF45) * 通過設置這兩個 register,可以讓畫面僅對某個部份做 scrolling,大致的原理為,設定 LCD Y compare 使得畫面更新到某個特定 scanline 時產生 interrupt,然後改變 scroll X register * 或者也可以用來讓 window 僅顯示部份: 設定 LCD Y compare 使得畫面更新到某個特定 scanline 時產生 interrupt,然後關閉 window 的顯示 ### Rendering ![](https://i.imgur.com/qPb1weK.png) * 整個畫面更新的時間軸大致如圖所示: 每個 scanline 的前半部份會先做 **OAM Search**,搜尋該 scanline 需要顯示的 sprites,接著將對應的 pixel 呈現在畫面上(**Pixel Transfer**),然後進入 **H-Blank** mode,依此順序依次更新所有 scanline * 在更新到畫面最後一個 scanline 並回到第一個 scanline 前,會進入一段 **V-Blank** * 在 Pixel Transfer 階段,CPU 不能 access VRAM * 如果是對於 OAM,在 Pixel Transfer 和 OAM Search 階段,CPU 都不能 access ## Input * Gameboy 中的8個按鈕正好對應一個完整 bytes 的 8 bits,當按鍵被壓下,對應的 bit 會被設置並且觸發 interrupt,當按鍵被放開時則不會觸發 interrupt * 遊戲可以通過 access `0xFF00` 得知按鍵被按壓的情形 * 一次可以對 4 個按鍵進行檢查,`0xFF00` 的 bits 4 & 5 決定檢查哪個 subset * 設置 bit 5 要求檢查 4 個方向鍵,設置 bit 4 則要求檢查另外 4 個按鍵,同時設置兩個 bit 則會檢查 GameBoy 的硬體類型 ## Audio > [Gameboy sound hardware](https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware) 暫略,可以參考 [The Ultimate Game Boy Talk: sound controller](https://media.ccc.de/v/33c3-8029-the_ultimate_game_boy_talk#t=1445) ## Reference * [The Ultimate Game Boy Talk](https://media.ccc.de/v/33c3-8029-the_ultimate_game_boy_talk) * [Gameboy Overview](https://thomas.spurden.name/gameboy/)