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

[CHIP-8 小東西] 實作虛擬硬體(系統初始化與字元集)

在完成記憶體、暫存器、堆疊與鍵盤等基礎元件後,接下來終於要讓 CHIP-8 模擬器真正進入可運作的狀態啦!

這一階段會先建立統一的初始化流程,確保各項硬體資料在程式啟動時都有一致的初始值;接著加入 CHIP-8 內建的十六進位字元集,將 0F 的精靈資料載入直譯器保留的記憶體區段。

完成這些準備後,模擬器就具備基本的啟動環境,也能繼續進入後續的畫面繪製與指令執行階段囉!


系統初始化實作

在陸續完成記憶體、暫存器、堆疊與鍵盤等硬體元件的資料結構設計後,啟動模擬器的第一步,就是確保這些元件都有乾淨且一致的初始狀態。

宣告與實作初始化函式

首先,我們需要在專案中建立一個新的原始碼檔案 chip8.c,並在對應的標頭檔(例如 chip8.h)中宣告一個初始化函式 chip8_init

傳入指標:這個函式會預期接收一個 CHIP-8 實例的指標作為參數,以便針對該硬體實例進行操作。

void chip8_init(struct chip8* chip8);

運用 memset 歸零:在 chip8.c 中引入必要的標頭檔後,便可開始實作初始化函式。

最核心的步驟是呼叫 memset 函式,我們依序傳入 CHIP-8 實例的指標、代表歸零的數值 0,以及結構體所占用的記憶體大小。這樣就能一次將整個 CHIP-8 結構體的內容設為零,以簡單的方式完成目前所需的初始化。

#include "chip8.h"
#include <memory.h>

void chip8_init(struct chip8* chip8) {
    memset(chip8, 0, sizeof(struct chip8));
}

在主程式中呼叫與啟動

完成初始化函式的實作後,我們回到主程式 main.c 檔案中。在宣告了 CHIP-8 的實例變數後,我們只需呼叫 chip8_init 函式,並透過取址將該實例的記憶體位址傳遞給函式即可。這確保了模擬器在執行任何操作前,所有狀態皆已初始化為零。

int main(int argc, char** argv) 
{
    struct chip8 chip8;
    chip8_init(&chip8);
    ...
}

Makefile 編譯設定更新

最後一個步驟,是確保新的原始碼檔案有被正確編譯並整合進專案中。

新增編譯規則:比照先前處理鍵盤與堆疊檔案的相同方式,為 chip8.o(物件檔)新增編譯規則。

OBJECTS = ./build/chip8_memory.o \
          ./build/chip8_stack.o \
          ./build/chip8_keyboard.o \
          ./build/chip8.o

指定來源檔案:在 Makefile 中指定 chip8.osrc/chip8.c 編譯產生。

./build/chip8.o:src/chip8.c
	gcc ${FLAGS} ${INCLUDES} ./src/chip8.c -c -o ./build/chip8.o

內建字元集實作

根據 CHIP-8 規格,預設的字元集(Character Set)資料應當被存放在記憶體位址 0x0000x1FF 之間。這塊區域是特別保留給直譯器(Interpreter)使用的,因此在此載入字元集並不會影響到一般程式的執行空間。

由於有許多 CHIP-8 程式都會依賴這組內建的字元集,因此將其正確地寫入系統是非常重要的一步。這部份將會完成 16 個十六進位字元的資料宣告,並將其整合到模擬器的初始化流程中。

記憶體配置與精靈(Sprite)原理

記憶體保留區段

如同前述,這組內建字元集會被載入直譯器專用的保留記憶體區段。如此一來,後續開發螢幕顯示功能時,就能直接從指定的記憶體位址讀取字元圖形,並將它們繪製於畫面上。

像素顯示與二進位結構

CHIP-8 以精靈(Sprite)資料表示字元與圖形。精靈的每一列皆以一個 byte 儲存,因此每列包含 8 個位元,也就是 8 個像素。

精靈的像素表示方式相當單純:位元值為二進位的 1 時,表示該位置具有像素;位元值為二進位的 0 時,則表示該位置沒有像素。

實際繪製時,會將精靈資料與畫面中的像素進行 XOR 疊加。

宣告字元集陣列

回到先前建立的原始碼檔案,我們需要宣告一個常數陣列,用來存放預設的字元資料。這個陣列會依序包含從 0F,共 16 個十六進位字元的圖形資料。

每個字元由 5 bytes 組成,每個 byte 代表精靈中的一列。以數字 0 為例,其資料如下:

0xF0, 0x90, 0x90, 0x90, 0xF0

將這些數值轉換成二進位後,可以得到:

11110000
10010000
10010000
10010000
11110000

其中每個 1 代表圖形中具有像素的位置,組合後便會形成數字 0 的外觀。

完整的字元集陣列如下:

const char chip_8_default_character_set[] = {
    0xf0, 0x90, 0x90, 0x90, 0xf0,  // 0
    0x20, 0x60, 0x20, 0x20, 0x70,  // 1
    0xf0, 0x10, 0xf0, 0x80, 0xf0,  // 2
    0xf0, 0x10, 0xf0, 0x10, 0xf0,  // 3
    0x90, 0x90, 0xf0, 0x10, 0x10,  // 4
    0xf0, 0x80, 0xf0, 0x10, 0xf0,  // 5
    0xf0, 0x80, 0xf0, 0x90, 0xf0,  // 6
    0xf0, 0x10, 0x20, 0x40, 0x40,  // 7
    0xf0, 0x90, 0xf0, 0x90, 0xf0,  // 8
    0xf0, 0x90, 0xf0, 0x10, 0xf0,  // 9
    0xf0, 0x90, 0xf0, 0x90, 0x90,  // A
    0xe0, 0x90, 0xe0, 0x90, 0xe0,  // B
    0xf0, 0x80, 0x80, 0x80, 0xf0,  // C
    0xe0, 0x90, 0x90, 0x90, 0xe0,  // D
    0xf0, 0x80, 0xf0, 0x80, 0xf0,  // E
    0xf0, 0x80, 0xf0, 0x80, 0x80   // F
};

請務必參考《Cowgod’s Chip-8 Technical Reference v1.0》中 Display 的章節,確認字元集內容與精靈的資料格式。

將字元集載入系統記憶體

設定載入位址(config.h)

為了保持專案架構的整潔,我們先到設定檔(config.h)中定義一個常數巨集,用來表示字元集的載入位址為 0x000

#define CHIP8_CHARACTER_LOAD_ADDRESS 0x00

透過 memcpy 載入資料

接著,我們回到模擬器的初始化函式(chip8_init)中,運用 memcpy 函式來進行資料拷貝。在函式中傳入記憶體的目標位址指標、剛才建立的字元集陣列來源,以及該陣列的確切大小。

void chip8_init(struct chip8* chip8) {
    ...
    ...
    memcpy(&chip8->memory.memory[CHIP8_CHARACTER_LOAD_ADDRESS], 
        chip_8_default_character_set, 
        sizeof(chip_8_default_character_set));
}

完成這段程式碼後,只要在主函式中呼叫 chip8_init,預設字元集就會被複製到記憶體中的指定位置。

最後,透過重新編譯並成功建置執行檔後,就可以進入下一個階段,開始實作螢幕圖形渲染。


References

讓我知道你在想什麼!