CHIP-8 & Golang でエミュレータ作成入門してみた

## はじめに
CPUとかあんまり詳しくないのですが、ファミコンのエミュレータを書きたいなと思い、
[自作エミュレータで学ぶx86アーキテクチャ](https://www.amazon.co.jp/dp/B0148FQNVC)を買って読み、よし作ろうと思っていろいろ調べてたら

> まず最初に`CHIP-8`のエミュレータ書いてみるのがいいよ

というのを見かけ、数日でさくっと書けそうだったので`golang`で書いてみました。
（海外では割とそうみたい？なのか、実装がたくさんありました）

日本語の資料は全く見かけなかったため、CHIP-8について簡単に説明したいと思います。
※コンピュータ用語やCPU自体の説明等はほとんどありません。
※エミュレータの実装方法についてもあまり説明しません。

## CHIP-8について
インタプリタ型の言語であるらしい。`COSMAC-VIP`なるもの等で使われていたとか。
(SuperChip/CHIP48なるものもあるみたいですが、ここでは対象としません）

## CHIP-8アーキテクチャ
以下簡単な説明です。間違ってたらごめんさい。
(最後に[参考資料](#参考資料)をのせてますので、詳しくはそちらを参考にしてください。)

### メモリ
4KB(0x000-0xFFF)のメモリが必要。メモリマップは以下。

|アドレス　　　　　|説明|
|:--|:--|
|0x000-0x1FF|オリジナルではインタプリタ自身が置かれる|
|0x200-0xFFF|CHIP-8のROMが置かれる領域(プログラム+データ)|

エミュレータ等の実装では、プログラム自身は当然**上記領域外に置かれる**ため、
大抵`0x000-0x1FF`にはフォントデータが置かれる。

### レジスタ
* **汎用レジスタ**
    * 8bit - 16個のレジスタ(V0-VF)
    * VFはフラグのみに使われる
        * carry/borrow/なにか
        * フラグがどう更新されるかは、命令実行の結果による。
* **インデックスレジスタ**
    * 16bitレジスタ
    * メモリアドレスを記憶するために使われる
* **DelayTimer/SoundTimer**
    * 後述
* **その他**
    * スタックポインタ
        * スタックの位置管理
    * プログラムカウンタ
        * 実行する命令のアドレス管理

### スタック
`16bit x 16`で、**サブルーチンからの戻りアドレス**のみ記憶される。

### タイマー
`2つのタイマー`がある。0より大きい場合、**60Hzごとにデクリメント**される。

* **DelayTimer**
    * 実行のタイミングを取るために使われる。
* **SoundTimer**
    * 0になるまで音を鳴らす。

### キー入力
**16個のキー**がある。`2/4/6/8`は大体`up/left/right/down`として使われる。

```
1 2 3 C
4 5 6 D
7 8 9 E
A 0 B F
```

### サウンド
音がなれば、**なんでもいい**らしい。今回は`sin波`を鳴らしてみたつもり。

### ディスプレイ
**64 x 32**のモノクロディスプレイ。`左上が(0,0)`で、`右下が(63,31)`。
描画は`XOR`で行われる。
描画に使われるデータは、`1byte(8bit) = 8pixel`で表現される。
（Nバイトの描画データで、`8xN`の領域に描画される。詳しくは次のフォントデータを参照）

#### フォントデータ
**8 x 5**の16文字(0-F)のフォント。

golangで書くと以下のようなデータになる。

```golang
/* 1
|  *  | 0b00100000 | 0x20 |
| **  | 0b01100000 | 0x60 |
|  *  | 0b00100000 | 0x20 |
|  *  | 0b00100000 | 0x20 |
| *** | 0b01110000 | 0x70 |
...
*/
var characterSprites = []uint8{
	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
}
```
### 命令一覧
命令はすべて`2byte`で表現される。
以下の命令がある。

```
テーブル内表記の説明
Vx -> x番目のレジスタ
PC -> プログラムカウンタ
I  -> インデックスレジスタ
DT -> Delay Timer
ST -> Sound Timer
```

|オペコード|命令　　　　　　　　|説明|
|:--|:--|:--|
|0x00E0|CLS|ディスプレイをクリアする|
|0x00EE|RET|サブルーチンから戻る(スタックからpopしたアドレスに戻る)|
|0x1NNN|GOTO 0x0NNN|0x0NNNにジャンプする(PC=0x0NNN)|
|0x2NNN|CALL 0xNNN|0x0NNNのサブルーチンを呼び出す（スタックに戻り先をpushして）|
|0x3XNN|SE Vx, 0xNN|Vx==0xNNであれば、次の命令をスキップする|
|0x4XNN|SNE Vx, 0xNN|Vx!=0xNNであれば、次の命令をスキップする|
|0x5XNN|SE Vx, Vy|Vx==Vyであれば、次の命令をスキップする|
|0x6XNN|LD Vx, 0xNN|Vx=0xNN|
|0x7XNN|ADD Vx, 0xNN|Vx+=0xNN（フラグ変更なし）|
|0x8XY0|LD Vx, Vy|Vx=Vy|
|0x8XY1|OR Vx, Vy|Vx=Vx|Vy|
|0x8XY2|AND Vx, Vy|Vx=Vx&Vy|
|0x8XY3|XOR Vx, Vy|Vx=Vx^Vy|
|0x8XY4|ADD Vx, Vy|Vx=Vx+Vy（桁あふれしたらVF=1、しなければVF=0）|
|0x8XY5|SUB Vx, Vy|Vx=Vx-Vy（桁借りしたらVF=0、しなければVF=1）|
|0x8XY6|SHR Vx|Vx=Vx>>1（その前にVxの最下位ビットをVFにセットする）|
|0x8XY7|SUBN Vx, Vy|Vx=Vy-Vx（桁借りしたらVF=0、しなければVF=1）|
|0x8XYE|SHL Vx|Vx=Vx<<1（その前にVx最上位ビットをVFにセットする）|
|0x9XY0|SNE Vx,Vy|Vx!=Vyであれば、次の命令をスキップする|
|0xANNN|LD I, 0xNNN|I=0xNNN|
|0xBNNN|JP V0, 0xNNN|PC=V0+0xNNN|
|0xCXNN|RND Vx, 0xNN|Vx=rand()&0xNN|
|0xDXYN|DRW Vx, Vy, 0xN|(Vx, Vy)から幅:8、高さ:0xNを、Iが示すアドレスから始まるNバイトのデータで描画する|
|0xEX9E|SKP Vx|Vxが示すキーが押されていたら、次の命令をスキップする|
|0xEXA1|SKNP Vx|Vxが示すキーが押されていなければ、次の命令をスキップする|
|0xFX07|LD Vx, DT|Vx=DT|
|0xFX0A|LD Vx, K|Vxに押されているキーをセットする（何も押されていなければ、次の命令へ進まない）|
|0xFX15|LD DT, Vx|DT=Vx|
|0xFX18|LD ST, Vx|ST=Vx|
|0xFX1E|ADD I, Vx|I=I+Vx|
|0xFX29|LD F, Vx|I=fontData[Vx]|
|0xFX33|BCD Vx|10進数で100の位、10の位、1の位を、`*(I+0)`,`*(I+1)`,`*(I+2)`にセットする|
|0xFX55|LD [I],Vx|V0-Vx(Vxを含む)を、Iから始まるアドレスにセットする|
|0xFX65|LD Vx, [I]|↑の逆をする|

## 実装について
エミュレータの基本部分の実装は以下のようになります。

1. メモリ/各種レジスタの定義
1. 初期化
1. ROMの読み込み
1. 命令のフェッチ/デコード/実行　の繰り返し

簡単なプログラムで表すと、こんな感じです。

```golang
// 1. メモリ/レジスタ等の定義
type Chip8 struct {
	mem   [4096]uint8    // memory
	pc    uint16         // program counter
	v     [16]uint8      // registers
	i     uint16         // index register
	dt    uint8          // delay timer
	st    uint8          // sound timer
	sp    uint8          // stack pointer
	stack [16]uint16     // stack
	keys  [16]uint8      // keyboards state
	disp  [64 * 32]uint8 // graphics
}

func main() {
    // 2. 初期化
    c := &Chip8{ pc : 0x200, sp : 0x0f }
    copy(c.mem, fondData)
    
    // 3. ROMの読み込み
    f, _ := os.Open(*romPath)
    binary, _ := ioutil.ReadAll(f)
    copy(c.mem[0x200:], []uint8(binary))
    
    // 4. 命令のフェッチ/デコード/実行 の繰り返し
    for {
        // フェッチ
        op := uint16(c.mem[c.pc])<<8 | uint16(c.mem[c.pc+1])
        // プログラムカウンタ更新
        c.pc += 2
        // 実行...
    }
}
```

その他、考えなければならないこととして

* 画面の表示
* サウンド
* キー入力
* DelayTimer/SoundTimerの同期
* 実行速度

などがあります。自分は以下のようにしてみました。

* 画面の表示/サウンド/キー入力
    * [SDL2](https://www.libsdl.org/download-2.0.php)を使いました。
    * [go-sdl2](https://github.com/veandco/go-sdl2)
* DelayTimer/SoundTimer
    * 60Hzごとのデクリメントなので、VBlankと同期させてみました。
* 実行速度
    * これがよくわかっていません。他のソースをみてなんとなく決めました。
    * 大体のROMはDelayTimerを使って同期しているため、気にしなくても大丈夫かも？

## おわりに
できたものは[GitHub](https://github.com/whisper0077/chip-8-golang)にあげました。
（今回は勉強のためできるだけ他実装を見ないというのを意識しました）

よくわからないけど、なんかエミュレータ書いてみたい！という人の最初の一歩にはいいと思います。
実装自体もあまり時間をかけずに済みます。

## 参考資料
* アーキテクチャについて
    * https://en.wikipedia.org/wiki/CHIP-8
    * http://devernay.free.fr/hacks/chip8/C8TECH10.HTM
* CHIP-8でのエミュレータ実装方法について
    * http://www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/
* ROM置き場
    * https://www.zophar.net/pdroms/chip8.html
    * https://github.com/dmatlack/chip8/tree/master/roms