--- 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  > [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 做檢查  > 經典的 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 為:  > [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)  * 物理上其實具有兩個 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   * 顯示上,畫面是由 20 x 18 個 tiles 組成,但實際上 VRAM 中可表示的畫面是 32 x 32 個 tiles * 可以透過 Scroll X and Scroll Y registers (`0xFF42` & `0xFF43`) 來捲動,決定展示在畫面上的部份是何區塊 * 則在像馬利歐兄弟這樣的遊戲,可以透過 wrap around 的方法不斷把 offscreen 的畫面更新並捲動呈現  * 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 位址小者優先度最高: 如圖,當白框在怪物右邊時,白框會畫在怪物底下,在左邊時則會顯示在其上方   * 每個 scaneline 中最多只能畫出 10 個物件,以下圖為例,scanline 中第 11 個以後的物件將不會被顯示  * 在 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  * 整個畫面更新的時間軸大致如圖所示: 每個 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/)
×
Sign in
Email
Password
Forgot password
or
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.