自作ゲームボーイエミュレータメモ

正直なところエミュレータを完成させていないし、完成しない気もしているので理解がいろいろとおかしいところもあるかと思うので基本的には元資料見ていただくのが正解かと。

(ターミナルでもグラフィックのデバッグできたりします)

資料

ざっくり

上記のgbdev.ioを見てもらえば特に説明することないんだけど、簡単に。 ゲームボーイエミュレータは主に以下で考えればよい。

  • Memory
  • CPU
  • PPU (Picture Processing Unit)
    • グラフィックスはGPUとは言わずPPUと言うらしい
    • ファミコンなんかではPPUやAPUがCPUとは別々のプロセッサだった
    • ゲームボーイではCPUもPPUもAPUもワンチップらしい。
  • APU (Audio Processing Unit)
    • 音楽
  • MBC (Memory Bank Controller)
    • メインメモリが0xFFFFまでしかアドレス空間がないのを拡張的に扱う物
  • ROM (ポケモンなどのゲームカセット)
  • Log
    • ログはないとまったくデバッグできないので最初から実装すべし
  • Display
    • ディスプレイはお好みのGUIライブラリで描写
    • 最初はターミナルでアスキーアート的にやってもいいと思う

CPU

8ビットのレジスタがA,F,B,C,D,E,H,L
16ビットのレジスタがAF,BC,DE,HL,SP(Stack Pointer),PC(Program Counter)
AF,BC,DE,HLは8ビットのレジスタ二つを並べて疑似的に16ビットレジスタとして扱っている。
BCはBとCで、BC=0xABCDとすれば、B=0xAB、C=0xCDとなる。
AとFは特殊でAはアキュムレーターとして、Fはフラグとしても扱われる。
フラグはZero,Negative,Half Carry,Carryの4つ

F:11110000
  |||+-> Carry
  ||+-> Half Carry
  |+-> Negative
  +-> Zero
  • Zeroは演算結果が0だった場合に1になる。
  • Negativeは引き算を使ったときなどに1になる。
  • Half Carryは4ビット目が繰り上がったときなどに1になる。
  • Carryは8ビット目が繰り上がったときなどに1になる。

命令のクロック数は全体を同期するときに重要。

割り込み、Interrupts

割り込みは主にV-Blank、LCD STAT、Timer、Serial、Joypadの5種類。

  • V-BlankはPPUが画面の書き込み操作が一定以上完了した時にオンになる。
  • LCD STATはPPUによって4種類ほど割り込みのタイミングが設定でき、それによってオンになる。
  • TimerはTACで設定したクロックのタイミングによってオンになる。
  • Serialはケーブル接続時のゲームボーイtoゲームボーイのシリアル通信に完了時にオンになる。
  • Joypadはプレイヤーによるコントローラーの操作時にオンになる。

IME,IE(0xFFFF),IF(0xFF0F)の三つのレジスタで操作される。

  • IMEはInterrupt Master Enable Flagであり、すべての割り込みのオン、オフを決める。
  • IEはInterrupt Enableで、個別の割り込みのオン、オフを操作できる。
  • IFはInterrupt Flagで、個別に割り込みをリクエストすることができる。 三つのフラグがあり、ややこしい。

タイマー、Timer

DIV(0xFF04),TIMA(0xFF05),TMA(0xFF06),TAC(0xFF07)のレジスタによって操作される。

  • TACはタイマーコントロール。タイマーのオン、オフとタイマーの周期を4種類設定できる。
  • DIVはタイマーのオンオフ関係なく16384hz周期でインクリメントされる。8ビットなので0xff超えると0になる。
    • DIVの16384hzというのはゲームボーイのクロックが4194304hzで、4194304/16384=256(0xFF)からなんじゃないかと。
  • TIMAはタイマーカウンター。TACで設定した周期でインクリメントされる。
    • オーバーフローすると割り込みのフラグを立てる。
  • TMAはTIMAはオーバーフローしたときにTIMAにセットする値。

PPU

下から順にBackground、Window、Spritesとレイヤーになっている。
スプライトないしタイルという単位でキャラクターや背景や文字などが管理されている。
スプライトはOAM(Object Attribute Tabe)で位置や向きなどが設定される。

LCD Display Timing

ディスプレイはlineごとに描写される。
lineごとにOAMからスプライト検索し、書き込み、H-Blankに入る。
144lineでV-Blankに入る。
STAT(0xFF41)で設定されていれば、OAM検索時、H-Blank時、V-Blank時に割り込みフラグが立つ。

ラインごとに描写するのは実装が難しいので、CPUと同期をとってフラグ管理だけ進めていき、
最後にまとめて描写するのが簡単な実装になるのではないかと思う。

APU

手付かずにつき、省略。

MBC

Game Boy: Complete Technical Referenceが分かりやすい。

ゲームボーイのメインメモリは1Byteが0xFFFF個しか乗らないし、PCも16ビットで0xffffまでしか数えられない。
ROMによっては1.5MBまであり、バンクと言う概念を使い、これらにアクセスしていく。
MBCにもいくつか種類がある。ROMによってMBCが変わってくる。
たとえば、ゼルダの伝説 夢を見る島であればMBC1。ポケットモンスター 赤であればMBC3。

MBC1であれば、メインメモリの0x0000-0x3fffがROM Bank1、0x4000-0x7fffがROM Bank2、
0xa000-0xbfffにRAM Bankのコントロールレジスタがある。
やたら範囲が広いがたいてい一か所に書き込まれるだけのような気がするがわからん。
アドレス0x0000-0x3fffにBank 0、0x4000-0x7fffにBank Nが配置される。
こんな感じでアドレスを組み立てていく。

// Read Bank N
let i = (bank2 << 19) | (bank1 << 14) | (index - 0x4000);
cartridge.rom[i];

個人的つまづきポイントとして、0x0000-0xbfffにMBCの設定レジスタが配置されることになっているのに、
0x0000-0x7fffのアドレスからどのようにROMの内容を読みだすのか混乱した。
これは、MBCを通した0x0000-0x7fffの読み込みの時はROMから読んで、0x0000-0xbfffに書き込むときはメモリに書き込む。
つまり、ReadとWriteは別の場所にそれぞれ行われている。
メインメモリに両方とも展開されていると思い込んでいたのが混乱した要因だった。