---
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/)