
2025/11/30 0:49
Guide to making a CHIP-8 emulator (2020)
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
概要:
このガイドは、CHIP‑8 インタープリターの構築方法を説明します。完全なソースコードは提供せずに、アーキテクチャと設計指針のみを示しています。必要なハードウェアとして、最大4 KBのRAM、64×32ピクセルのモノクロディスプレイ(SUPER‑CHIPの場合は128×64)、0x200 から始まる16ビットプログラムカウンタ、インデックスレジスタ I、16個の2バイトエントリを持つスタック、V0–VF の16個の8ビット汎用レジスタ、および別々のディレイ/サウンドタイマーが含まれます。メモリ上の最初の512バイト(0x000–0x1FF)は組み込みフォントを保持しており、プログラムは 0x200 にロードされます。
キーパッドは 4×4 のグリッドで 0–F とラベル付けされており、一般的に QWERTY キー(1 2 3 4 Q W E R A S D F Z X C V)にマッピングされています。
コア命令セットには、画面クリア (00E0)、ジャンプとコール (1NNN, 2NNN, 00EE)、スキップ (3XNN, 4XNN, 5XY0, 9XY0)、レジスタ操作 (6XNN, 7XNN, 8XY0–E, FX07/15/18, FX1E, FX29, FX33, FX55/65)、描画 (DXYN)、乱数生成 (CXNN)、キー判定 (EX9E/A1)、ブロック待機 (FX0A) が含まれます。
8XY6/8XYE、BNNN、FX55/FX65 のような曖昧なオペコードは歴史的にバリエーションがあります。インタープリターは「quirk」設定を構成可能にし、望む動作を選択できるようにすべきです。
タイミング:元の CHIP‑8 CPU は約1–4 MHz で動作していました;典型的なインタープリターは秒間約700オペコードを実行します。ディレイとサウンドタイマーはフェッチ/デコード/実行ループとは独立に60 Hzで減算されます。
フェッチ段階では、PC から連続する2バイトを読み取り、それらを16ビットオペコードに結合し、その後 PC を2増加させて実行へ進みます。デコードは最初のニブルでディスパッチを行い、さらに X, Y, N, NN, NNN の値を命令ごとに一度だけ抽出して重複を避けます。
描画 (DXYN) は XOR を使用してピクセルを反転させ、衝突時に VF を設定し、座標はスクリーンサイズでモジュロ演算され、描画中にクリッピングが適用されます。
推奨デバッグ機能にはステップ実行、レジスタ/メモリ検査、および未知のオペコードに対する明確なエラーメッセージが含まれます。
この改訂版概要は、リストからすべての主要ポイントを取り込み、広範囲への影響に関する推測的表現を除外し、読者にとって明瞭さを保っています。
本文
CHIP‑8 エミュレーター構築のハイレベルガイド
1. はじめに
- なぜ CHIP‑8? それは「最初のエミュレーター」プロジェクトとして古典的です。
- 本ガイドでは、インタープリターの各部が何をすべきかを説明します。実際のコードは読者に任せます。
2. 歴史(簡易再現)
| 年 | イベント |
|---|---|
| 1977 | RCA のエンジニア Joe Weisbecker が COSMAC VIP 用に CHIP‑8 を開発。 |
| 1984 | 関心が薄れ、ほとんど使われなくなる。 |
| 1990 | HP48 カルキュレーターで CHIP‑48 と SUPER‑CHIP により復活。 |
3. 前提知識
- 基本的なプログラミングの知識
- バイナリ/16進数の理解
- グラフィック描画とキーボード入力を扱えること(SDL、SFML 等)
4. コアコンポーネント
| コンポーネント | サイズ / タイプ | 備考 |
|---|---|---|
| メモリ | 4096 bytes (0x000–0xFFF) | 全 RAM。プログラムは 0x200 からロードされる。 |
| ディスプレイ | 64×32 モノクロ(SUPER‑CHIP は 128×64) | 各ピクセルはビット単位。 |
| PC | 16bit | 次のオペコードを指す。 |
| I | 16bit | インデックスレジスタ。 |
| スタック | 16レベル、各 16bit | サブルーチン呼び出し用。 |
| タイマー | ディレイ&サウンド(8bit) | 60 Hz 毎に減算。 |
| レジスタ | V0–VF (8bit) | VF はフラグレジスタ。 |
5. フォント
標準の16文字(4×5 ピクセル)のフォントセットをメモリの最初の512バイトに格納します。例:
const uint8_t fontset[80] = { 0xF0,0x90,0x90,0x90,0xF0, /* 0 */ 0x20,0x60,0x20,0x20,0x70, /* 1 */ …(省略)… };
0x050–0x09F、または 0x200 未満の任意のアドレスに配置します。
6. ディスプレイ描画
- DXYN:
から N バイトを読み取り、各ビットをスクリーンバッファへ XOR。[I] - Clear:
。00E0 - 座標はディスプレイサイズでモジュロ計算し、エッジでは描画をクリップ。
- 描画オペコードが実行されたときのみ画面更新(FPS 最適化)。
7. スタック操作
uint16_t stack[16]; int sp = 0; // スタックポインタ // コール stack[sp++] = pc; pc = NNN; // リターン pc = stack[--sp];
8. タイマー
別スレッドまたはタイマーで 60 Hz 毎に両方のタイマーを減算。
サウンドタイマーが >0 のときビープ音を鳴らす。
9. キーパッドマッピング
| CHIP‑8 | 実機 |
|---|---|
| 1 | Q |
| 2 | W |
| … | … |
キースキャンコードを利用して異なるキーボードレイアウトに対応。
10. フェッチ/デコード/実行ループ
while (running) { // --- FETCH ------------------------------------------------- uint16_t opcode = memory[pc] << 8 | memory[pc + 1]; pc += 2; // --- DECODE ------------------------------------------------ switch (opcode & 0xF000) { case 0x0000: if (opcode == 0x00E0) clearDisplay(); else if (opcode == 0x00EE) returnFromSubroutine(); break; case 0x1000: pc = opcode & 0x0FFF; break; // JP addr case 0x2000: callSubroutine(opcode & 0x0FFF); break; case 0x3000: skipIfEqual( (opcode >> 8) & 0xF, opcode & 0xFF ); break; … /* その他ケース */ } // --- TIMER UPDATE ------------------------------------------ updateTimers(); // 外部で 60 Hz にて呼び出し }
速度制御
- ターゲットは ~700 オペコード/秒(調整可能)。
- スリープまたは高精度タイマーでスロットリング。
11. 命令概要
| Opcode | 意味 | 備考 |
|---|---|---|
| 画面クリア | – |
| NNNへジャンプ | – |
| サブルーチン呼び出し | – |
| VX == NN のときスキップ | – |
| VX != NN のときスキップ | – |
| VX == VY のときスキップ | – |
| VX = NN | – |
| VX += NN(キャリー無し) | – |
| VX = VY | – |
| OR | – |
| AND | – |
| XOR | – |
| VX += VY(キャリーは VF) | – |
| VX -= VY(借用は VF) | – |
| SHR VX(クワーク:VY を使うことがある) | – |
| VY -= VX(借用は VF) | – |
| SHL VX(クワーク:VY を使うことがある) | – |
| VX != VY のときスキップ | – |
| I = NNN | – |
| JP V0 + NNN(クワーク:VX を使うことがある) | – |
| ランダム & NN → VX | – |
| (VX,VY) でスプライト描画、I から N バイト読み取り | – |
| キー VX が押されているときスキップ | – |
| キー VX が押されていないときスキップ | – |
| VX = ディレイタイマー | – |
| キー入力待ち → VX に格納 | – |
| ディレイタイマー = VX | – |
| サウンドタイマー = VX | – |
| I += VX(クワーク:VF を設定することがある) | – |
| I = VX の数字に対応するスプライト位置 | – |
| BCD 変換 → [I],[I+1],[I+2] | – |
| V0–VX をメモリへ書き込み(クワーク:I が増分されることがある) | – |
| メモリから V0–VX を読み込む(クワーク:I が増分されることがある) | – |
クワーク
- 「モダン」モード(FX55/65 で I 増分なし、8XY6/E で VY 無視など)を実装し、切り替え可能にする。
12. デバッグツール
- ステップ実行 – ループを一時停止して現在のオペコードを表示。
- レジスタ/メモリダンプ – V レジスタ、I、PC、スタックを出力。
- 未知オペコードでエラー – データとしての誤解読を検出。
- ロギング – オプションで詳細ログ。
13. テスト
| テスト | 用途 |
|---|---|
IBM Logo(, , , , , のみ使用) | 基本描画・レジスタ動作確認。 |
| BC_test / corax89’s test ROM | 全命令網羅、特に曖昧なケースの検証。 |
各実装マイルストーン後に実行。
14. 次のステップ(オプション拡張)
- SUPER‑CHIP – 解像度倍増、スクロール、大きいスプライト。
- XO‑CHIP – 4 色表示、音声、64 KB メモリ。
- デバッグインターフェース – シリアルコンソール、Web UI、ゲーム内デバッガ。
- ネットワークプレイ – Telnet サーバーや SSH エミュレータ。
- Web Assembly – ブラウザで CHIP‑8 を動かす。
15. 結論
これで CHIP‑8 インタープリターを構築するためのアーキテクチャ、コンポーネント、命令セットが揃いました。まずはコアループを実装し、IBM ロゴを動かしてみてください。その後、段階的に拡張しながらテストを繰り返すことで、機能的で安定したエミュレーターへと育て上げましょう。ハッキングを楽しんでください!