ファミコンのエミュレータを自作してHello, World!を表示してみた
ファミコンのエミュレータを自作してみました。
前半はファミコンのエミュレータを作るとは何をつくることなのか、後半は参考サイトである程度知識がついたうえでHello, Worldの表示までにどこまで実装すべきかを挙げます。
目標
Hello, Worldの表示のみ。
参考
ファミコンエミュレータの創り方 - Hello, World!編 - - Qiita
今回使用した言語とかライブラリ
C++
Windows API
(C++やWindowsAPIでなくとも大丈夫です)
使用したROM
NES研究室のHello, World!を使用しました。
参考のとこに書いてあるリンクの先から取得できます
自作するにあたって事前に必要な知識
16進数に関しての理解
使用する言語においての
bit演算の書き方
画面表示においてピクセル単位での書き方
この辺りの知識があれば、参考サイトを読み進めていくだけで大丈夫だと思います。
あれば良い知識としてアセンブリ言語についての知識あたりです。
用語の簡単な説明
ROM
自分の世代でいうとこのカセット。
キャラクターROM、プログラムROM
ROMは2つの領域(拡張ROMも存在するけど今回は使わない)がある。
キャラクターROMにはキャラ画像やオブジェクトの画像(スプライト)などが保存されている。
プログラムROMによってその画像をどこに表示するか制御し、ゲーム画面を生成できるようにする。
CPU
プログラムROMの命令を実行するもの。PCに入ってるCPUとは別のものとして考えた方が楽かも。
PPU
GPU的な存在。画面の表示はPPUを介して行われる。
何をするのかについて簡単な説明
実際につくると言っても全くイメージが湧かないと思います。
以下がおおまかな流れです。
カセット内部(ROM)から命令を取り出す
命令一つ一つを実行してPPUのメモリにデータをセット
PPUが自分のメモリを読み取ってスプライトをセットする
カセット内部(ROM)から命令を取り出す
内部的にはかなり簡単な話です。
サンプルROM自体は"sample1.nes"というファイルです。
中身を表示すると以下の感じになっています(Hex Editorで表示)
先頭の16Byteはヘッダーです。ここをルールに従って解釈すると、2行目の先頭がプログラムROMの先頭であることが分かります。
Hello, Worldのプログラムは2行目から8行目までしかありません。
ファイルを読み込んで、1Byteの配列(char配列とか)に格納。
以降は配列へアクセスするだけでプログラムROMを読めるようにしておきましょう。
命令一つ一つを実行してPPUのメモリにデータをセット
詳しい命令の意味については省略しますが、
0x11に書かれているA2について説明します。
まずA2というコードはLDX命令、アドレッシングはImmediateということを知る必要があります。
これはNES研究室の6502というページに書かれています。今後も他のコードの動きを知るためにこのページを確認する必要があります。
アドレッシングは命令コード以降のByteをどう解釈するかを決めています。
今回は0x12にあるFFという値はA2の命令を実行するために使われ、次の命令が0x13に書かれている9Aになります。
これを続けていくとPPU内のメモリにデータが配置されます。
この命令を実行していくものをCPUと呼びます。
PPUが自分のメモリを読み取ってスプライトをセットする
PPUの中に「どの位置にどのスプライトでどの色を使って描画するか」の情報が入れられているためそれ通りに描画を行います。
この描画にはキャラクターROMの内容も参照する必要があります。
詳しい話は長くなるので参考サイトを見てもらった方が良いと思います。
じゃあ何を作ればいいのか
CPUとPPUを作る必要があります。
CPUを作るとは
メモリを用意して命令を実行できるよう実装します
メモリだとか言ってますが、基本的には1Byteの配列を定義してそこを読み取ったりできるようにするだけです。
ただ、基本的にの0x0000~0xFFFFのアドレスにデータがあると思いながら動きます。0x1000にデータを置きたいとなった場合は配列の先頭から0x1000番目にデータを置くよう変換する必要があります。
LDX命令を例にとってみてみると
レジスタXにデータを命令です。このレジスタも配列や構造体としてメモリを確保し、そこを利用する感じです。
A2 FF と並んでいる場合はFFをレジスタXに保存します。
また、サイクル数を数えたり、ステータスレジスタを変更したりしますが、その辺りは省略します。
PPUを作るとは
CPUみたいにメモリを用意します。
後はメモリ内の情報から描画できるように実装するだけです。
描画に関しては省略
Hello, Worldまでに何を実装すればいいか
ここまではエミュレータって実際何をするのかの話でしたが、これ以降は実際に作るにあたって何が必要かの話になります。
参考サイトからある程度学んだ前提で進めていきます。
CPUの命令の実装
とはいえ用意する命令コードは少なく、
0x78
0xA2
0x9A
0xA9
0x8D
0xA0
0xBD
0xE8
0x88
0xD0
の10種です。
0x4Cを最後に呼び出しますが無限ループ用なので、0x4Cが読み込まれたらプログラムROMの終端と認識とできます。
CPUステータスレジスタの実装
全て実装する必要はなくゼロのみ実装するだけです。
演算結果が0になったらフラグを立てるだけですね。
PPUレジスタの実装
PPUADDRとPPUDATAの部分の実装のみです。
PPUADDRにデータを2回書き込んでPPUDATAにデータをセットすると、PPU内のメモリにPPUDATAで書き込んだデータを保存できるまで実装が必要です。
PPUの描画処理の実装
ネームテーブル、属性テーブル、パレットを参照してキャラクターROMの情報から描画できるまで実装する必要があります。
その他
画面に描画する機能。今回はWindowsAPIを使ってますが、ピクセル単位で描画できる必要があります。
逆に何を実装しなくていいのか
割り込み
ほぼ使いません。
本来は起動時にリセットという割り込みを行ってプログラムROMの先頭から読むみたいですが、1つのROMにしか対応しないので先頭の位置は毎回同じです。決め打ちで対応できます。PCを0x8000に初期化しておきましょう。
フレームレートの処理(サイクル数の管理)
1度画面に表示した後は無限ループに入ります。ジャンプ命令までいったら描画、その後はその画面で待機するように作ります。
音の処理
APUを使うようですが、今回は必要なし
入力の処理
コントローラーからの入力なども使いません。
ちゃんとCPUが動くとPPU内のメモリはどうなるか(確認用)
0x21c9~ 72, 69, 76, 76, 79, 44, 32, 87, 79, 82, 76, 68, 33
0x3f00~ 15, 0, 16, 32, 15, 6, 22, 38, 15, 8, 24, 40, 15, 10, 26, 42,
がセットされているはずです。それ以外は0。
実行画面
実際に作った感想
最初はちんぷんかんぷんでしたが、一つ一つの単語の意味をメモしながら進めていきましたね・・・。
CPUの実装を始めたあたりからはスラスラ進みました。似たような命令も少ないのでコード自体の構成も深く気にしなくて大丈夫そうです。
WindowsAPIを使った影響で、標準出力先をファイルなどに指定しないとデバッグなどが難しかったです・・・。WindowsAPIはあまりおすすめできないですね(とはいえ他にいいものがあるかも知りません)
CPU、PPUの実装に共通してですが、デバッグ機能として「どこの番地をReadして、どんな値がはいっていたか」などを出力できるように表示用の関数を仕込んでおくと良いかと思います。
終わりに
自分が始めるときに欲しかった情報をまとめてみました。
やる気がでればGBのエミュレータとかつくってみたいですね。