NESエミュレータを作る-CPUちょっと理解編

[PONOS Advent Calendar 2022](https://qiita.com/advent-calendar/2022/ponos) 15日目です。
昨日は[@honeniq](https://qiita.com/honeniq)さんの[git cat-fileでGitとちょっと仲良くなる
](https://qiita.com/honeniq/items/18644bb08601260b1ea4)でした。

[NESエミュレータを作る-準備編](https://qiita.com/ackyla/items/76a33198c077fc0093ee)の続きです。
今回は[こちらのサイト](http://hp.vector.co.jp/authors/VA042397/nes/sample.html)で配布されているサンプルROM「Hello, World!」を使ってプログラムがCPU上でどのように処理されていくかを追っていきます。

# この記事の目標
いきなりファミコンはこう動いている！のようなデカイ部分を見ると大変なので
この記事ではサンプルROMをどう動かすかの部分にのみ注目します。
サンプルROMに登場しない命令やファミコンのハード的な部分の説明は省きます。
プログラムがどのように処理されていくかの流れが理解できれば目標達成です。

# Hello, World!
サンプルROMは起動するとHello, World!という画像を画面中央に表示するだけのものです。
![helloworld.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/356727/c15cf4e3-50cc-ac15-72f7-18aaa2fe477a.png)

# iNESヘッダー
さっそくROMの中身を見ていきましょう。
```ruby
# この読み込み方だと実際は10進数で表示されますがわかりにくいので16進数表示にしています
buf = File.open("./sample1.nes", "rb").read.bytes
buf[0] # 0x4e(N)
buf[1] # 0x45(E)
buf[2] # 0x53(S)
buf[3] # 0x1a(SUB)
buf[4] # 0x02
buf[5] # 0x01
```
ROMの先頭16Byteは**iNESヘッダー**と呼ばれる情報が入っています。
1〜4Byteは固定値のAsciiコードです。
重要なのは5, 6Byte目でそれぞれ**プログラムROM**と**キャラクターROM**のサイズになっています。
プログラムROMは16KiB、キャラクターROMは8KiB単位になっているのでサンプルROMのプログラムROMサイズは32KiB、キャラクターROMサイズは8KiBになります。
よって、以下のように切り出すことができます。
```ruby
program_rom = buf[0x10, 0x8000]
character_rom = buf[0x8010, 0x2000]
```
ファミコンはこのプログラムROMをCPUのメモリのアドレス0x8000〜に読み込んで逐次処理していきます。

# 命令セット
プログラムROMには1Byteで表された命令の種類とアドレッシングモードの組み合わせ（命令セット）が入っています。
サンプルROMで使われる命令セットは11種類（命令9種類、アドレッシングモード5種類）のみです。
### LDA Immediate (0xa9)
命令セットの次のアドレスにある値をAレジスタに読み込みます。
### LDA AbsoluteX (0xbd)
命令セットの次のアドレスに入っている値を下位、次の次の値を上位としたアドレスにXレジスタの値を足したアドレスにある値をAレジスタに読み込みます。
### LDX Immediate (0xa2)
命令セットの次のアドレスにある値をXレジスタに読み込みます。
### LDY Immediate (0xa0)
命令セットの次のアドレスにある値をYレジスタに読み込みます。
### STA Absolute (0x8d)
命令セットの次のアドレスに入っている値を下位、次の次の値を上位としたアドレスにAレジスタの値を書き込みます。
### TXS Implied (0x9a)
Xレジスタの値をスタックポインタにコピーします。
### DEY Implied (0x88)
Xレジスタの値をデクリメントする。
### INX Implied (0xe8)
Yレジスタの値をインクリメントする。
### JMP Absolute (0x4c)
次に処理するアドレスを命令セットの次のアドレスに入っている値を下位、次の次の値を上位としたアドレスに変更する。
### BNE Relative (0xd0)
ステータスレジスタzeroがfalseのときのみ次に処理するアドレスを次のアドレスに入っている値を次の次のアドレスに足したアドレスに変更する。
このとき足す値は符号付き8bit値として扱う。
### SEI Implied (0x78)
ステータスレジスタinterruptをtrueにする。

文字で説明してもなんのこっちゃという感じだと思うので実際にプログラムROMの処理を追っていきましょう。

# プログラムROMの処理を追う
ゲームを起動した時やリセットボタンを押したときはプログラムの先頭から処理が始まります。
プログラムの先頭を示すアドレスはCPUメモリ上のアドレス0xfffc、0xfffdにあります。
```ruby
# プログラムROMはCPUメモリのアドレス0x8000から配置されているので
# メモリ上のアドレスから0x8000を引いたインデックスの値を取得する
def read(address)
    return program_rom[address - 0x8000]
end

lower = read(0xfffc) # 0x00
upper = read(0xfffd) # 0x80
address = lower | upper << 8 # 0x8000
```
サンプルROMでは0xfffcの値0x00を下位、0xfffdの値0x80を上位としたアドレス0x8000
つまり`program_rom`として切り出した先頭から処理をはじめます。

```ruby
read(0x8000) # 0x78 SEI Implied
read(0x8001) # 0xa2 LDX Immediate
read(0x8002) # 0xff
read(0x8003) # 0x9a TXS Implied
read(0x8004) # 0xa9 LDA Immediate
read(0x8005) # 0x00
read(0x8006) # 0x8d STA Absolute
read(0x8007) # 0x00
read(0x8008) # 0x20
read(0x8009) # 0x8d STA Absolute
read(0x800a) # 0x01
read(0x800b) # 0x20
```
まず0x8000の値は0x78なので`SEI Implied`が実行されステータスレジスタinterruptがtrueになります。
次のアドレス0x8001の値は0xa2なので`LDX Immediate`が実行され次のアドレス0x8002の値0xffがXレジスタに読み込まれます。
演算対象になったアドレスは飛ばされ次は0x8003の`TXS Implied`が実行されXレジスタの値がスタックポインタにコピーされます。ここでは先程Xレジスタに読み込まれた値0xffがコピーされます。
0x8004は`LDA Immediate`が実行され0x8005の値0x00がAレジスタに読み込まれます。
0x8006は`STA Absolute`が実行され0x8007を下位、0x8008を上位としたアドレス0x2000にAレジスタの値0x00を書き込みます。
0x8009も同様に0x800aを下位、0x800bを上位としたアドレス0x2001にAレジスタの値を書き込みます。

ここまでで何をしているかの説明は省きますがこのように処理が進んでいきます。

```ruby
read(0x800c) # 0xa9 LDA Immediate
read(0x800d) # 0x3f
read(0x800e) # 0x8d STA Absolute
read(0x800f) # 0x06
read(0x8010) # 0x20
read(0x8011) # 0xa9 LDA Immediate
read(0x8012) # 0x00
read(0x8013) # 0x8d STA Absolute
read(0x8014) # 0x06
read(0x8015) # 0x20
```
続いて0x800cの`LDA Immediate`でAレジスタに0x3fを読み込みます。
そして0x800eの`STA Absolute`でアドレス0x2006にAレジスタの値を書き込みます。
同様に0x8011の`LDA Immediate`でAレジスタに0x00を読み込み0x8013の`STA Absolute`でアドレス0x2006にAレジスタの値を書き込みます。

ここではアドレス0x2006に値を書き込むことで描画専用のユニットであるPPUの設定をしています。
0x3f、0x00を連続で書き込むことでこれからパレットの設定をするという状態にしています。

```ruby
read(0x8016) # 0xa2 LDX Immediate
read(0x8017) # 0x00
read(0x8018) # 0xa0 LDY Immediate
read(0x8019) # 0x10
read(0x801a) # 0xbd LDA AbsoluteX ===      ここから     ===
read(0x801b) # 0x51
read(0x801c) # 0x80
read(0x801d) # 0x8d STA Absolute
read(0x801e) # 0x07
read(0x801f) # 0x20
read(0x8020) # 0xe8 INX Implied
read(0x8021) # 0x88 DEY Implied
read(0x8022) # 0xd0 BNE Relative
read(0x8023) # 0xf6               === ここまで16回ループ ===
```
ここからはループを使ってパレットの設定をしています。
まず0x8016の`LDX Immediate`でXレジスタに0x00、0x8018の`LDY Immediate`でYレジスタに0x10を読み込みます。
Yレジスタがループカウンタの役割をしていてこれが0になるまでループが続きます。
0x801aの`LDA AbsoluteX`は0x801bを下位、0x801cを上位としたアドレス0x8051にXレジスタの0x00を足したアドレスの値をAレジスタに読み込みます。
最初のループではXレジスタが0x00なので0x8051の値0x0fが読み込まれます。ループの最後でXレジスタがインクリメントされることでここで読み込むアドレスが変化していきます。
0x801dの`STA Absolute`でAレジスタの値0x0fをアドレス0x2007に書き込みます。これでパレットの設定をしたことになります。
ループの最後0x8020`INX Implied`、0x8021`DEY Implied`でXレジスタをインクリメント、Yレジスタをデクリメントしています。
0x8022の`BNE Relative`の時点でYレジスタが0になっていれば処理が0x8024に進みますが、なっていない場合は0x8023の値0xf6（符号付き8bit値の-10）と0x8024を足したアドレス0x801aに処理が戻ります。
このループを16回繰り返すと0x0f, 0x00, 0x10, 0x20, 0x0f, 0x06, 0x16, 0x26, 0x0f, 0x08, 0x18, 0x28, 0x0f, 0x0a, 0x1a, 0x2aが書き込まれ、これがパレットの値になります。

```ruby
read(0x8024) # 0xa9 LDA Immediate
read(0x8025) # 0x21
read(0x8026) # 0x8d STA Absolute
read(0x8027) # 0x06
read(0x8028) # 0x20
read(0x8029) # 0xa9 LDA Immediate
read(0x802a) # 0xc9
read(0x802b) # 0x8d STA Absolute
read(0x802c) # 0x06
read(0x802d) # 0x20
```
ここではPPUにパレット設定を伝えた時の処理と同じように0x21, 0xc9を連続で書き込むことで
これから画面描画の処理をするという状態にしています。

```ruby
read(0x802e) # 0xa2 LDX Immediate
read(0x802f) # 0x00
read(0x8030) # 0xa0 LDY Immediate
read(0x8031) # 0x0d
read(0x8032) # 0xbd LDA AbsoluteX ===      ここから     ===
read(0x8033) # 0x61
read(0x8034) # 0x80
read(0x8035) # 0x8d STA Absolute
read(0x8036) # 0x07
read(0x8037) # 0x20
read(0x8038) # 0xe8 INX Implied
read(0x8039) # 0x88 DEY Implied
read(0x803a) # 0xd0 BNE Relative
read(0x803b) # 0xf6               === ここまで13回ループ ===
```
パレットの設定をしたときと同じような処理をしています。
このループを13回繰り返すと0x48, 0x45, 0x4c, 0x4c, 0x4f, 0x2c, 0x20, 0x57, 0x4f, 0x52, 0x4c, 0x44, 0x21が書き込まれ、これがキャラクターROMにある「Hello, World!」のスプライトを示す値になります。

```ruby
read(0x803c) # 0xa9 LDA Immediate
read(0x803d) # 0x00
read(0x803e) # 0x8d STA Absolute
read(0x803f) # 0x05
read(0x8040) # 0x20
read(0x8041) # 0x8d STA Absolute
read(0x8042) # 0x05
read(0x8043) # 0x20
read(0x8044) # 0xa9 LDA Immediate
read(0x8045) # 0x08
read(0x8046) # 0x8d STA Absolute
read(0x8047) # 0x00
read(0x8048) # 0x20
read(0x8049) # 0xa9 LDA Immediate
read(0x804a) # 0x1e
read(0x804b) # 0x8d STA Absolute
read(0x804c) # 0x01
read(0x804d) # 0x20
read(0x804e) # 0x4c JMP Absolute
read(0x804f) # 0x4e
read(0x8050) # 0x80
```
色々書き込みをしたあと0x804eの`JMP Absolute`で0x804eに処理を進めて無限ループに入るところでプログラムは終了です。
# おわりに
今回はサンプルROMのプログラム部分がCPUでどのように処理されていくのかを細かく見ていきました。
詳細は省いていますがなんとなく流れは理解できたんじゃないかと思います。

次回に続きます。

明日は[@nisei275](https://qiita.com/nisei275)さんです。