在完成記憶體、暫存器、堆疊與鍵盤等基礎元件後,接下來終於要讓 CHIP-8 模擬器真正進入可運作的狀態啦!
這一階段會先建立統一的初始化流程,確保各項硬體資料在程式啟動時都有一致的初始值;接著加入 CHIP-8 內建的十六進位字元集,將 0 到 F 的精靈資料載入直譯器保留的記憶體區段。
完成這些準備後,模擬器就具備基本的啟動環境,也能繼續進入後續的畫面繪製與指令執行階段囉!
系統初始化實作
在陸續完成記憶體、暫存器、堆疊與鍵盤等硬體元件的資料結構設計後,啟動模擬器的第一步,就是確保這些元件都有乾淨且一致的初始狀態。
宣告與實作初始化函式
首先,我們需要在專案中建立一個新的原始碼檔案 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.o 由 src/chip8.c 編譯產生。
./build/chip8.o:src/chip8.c
gcc ${FLAGS} ${INCLUDES} ./src/chip8.c -c -o ./build/chip8.o
內建字元集實作
根據 CHIP-8 規格,預設的字元集(Character Set)資料應當被存放在記憶體位址 0x000 到 0x1FF 之間。這塊區域是特別保留給直譯器(Interpreter)使用的,因此在此載入字元集並不會影響到一般程式的執行空間。
由於有許多 CHIP-8 程式都會依賴這組內建的字元集,因此將其正確地寫入系統是非常重要的一步。這部份將會完成 16 個十六進位字元的資料宣告,並將其整合到模擬器的初始化流程中。
記憶體配置與精靈(Sprite)原理
記憶體保留區段
如同前述,這組內建字元集會被載入直譯器專用的保留記憶體區段。如此一來,後續開發螢幕顯示功能時,就能直接從指定的記憶體位址讀取字元圖形,並將它們繪製於畫面上。
像素顯示與二進位結構
CHIP-8 以精靈(Sprite)資料表示字元與圖形。精靈的每一列皆以一個 byte 儲存,因此每列包含 8 個位元,也就是 8 個像素。
精靈的像素表示方式相當單純:位元值為二進位的 1 時,表示該位置具有像素;位元值為二進位的 0 時,則表示該位置沒有像素。
實際繪製時,會將精靈資料與畫面中的像素進行 XOR 疊加。
宣告字元集陣列
回到先前建立的原始碼檔案,我們需要宣告一個常數陣列,用來存放預設的字元資料。這個陣列會依序包含從 0 到 F,共 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,預設字元集就會被複製到記憶體中的指定位置。
最後,透過重新編譯並成功建置執行檔後,就可以進入下一個階段,開始實作螢幕圖形渲染。





