Open11

GBAエミュレータ開発記

mjhdmjhd

参考資料たち

CPUやメモリレイアウトなど

グラフィック周り

参考にするOSS実装

mjhdmjhd

ROM吸い出し

メルカリで中古のゲームを買う

  • くるくるくるりん
  • ロックマンEXE3
  • 黄金の太陽 開かれし封印

を準備した!懐かしい

ROM吸い出し機を購入

CUBIC STYLEさんのラズパイにGBAカードリッジさせるアダプタ
https://cubic-style.jp/rpa_exp/

ラズパイは Raspberry Pi Zero W を購入。めちゃちっちゃい

mjhdmjhd

まずはROMのデコード。
GBAはGBやNESのようにカードリッジ内にマッパーがないので、かなりシンプルに書けた
配列に吸い出したROMのデータを取り込むだけ〜

次にある程度の構成を作った

  1. CPU: 命令の実行
  2. PPU: グラフィックス
  3. BUS: データと番地の割り当て
mjhdmjhd

CPU
GBAのCPUはARM7TDMIという32bit ARMプロセッサ。命令セットにはARMv4を使ってる様子。

大きく、ARM命令系とTHUMB命令系に別れていて、32bitCPU + 16bitCPUといった構成。
CPSR(状態レジスタ)のTフラグと連動して、ARMモードとTHUMBモードを行ったり来たりしながら実行される。ハイブリッド!

他のOSSとかみると、armとthumbでファイルを分けたりする程度には趣の違う命令セットになってる。

命令サイクル中で、

if self.cpsr.t() {
  // decode THUMB OPECODE
  // execute
} else {
  // do ARM OPECODE
  // execute
}

といった分岐をする感じ…

mjhdmjhd

レジスタ
ARM7TDMIのレジスタは18個もある!(GB, NESなどのレジスタの少ないCPUしか作って来なかったので衝撃)

汎用レジスタはR0~R12、あとはSP(R13)レジスタ、LR(R14)レジスタ、PC(R15)レジスタ、CPSR(状態レジスタ)、SPSR(CPSRのバックアップ)を用意する。
SPはスタックポインタ、LRはリンクレジスタ(戻り先番地とかが保存される)、PCはPC。

また、CPUには権限モードがあり、User、System、FIQ、Supervisor、Abort、IRQ、Undefinedの状態で命令の挙動が変わったり、レジスタの値が変わる。
(モード自体は、CPSRの下位5bitで決まり、この値は後で実装する例外発生時に描き変わる)

R8~R12はFIQモードかそれ以外のモードの二つで値がバンクされる。
R13, R14, SPSRは全てのモードで値がバンクされる。(ただし、SystemとUserは同じ扱い)

なので、入出力だけ定義したtrait Registerを用意し、

  1. CommonRegister(全てのモードで共通)
  2. FiqRegister(Fiqかそうじゃないかでバンク)
  3. ModeRegister(モードごとにバンク)

でレジスタ構造体を分けた。

struct Cpu {
    common_r: [CommonRegister; 8], // R0~R7
    fiq_r: [FiqRegister; 5], // R8~R12
    mode_r: [ModeRegister; 2], // R13, R14

    pc: u32, // R15(簡単のためにu32)
    cpsr: Psr, // CPSR
    spsr: ModeRegister<Psr>, // SPSR
}
mjhdmjhd

CPSR
状態レジスタCPSRは、

N: Negative
Z: Zero
C: Carry
V: Overflow
I: IRQ Disable
F: FIQ Disable
T: State(0=ARM, 1=THUMB)
Mode(5bit): User, System, ...

で構成される。

今後もたびたび出てくるけど、FIQはGBAでは使われてないらしいのであんまり気にしなくてよさそう。

mjhdmjhd

ARM命令
数は少ないけど、一個一個が複雑でヘビー。
やるしかないのでやる。

CPSRの値によって、実行するかしないかを決める4bitが先頭についてるものが多い。共通化チャンス
https://rust-console.github.io/gbatek-gbaonly/#armcpuflagsconditionfieldcond

Op2という第二オペランドがたびたび出てくるが、これが厄介。
ALU命令などのSourceを指定するオペランドで、即値/レジスタ値をビットシフト(LSL, LSR, ASR, ROR)した値を計算に使うことができる欲張りセット。
ここでテストROMがめちゃくちゃ落ちた。

  1. ゼロシフトがある(シフト値が0の時の挙動)
  2. 即値の場合とレジスタ値の場合で若干挙動変わる
  3. Cフラグが更新されたりされなかったりする

最終的にmGBAの実装を見てだいぶ参考にした。

PC(R15)がオペランドに指定された場合も特殊な挙動があるので注意。

mjhdmjhd

THUMB命令
THUMBは割とシンプル。命令長は16bitだけどレジスタや各種演算は引き続き32bitなので注意。
THUMBNAIL的に、あくまで32bit命令のショートカットなイメージ?

ハマった点は、

  1. シフト系命令が、ARM命令のOp2と同じ挙動をするので共通化しとかないと辛い

他はシンプルなので特につまらなかった(今のところ)

mjhdmjhd

ARMのLDR系命令の罠として、misaligned(4byteの読み込みなのに、4で割り切れないアドレスなど)の場合は、ズレた分だけRORする

https://github.com/mgba-emu/mgba/blob/2ac6920238d4c705e6cad0729c90d778247e972e/src/gba/memory.c#L634
https://github.com/jsmolka/gba-tests/blob/master/arm/single_transfer.asm#L81

ただ、本来は未定義の動作なので自分のエミュレータではテストROMをスキップする(多分大丈夫でしょう…)

mjhdmjhd

CPUタイミング

消費サイクル数は基本、N, S, Iの三つで表されて
N: non-sequential メモリアクセス (アクセス先と、WAITCNTの値、ビット幅でサイクル数が変わる)
S: sequential メモリアクセス ( 〃 )
I: 固定で1サイクル消費

バスに浪費するサイクル数持たせた方が楽かもしれない