「Vintage Computer Festival Midwest 11.0」 (由 Dave Ruske 製作), CC BY 2.0

[CHIP-8 小東西] CHIP-8 規格介紹

CHIP-8 是一種直譯式程式語言,出現於 1970 年代末期至 1980 年代初期,主要應用在 DIY 電腦系統上。本篇將藉由介紹 CHIP-8 背景以及實際規格,為之後的小型專案鋪路。

背景知識

什麼是 CHIP-8

感覺這個名字似乎會引起誤會,CHIP-8 不是 chip,它是一種簡單的直譯式程式語言,也是一套簡單的虛擬機規格,出現於 1970 年代末期至 1980 年代初期,主要應用在 DIY 電腦系統上。那個年代系統規格非常有限:

  • 通常被設計成直接連接電視作為顯示螢幕
  • 系統的 RAM 通常只有 1K 到 4K 的容量
  • 主要的輸入裝置是 16 鍵的十六進位鍵盤
  • CHIP-8 的直譯器本身僅佔用 512 bytes 的記憶體空間,而使用者以十六進位輸入的程式碼所佔用的空間甚至更小

它最早由 Joseph Weisbecker 在 1970 年代設計,用在 COSMAC VIP、Telmac 1800 這類早期 8-bit 微電腦上,目的是讓人在硬體資源很少的情況下,也能比較容易寫小遊戲或互動程式。

可以簡單想成 CHIP-8 規定了:

  • 有多少記憶體
  • 有哪些暫存器
  • 螢幕怎麼顯示
  • 鍵盤怎麼輸入
  • timer 怎麼運作
  • 每個 opcode 代表什麼操作

這個小型的系統架構,總共只有 34 個指令需要實作,非常適合第一次學習模擬器開發的一套系統。很多經典入門 emulator 專案會選它,因為它規格簡單,但又包含完整的電腦概念。

什麼是 Emulator(模擬器)

要了解 emulator 在做什麼,可以把它想像成一位翻譯官,它的核心任務是讀取原本專為目標機器編譯出來的機器碼指令,接著進行解譯,最後在 host machine(在這個案例中就是現代電腦)重現目標機器的所有功能。

舊遊戲的 ROM 檔案裡面裝的其實就是滿滿的原始機器指令。模擬器會讀取這些 ROM 檔案裡的指令,並且透過軟體的方式,去模仿當年那台機器的真實運作行為。

如果是開發更進階的系統,模擬器不僅要模擬 CPU,還要模擬圖形處理器與音效晶片等複雜的硬體元件。那能不能現在不要搞那麼複雜,圖形先處理單色像素,音效也處理一道單一頻率的蜂鳴聲……那就是架構非常單純的 CHIP-8 啦!

所以寫 CHIP-8 emulator,就是用現代程式語言,例如 C 或是 C++ 去實作這套規格,讓 CHIP-8 程式可以在現代電腦上跑。

CHIP-8 = 一套小型程式語言 / 虛擬機規格
CHIP-8 ROM = 用 CHIP-8 寫成的程式檔
CHIP-8 emulator = 負責執行 CHIP-8 程式檔的模擬器

CPU 簡介

在開始動手寫 emulator 之前,必須先對 CPU(中央處理器)的運作方式有一個基本的認識。

CPU 的運作原理其實非常直觀:它會不斷地從記憶體中的某個地方讀取「指令」,然後照著指令告訴它的內容去做事。可以把 CPU 想像成一個執行速度極快、但本身沒有自主思考能力的工人,必須用一組非常明確且邏輯嚴謹的指令集(Instruction Set),一步一步地告訴它該做什麼。

這些指令在電腦底層都是以數字的方式進行編碼,用來代表某種操作與對應的資料。以 CHIP-8 來說,指令通常會以十六進位(Hexadecimal,常帶有 $0x 的前綴)來表示。在處理電腦底層資訊時,十六進位是最常被使用的進位制,因為它能用更簡潔的方式來表示龐大的二進位數值與記憶體位址。

雖然不同 CPU 架構有不同的指令集與硬體設計,但大多數 CPU 執行程式時,都可以簡化成三個主要步驟:

  • 提取指令 Fetch:從記憶體取出下一條指令
  • 解碼指令 Decode:解析這條指令代表什麼操作
  • 執行指令 Execute:執行該指令,並更新暫存器、記憶體或其他系統狀態

CPU 都在不斷重複三個核心動作,而撰寫模擬器的核心任務,就是用軟體程式碼完美重現這個無窮迴圈。CHIP-8 emulator 的核心也會圍繞這個流程實作。每一次執行循環中,emulator 會從 CHIP-8 記憶體取出 opcode,判斷它是哪一種指令,然後執行對應的行為。


從這個專案起頭的原因

要做一件事情之前一定要搞清楚目的,為什麼選擇做 CHIP-8 emulator 主要有兩個原因:

現代電腦看不懂 CHIP-8 程式

CHIP-8 遊戲檔裡面不是 Windows / macOS / Linux 可以直接執行的指令。

例如 CHIP-8 程式可能有一條指令意思是:把 0x05 放進 V1 暫存器,但我現在手邊的機器 CPU 不知道什麼是 V1,也不知道 CHIP-8 的螢幕、鍵盤、timer 等規則,所以需要一個 CHIP-8 emulator 來做翻譯,轉換成現代電腦上的行為,讓 CHIP-8 程式可以在現代電腦上面跑。

Emulator / CPU 基礎練手

這其實才是最最最主要的目的:CHIP-8 很小,所以很適合當入門專案。
從讀取程式放進 memory、fetch、decode、執行指令,甚至於更新畫面、鍵盤、timer 等等,這就是很多 CPU、VM、emulator 的核心流程。所以初學者很適合從 CHIP-8 emulator 著手,主要不是因為實用度,而是因為它剛好夠小又包含完整概念。

用軟體開發角度看,CHIP-8 emulator 很適合練:

  • instruction decoding
  • memory model
  • fetch-decode-execute cycle
  • graphics rendering
  • keyboard input
  • timers
  • emulator architecture

至少我會因為這個專案學會一台電腦怎麼執行指令。


認識 CHIP-8 規格

接下來呢,就要來解讀規格,這樣才可以知道 emulator 該怎麼製作。我將會以經典的《Cowgod’s Chip-8 Technical Reference》這份規格書作為藍圖。

Memory

首先需要了解這台虛擬機器有多少記憶體以及如何分區。

CHIP-8 具有 4KB(4,096 bytes)的記憶體,位址範圍從 0x0000xFFF

  • 系統保留區(0x0000x1FF
    這前 512 bytes 原本是保留給早期的直譯器使用的。但在現代模擬器中,這裡通常用來存放 0F 的十六進位字型資料(Font Data)。
  • 程式存放區(0x2000xFFF
    大多數的 Chip-8 遊戲或程式,都會從 0x200 這個位址開始載入並執行。
+---------------+= 0xFFF (4095) End of Chip-8 RAM  
|               |  
|               |  
|               |  
|               |  
|               |  
| 0x200 to 0xFFF|  
|     Chip-8    |  
| Program / Data|  
|     Space     |  
|               |  
|               |  
|               |  
+- - - - - - - -+= 0x600 (1536) Start of ETI 660 Chip-8 programs  
|               |  
|               |  
|               |  
+---------------+= 0x200 (512) Start of most Chip-8 programs  
| 0x000 to 0x1FF|  
| Reserved for  |  
|  interpreter  |  
+---------------+= 0x000 (0) Start of Chip-8 RAM

Registers

CPU 在進行運算時,需要有地方暫時存放資料,這就是暫存器的功用。CHIP-8 系統中定義了幾種不同用途的暫存器:

  • 一般用途暫存器(V0 – VF)
    CHIP-8 共有 16 個 8-bit 的暫存器,通常被標示為 V0VF。其中,VF 是一個非常特殊的暫存器,一般程式不應該隨意使用它,因為某些特定的指令會把它當作「旗標(Flag)」來使用,例如用來標記加減法是否發生進位(Carry Flag、Borrow Flag)、像素是否產生碰撞(Collision Flag)等。
  • 索引暫存器(I)
    這是一個 16-bit 的特殊暫存器。由於 CHIP-8 的最大記憶體位址到了 0xFFF,超出了 8-bit 暫存器能儲存的上限,因此必須使用這個 16-bit 的暫存器來專門儲存記憶體位址(通常用來指向繪圖時需要的圖形資料位置)。
  • 程式計數器(Program Counter, PC)
    這是一個 16-bit 的指標,CPU 必須依靠它來記錄「下一步要從哪個記憶體位址讀取指令」。
  • 堆疊(Stack)與堆疊指標(Stack Pointer, SP)
    當程式執行「呼叫副程式」的指令時,CPU 必須記住原本執行的位置以便未來返回。CHIP-8 具備了 16 層的堆疊,代表它可以處理最多 16 層的副程式呼叫。同時,它需要一個 8-bit 的堆疊指標來記錄目前堆疊陣列使用到了哪一層(最頂端在哪裡)。

Keyboard

原本執行 CHIP-8 語言的早期電腦,使用的是一種 16 鍵的十六進位鍵盤。它的原始佈局長這樣:

123C
456D
789E
A0BF

在現代開發模擬器時,就必須將這個 16 宮格的硬體鍵盤,對應到現代電腦鍵盤的按鍵上。

Display

原始的 CHIP-8 語言是透過一台解析度為 64×32 像素的單色顯示器來呈現畫面。它的座標系統從左上角開始:

(0,0)(63,0)
(0,31)(63,31)

關於畫面的顯示,規格書有幾個重要的定義:

  • 精靈圖(Sprites)
    CHIP-8 是透過把精靈圖畫到螢幕上來顯示圖像。精靈圖本質上是一段二進位資料(1 代表亮,0 代表暗),一般的精靈圖最大可以達到 8×15 像素(15 bytes)。
  • 內建字型
    系統本身內建了 0F 共 16 個十六進位數字的精靈圖,每個字型固定為 8×5 像素(5 bytes)。這些字型的資料就存放在剛剛提到的「記憶體系統保留區」中。
  • XOR 繪圖邏輯
    這是 CHIP-8 繪圖最特別的地方。當新圖形被畫到螢幕上時,會與螢幕現有的像素進行「互斥或(XOR)」運算。也就是說,如果畫上去的新像素與原本畫面上的亮點重疊,該像素就會反白變暗(關閉)。如果這導致了畫面上有任何像素被關閉,剛剛提到的 VF 暫存器就會被設定為 1,遊戲就是靠這個機制來判斷物體是否發生了碰撞。

Timers & Sound

CHIP-8 提供了兩個獨立的 8-bit 計時器來控制遊戲節奏與發出聲音:

  • 延遲計時器(Delay Timer)
    這個計時器負責控制時間。只要它的數值不為 0,它就會以 60Hz 的速率自動遞減,直到歸零為止。
  • 音效計時器(Sound Timer)
    運作的原理與延遲計時器一模一樣,也是以 60Hz 的速率遞減。唯一的差別在於,只要音效計時器的數值大於 0,CHIP-8 就會控制蜂鳴器發出單一頻率的嗶聲,直到數值歸零聲音才會停止。

後記

之前因為工作花了不少心思在硬體概念上,買了不少書、聽了不少線上課程都一知半解,結果透過經典案例卻學更多更快,更能融會貫通。這個 CHIP-8 emulator 的例子也是,因為要認識軟體的 emulator 製作概念,透過大家的熱心分享以及專案的開放,反而先理解了所謂 CPU 基礎架構,進而貫通了整個運作方式。

在我學習硬體概念的這條路上反而是因為軟體的開放性而被解救(或是西方文化?),果然還是比較喜歡軟體圈的開放氛圍。


References

讓我知道你在想什麼!