用語
- waitstates:
- メモリアクセスの完了を待つためにCPUがストールしている状態、または、そのときに消費されるクロックサイクル数を指す
- メモリの種類やアクセスの種類によって消費される時間が異なる
- クロックサイクル:
- 電子回路同士が協調動作を行うために基準とする信号(クロック信号)の1周期分の時間
- 単にサイクルとも呼ばれる
- AGBでは、1サイクルあたり約59.99ns
命令
サイクルの内訳
- N = (1 + waitstates)サイクル
- ランダムメモリアクセスにかかるサイクル
- S = (1 + waitstates)サイクル
- シーケンシャルメモリアクセスにかかるサイクル
- I = 1サイクル
- メモリ待ちでないCPUビジー状態のサイクル
- C
- コプロセッサとの通信でCPUがデータバスを使っているサイクル
- AGBはARM特有のコプロセッサを持たないので、考えなくて良い
Tips
- 乗算命令は被乗数の内容によって内部サイクル数が増減する
- 最上位からALL0やALL1の連続するバイトをスキップするため
- アクセスタイミングの異なるメモリ範囲にジャンプするときは移動先のメモリ特性に従う
- ただし、ThumbのBL命令は1Sだけ移動元で実行する
メモリ
仕様
- EWRAM:
- 256KiB
- 16bitバス幅
- 読み書き3サイクル
- IWRAM:
- 32KiB
- 32bitバス幅
- 読み書き1サイクル
- Palette RAM:
- 1KiB
- 16bitバス幅
- 読み書き1サイクル
- 8bit書き込み不可
- Blank以外では、1サイクル追加でかかる
- VRAM:
- 96KiB
- 16bitバス幅
- 読み書き1サイクル
- 8bit書き込み不可
- Blank以外では、1サイクル追加でかかる
- OAM:
- 1KiB
- 32bitバス幅
- 読み書き1サイクル
- 8bit書き込み不可
- Blank以外では、1サイクル追加でかかる
- Game Pak ROM:
- 最大32MiB
- 16bitバス幅
- N=5サイクル、S=3サイクル(変更可能)
- 16bit命令Prefetch機能がある
- Game Pak RAM:
- 最大64KiB
- 8bitバス幅
- 読み書き5サイクル(変更可能)
Tips
- IWRAMの末尾256バイトはシステムで予約されている
- 割り込みハンドラへのポインタ、ソフトリセットかどうかフラグ、BIOS用IFフラグ、サウンドバッファへのポインタ、スタック領域がある
- 文書外の仕様だが、EWRAMをオーバークロックできる、らしい
- オリジナルとSPのみ、MicroやDSでは機能しない
- クラッシュする可能性があるので、製品では使われていない
- VRAMにもプログラムを配置して実行することができる
- IWRAMのほうが良いが、最後の選択肢としてあり
- 海賊版対策で使われた位なので、エミュレータの実装によっては機能しないこともある
- VRAMなどへのアクセスはBlank中に行うほうが良い
- 表示システムがアクセス中だと、CPU側に1サイクルが追加されるため
- Game Pak ROMには性能に違いがある
- アクセスタイミングは
WAITCNTレジスタで制御される- デフォルトだと、N=5サイクル、S=3サイクルでプリフェッチ無効
- 公式製品だと、N=4サイクル、S=2サイクルでプリフェッチ有効、が多いらしい
- 高速な分だけ電力消費は増える
- 非公式Flash ROMの場合、高速設定にすると不安定になる場合がある
- アクセスタイミングは
- Game Pak ROMは128KiBブロックを超えてシーケンシャル読み書きできない
- ブロックをまたいだアクセスはランダム読み書きとして扱われる
- Game Pak ROMはバス幅が16ビットなので、サイズ的にThumb命令のほうが適している
- ARM命令はIWRAMにコピーしてから実行するほうが良い
- Game Pak ROMでは命令プリフェッチ機能が有効化できる
- カートリッジにアクセスしていない間、後続のプログラムの先読みを行う
- Iサイクル、または、カートリッジ以外でのNおよびSサイクルの間
- 16ビット値8つ分のバッファがある
- カートリッジにアクセスしていない間、後続のプログラムの先読みを行う
- Game Pak ROMのアドレスは3つ分割り当てられている
- 0番(0x80 - 0x9F)、1番(0xA0 - 0xBF)、2番(0xC0 - 0xDF)でそれぞれ異なるwaitstatesを設定できる
- 0番は通常の読み込み専用チップで使われる
- ブート時のエントリポイントが0x80000000にあるので
- 2番はセーブデータ用の読み書き可能なチップで使われる
- セーブデータ用ROM/RAMには種類がある
- SRAM:概ね32KiB、最大64KiB、ボタン電池で維持される
- Flash ROM:概ね64KiB、書換回数に制限がある(10,000回程度)
- EEPROM:4KiBや64KiB、書換回数に制限がある(100,000回程度)
- SRAMの最初と最後のバイトは使わないほうが良い
- 電源のON/OFFやカートリッジの抜き差しで壊れたりするので
ディスプレイ
- VRAMはBGとOBJ合わせて96KB
- VRAMは2バイト単位か4バイト単位で書き込む必要がある
- 1バイト書き込み不可のため
IO
DISPCNTの強制BlankビットをONにすると、スキャンラインが白で描画されるが、VRAMにいつでもアクセスできるようになる
タイル
- タイルの構造はBGとOBJで共通
- 4bppタイルは、タイルあたり8x8pxで32B、1024個で32KB
- 8bppタイルは、タイルあたり8x8pxで64B、256個で16KB、1024個で64KB
BG
- Mode 0,1,2の場合:
- BG0とBG1のマップは、タイルあたり2B、32x32で2KB、64x64で8KB
- BG2,3のマップは、タイルあたり1B、64x64で4KB、128x128で16KB
- Mode 3の場合、16bppのフレームバッファが240x160で80KB
- Mode 4の場合、8bppのフレームバッファが240x160で40KB、2枚で80KB
- Mode 5の場合、16bppのフレームバッファが160x128で40KB、2枚で80KB
BG#CNTでタイルデータの先頭アドレスを16KB単位で指定するBG#CNTでマップデータの先頭アドレスを2KB単位で指定する
OBJ
- BGがMode 0,1,2なら32KBをOBJタイル用に使える
- BGがMode 3,4,5なら16KBをOBJタイル用に使える
- タイルの構造はBGと同じだが、使用する領域は末尾のみで固定
- 0番のOBJが一番手前に描画される
- 基準点はOBJの左上だが、アフィン変換の中心はOBJの真ん中(w/2,h/2)
- アフィン変換されたOBJは元の矩形でクリッピングされる
- 2Dマッピング方式はVRAMを256x256pxの二次元とみなす
OAM
- 1つあたり6B、8B間隔でOBJ128個分ある
- 残りの2Bには、32個分のアフィン変換行列が分割して格納される
- H-Blank中にOAMにアクセスするには
DISPCNTの所定のフラグを立てる必要がある
Palette
- 2bppの256色で、BGとOBJ用にそれぞれ512B
- 4bppタイルは16色ごとのひとまとまりで参照する
- 0番の色は透過色として扱われる
- 0番のBGパレットの0番の色は最背面の色として、すべてが透過色だったときに使われる
ウィンドウ
- 矩形を2つ作り、内側と外側でBGとOBJのそれぞれどの種類を表示するか、ブレンディングを適用するかを選択する
- OBJをウィンドウモードにすると、そのOBJは透過色を外側とするウィンドウとして扱われる
- この場合、表示の任意選択はできない?
モザイク
- ピクセルが指定の縦横のサイズごとにまとめられ、その領域を左上のピクセルの色で塗りつぶす
色の特殊効果
- ブレンディングか明暗転のいずれかを可能
- ブレンディングは1回だけ行われ、2つの色のそれぞれに指定の要素の中から最前面にある色が選ばれる
- OBJを半透明モードにすると、マスタフラグに関わらず、最前面のOBJの色を第一色としてブレンディングが行われる
Tips
- “表示期間中に内部HV同期カウンタが強制Blankを解除する”と、表示が再開されるまでスキャンライン2つ分かかる
- H-Blank中にBGモードを切り替えると、有効化するまでスキャンライン3つ分くらいかかる
- 非表示のOBJはOAM上で後ろの方にまとめて配置する
- 優先度の高いOBJは非表示であっても後続のOBJに影響を与えるため
- 非表示のOBJは「8x8、アフィン変換OFF」にするとオーバーヘッドを小さくできる
- 縦幅128のOBJを
Y>128に配置すると、画面上部からハミ出てしまう- Y座標が8ビットで表現され、画面下の余白が96px (= 256 - 160) しかないため
- 後続のOBJが先行のOBJより高い対BG優先度を持つと描画がおかしくなる
- 実機だと、通常の照明条件下では、色の値が14まではほぼ黒に見える
- ピクセル1つを描画するのに4サイクルかかる
- 実機だと、通常の照明条件下では、画面上部8ライン分が非常に見えづらいため、重要な情報の表示には使わないほうが良い
- 実機は、偶数奇数ラインがフレームごとに交互に減光する、インターレース的な動作が行われる
- DSだと、約3秒ごとにフレームスキップ的なフレームの欠落(または挿入)が発生するので、インターレース対策をしたゲームでは同期ズレが起こる
サウンド
(ライブラリにお任せするので省略)
タイマー
- 0番と1番はサウンドでサンプルレートを指定するのに使う
- 16ビットのカウンタとして、指定の値に達する(overflowする)ときに割込を発生させられる
- カウントアップ方法は1,64,256,1024サイクルに1回のいずれかを指定できる
- 手前のタイマーがoverflowしたときにカウントアップするモードもある
DMA
- アサインされたチャンネルが複数あれば、若い番号のものから優先して処理される
- DMA0は、最優先で処理されるので、HDMAなどのタイミング・クリティカルな処理に使う
- DMA1,2をサウンドで使う都合上、数十msのような?長すぎる処理に使えない
- DMA1,2は、サウンドFIFOとの協調動作モードがあるので、サウンドストリーミングで使う
- もしくは、優先度の高い汎用用途で使う
- DMA3は、他の邪魔をしないので、優先度の低い汎用的な処理に使う
- Game Pak側から転送要求を出せるモードもあるらしいが、詳細不明
- GamePak ROMがソースのとき、ソースアドレス固定モードは禁止されている
Tips
- 転送中は、DMAがバスを専有するので、CPUが停止する
- 割り込みは転送中に発生せず、完了後に行われる
- DMAが速いのはCPUを必要としないからであって、読み書きは通常のサイクル数で行われる
- 転送処理はIOレジスタに書き込んで数(4?)サイクル経ってから動き始める
- 一度に転送できるワード数の最大値は、DMA0-2で16K個、DMA3で64K個
キー入力
- 通常は、フレームごとにIOレジスタを読み出して取得する
- 割込は低消費電力モードで待機するときに使う
割込み
(ツールチェインにお任せするので省略)
Game Pak
- カートリッジがささっていないことは割込で検知できる
- 割込中に処理を止めるなどして、カートリッジが抜けててもクラッシュさせないようにすると良い
- 割込みハンドラと必須データをIWRAMに置くかして、ROMに依存しないようにする
ROM
(ROMヘッダはツールチェインにお任せするので省略)
バックアップRAM
- バックアップRAMは実際の話なので省略
- SRAM:
- EEPROM:
- Flash ROM:
GPIO
- 4ビットのGPIOを備える
- RTC
- センサー(太陽光、チルト、ジャイロ)
- 振動
e-Reader
- 最古のDLC?
不測の事態
- BIOSのメモリは外から読み出せない
- プログラムカウンタがBIOSのメモリアドレスの中にないとダメ
- 不正なアドレスを読み出そうとすると変な値が返ってくる
- 対象となるのは、未使用のメモリアドレス、読み込み不可のIOポート、ささっていないカートリッジ、など
- すでにデータバスに入っている値を読み出すことになるが、プリフェッチやらパイプラインやら割込みやらで頻繁に変化するので、実際に何が返ってくるかは良くわからない
- VRAMは8ビットで書き込みできない
- 無理に書き込んでも、なにも書き換わらなかったり、16ビット分書き換わったりする
- BGのタイル番号が範囲外を指せたりする
- 何か写ったりするし、何も映らなかったりする
BIOS関数
- 算術:32ビット除算、16ビット平方根、16ビットarctan
- アフィン変換:BG用、OBJ用
- 展開:ビットのアンパック、デルタエンコーディング、ハフマン符号、LZ77、ランレングス
- コピー&フィル:32バイト単位の高速コピー、4バイトまたは2バイト単位のコピー
- 停止:任意割込み待ち、指定割込み待ち、V-Blank待ち、省電力待機
- リセット:ソフトリセット、メモリクリア(、ハードリセット)
- その他:(BIOSチェックサム計算、)マルチブートプログラム転送
- サウンド:MIDI KEY周波数変換、バイアス設定、チャンネル停止、ドライバ初期化、ドライバ更新、ドライバモード設定、更新、更新停止、更新再開(、何かしらx4、関数リスト取得)
プログラミング
Tips
- GCCの属性でコンパイルオプションを細かく制御できる
- 配置先のセクションを指定:
attribute((section("..."))) - ARMステートでコンパイル:
attribute((target("arm")))
- 配置先のセクションを指定:
long_call属性を関数につけると、RAMに置いた関数をROMから呼び出せるようになる- ROMからRAMへのジャンプ距離が64MiBを超えるので
long_callが必要
- ROMからRAMへのジャンプ距離が64MiBを超えるので
- コンパイルオプション
-mthumbで常にThumbステートでコンパイルさせるほうがおそらく良い- ARMステート用にコンパイルしたい関数にだけ属性をつける
- グローバル変数は安易に使わず、頻繁に読み書きする共有メモリとしてのみ使う
- グローバル変数はIWRAMに配置されるため、あまり多くを使えない
- コールドデータ用の大きな配列は、EWRAMに配置するか、動的割当する
- ハードウェア除算器がないので、割り算はソフトウェア実装に置き換わる
- これは安全だが比較的遅いので、独自の実装を使うと良い
- BIOSに除算関数があるのでそれを使うか、自前で高速な実装をIWRAMに用意して使う
- 2の倍数であれば、シフト演算やAND演算を活用する
- 近似で良ければ、固定小数点で逆数をかける
- 扱う値の範囲が狭ければ、テーブル化するのもあり
- 定数での除算なら、コンパイラがよしなにしてくれることもある
- GCCにはリンカで関数をラップする方法があるので、デフォルトを差し替えることも可能
-wrap=funcオプションを使うと、funcが__wrap_funcに置き換わり、オリジナルを__real_funcで呼び出せるようになる- 参考1:https://stackoverflow.com/questions/31404831/how-to-override-functions-from-stdlib
- 参考2:http://blog.kmckk.com/archives/5436026.html
- これは安全だが比較的遅いので、独自の実装を使うと良い
- コンパイル時以外で浮動小数点を使わない
- ハードウェア演算器がなくソフトウェア実装となるので、非常に遅い
- math関数はテーブル化すると良い
- 三角関数は、その性質から、のの範囲だけで済む
- ループ中の関数呼び出しコストを軽く見ないほうが良い
- 関数呼び出しには、スタック操作やCPUパイプラインのフラッシュで実行コストがかかる
- 呼び出し回数の多い短い関数はインライン化するとコスト削減のメリットが得られやすい
gba-toolchainのリンカスクリプト
- EWRAMに配置されるセクション:
.ewram、.sbss.ewramは関数や初期値ありグローバル変数が置かれる.sbssは初期値なしグローバル変数が置かれ、起動時にゼロ初期化される- 以降の領域はヒープメモリとして使われる
- IWRAMに配置されるセクション:
.iwram、.bss.iwramは関数や初期値ありグローバル変数が置かれる.bssは初期値なしグローバル変数が置かれ、起動時にゼロ初期化される
参考資料
- https://rust-console.github.io/gbatek-gbaonly/
- https://mgba.io/2015/06/27/cycle-counting-prefetch/
- https://mgba.io/2018/03/09/holy-grail-bugs-revisited/
- https://web.archive.org/web/20210212025145/http://www.devrs.com/gba/files/gbadevfaqs.php
- https://web.archive.org/web/20220925104529/https://forum.gbadev.org/viewtopic.php?t=418