GBAエミュレータ開発記
GBAエミュレータの開発記録を残していくぞ〜
参考資料たち
CPUやメモリレイアウトなど
- GBATEK(no$gba): http://problemkaputt.de/gbatek.htm
- GBA-only, 軽いのでこっちの方が良いかも: https://rust-console.github.io/gbatek-gbaonly/
グラフィック周り
参考にするOSS実装
- mGBA(C++): https://github.com/mgba-emu/mgba
- とにかくマクロで真っ黒な実装
- ARM系:
isa-arm.c
- THUMB系:
isa-thumb.c
- magia(Go): https://github.com/pokemium/magia
- RustBoyAdvance-NG(Rust): https://github.com/michelhe/rustboyadvance-ng
ROM吸い出し
メルカリで中古のゲームを買う
- くるくるくるりん
- ロックマンEXE3
- 黄金の太陽 開かれし封印
を準備した!懐かしい
ROM吸い出し機を購入
CUBIC STYLEさんのラズパイにGBAカードリッジさせるアダプタ
ラズパイは Raspberry Pi Zero W を購入。めちゃちっちゃい
まずはROMのデコード。
GBAはGBやNESのようにカードリッジ内にマッパーがないので、かなりシンプルに書けた
配列に吸い出したROMのデータを取り込むだけ〜
次にある程度の構成を作った
- CPU: 命令の実行
- PPU: グラフィックス
- BUS: データと番地の割り当て
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
}
といった分岐をする感じ…
レジスタ
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
を用意し、
- CommonRegister(全てのモードで共通)
- FiqRegister(Fiqかそうじゃないかでバンク)
- 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
}
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では使われてないらしいのであんまり気にしなくてよさそう。
ARM命令
数は少ないけど、一個一個が複雑でヘビー。
やるしかないのでやる。
CPSRの値によって、実行するかしないかを決める4bitが先頭についてるものが多い。共通化チャンス
Op2という第二オペランドがたびたび出てくるが、これが厄介。
ALU命令などのSourceを指定するオペランドで、即値/レジスタ値をビットシフト(LSL, LSR, ASR, ROR)した値を計算に使うことができる欲張りセット。
ここでテストROMがめちゃくちゃ落ちた。
- ゼロシフトがある(シフト値が0の時の挙動)
- 即値の場合とレジスタ値の場合で若干挙動変わる
- Cフラグが更新されたりされなかったりする
最終的にmGBAの実装を見てだいぶ参考にした。
PC(R15)がオペランドに指定された場合も特殊な挙動があるので注意。
THUMB命令
THUMBは割とシンプル。命令長は16bitだけどレジスタや各種演算は引き続き32bitなので注意。
THUMBNAIL的に、あくまで32bit命令のショートカットなイメージ?
ハマった点は、
- シフト系命令が、ARM命令のOp2と同じ挙動をするので共通化しとかないと辛い
他はシンプルなので特につまらなかった(今のところ)
ARMのLDR系命令の罠として、misaligned(4byteの読み込みなのに、4で割り切れないアドレスなど)の場合は、ズレた分だけRORする。
ただ、本来は未定義の動作なので自分のエミュレータではテストROMをスキップする(多分大丈夫でしょう…)
CPUタイミング
消費サイクル数は基本、N, S, Iの三つで表されて
N: non-sequential メモリアクセス (アクセス先と、WAITCNTの値、ビット幅でサイクル数が変わる)
S: sequential メモリアクセス ( 〃 )
I: 固定で1サイクル消費
バスに浪費するサイクル数持たせた方が楽かもしれない