ファミコンエミュレータ作成
CPUのエミュレーションのために、以下の内容を確認する
- 実機で使用されている素子
- CPU
- レジスタ
- メモリ
CPU
- CPUが見える範囲はどこまで?
- メモリ・レジスタ・カセットの3つ?
- 実行するのはどこになる?
- レジスタのPCの位置からオペコード・オペランドを読み取る?
レジスタ
- 各レジスタはCPUの演算結果に応じて更新され続けるイメージ?
- 各レジスタの使用用途
メモリ
- RAMは2KiBのWRAMを使用する
- 2KiB = 2 * 1024 (B) * 8 (bit)
- ROMはカセット中に存在し、CPUから直接読み取ることができる?
参考
原始的なCPUの処理サイクルとしては、以下の通り?
- 起動
- 初期化処理(PCに$80を設定・その他レジスタの初期化)
- 実行開始
実行
- CPUクロックごとにPCのオペコードを実行する(PC+1)
- オペコードに応じて、オペランドの読み取りも行う(PC+1またはPC+2)
- クロックタイミングで実行中かどうかはどうやって判別している?というかする必要がある?
オペコード・オペランドの実行を実装していくイメージ
オペコードについて
- 8bitで表される
- 上位4bitで命令の種類
- 下位4bitでアドレッシングモード
アドレッシングモード
- オペランドをどのようにメモリから引っ張ってくるかを決定している
- オペコード
0xA5
の場合、LDA
/Zero page addressing
となる-
LDA
:Aレジスタに値をロードする -
Zero page addressing
:$0000 - $00FFのいずれかから値を取得する。オペランドが1つ必要- この場合、オペランドはPC+1の番地から持ってくる
- 処理後、PCにはPC+2の番地が格納されている(たぶん)
-
オペコード・オペランドは順番に格納されている
カセット
以下のHello, world!カセットの読み取り・表示までいきたい
- カセットはプログラムROM・キャラクタROMに分かれてデータが格納されている
- プログラムROMはCPU側から
0x8000-0xBFFF
・0xC000-0xFFFF
の範囲で参照ができる?
カセット・CPU・WRAMは8bitのバスで接続されている。
→1クロックごとに8bit単位でリードが可能?
.nes
ファイルヘッダーの詳細
ROM分割・メモリマップ構成まで先にやっといた方がよさげ
→CPUから見える部分はとりあえずOKとした
PPU
グラフィック表示を行うチップ
PPUは自身にレジスタを持つ
VRAMはPPUがアクセスするRAM領域
CPUとPPUの連携
CPUがVRAMにアクセス・書き込みする場合はPPUのレジスタを介して行う
CPUから見ると、0x2000
~0x2007
に配置されている
→アクセス先がPPUレジスタであることに注意する
PPUの仕組み
3種類のテーブル・2種類のパレットを持ち、それを元に画面表示を行う
PPUはディスプレイへ直接表示する(実機であれば、ブラウン管モニターへの出力)
テーブル
ネームテーブル
タイル : 8x8pxの単位 スプライトの最小表示範囲?
256x240pxの画面領域に対して、32x30の背景タイルが並べられる
この背景タイルをどのように配置するかを決定するテーブル
→ネームテーブルの領域が960byteである理由
画面上の対応するタイルの番地に、スプライト番号を書き込んで画面表示をする?
スプライトはCHR-ROMに格納されている
→CPUからアクセス可能 というかCPUからVRAMに転送してやる必要がある
属性テーブル
ブロック : 2x2タイルの単位 パレット適用の最小単位
1ブロックに対して、どのパレットを適用するかを決定する
適用するパレットは2bitで表される→4つのパレットのうち、どれを使用するかを決定する
属性テーブルは画面左上のブロックから、順序どおりに配置される
グラフィック生成方式の解説
ソフト開発視点でのネームテーブル・属性テーブルの関連
パレット
ブロックに適用されるパレットを設定できる
バックグラウンドパレット
0x3F00
~0x3F0F
まで、4バイトx4で16バイトの領域を持つ
最初の4バイトのセット以外は先頭バイトが無視される(背景色が使用される)
スプライトパレット
0x3F10
~0x3F1F
まで、4バイトx4で16バイトの領域を持つ
各セットの先頭の値は背景色として使用される
パレットに書き込む値と、実際の色の関係
https://www.nesdev.org/wiki/PPU_palettes#:~:text=copy protection measure.-,Palettes,-The 2C02 (NTSC
エミュレータでは、それぞれの値に対応した色のドットを表示する
PPUはCPUと独立して、非同期で動作している
PPUはCPUの1/3のクロックが入力されている
- PPUはVRAMの内容を常に表示している?
VRAMへは誰が書き込んでいる?PPUまたはCPUのどっち?
2つの書き込み方法がある。
CPU→VRAM
CPUからPPUレジスタ経由での書き込みが可能。
- PPUのPPUADDRに2バイトでVRAMアドレスを書き込む
- PPUのPPUDATAに1バイトのデータを書き込む
上記流れを繰り返す。
PPUADDRに書き込まれているのが1回目なのか2回目なのかは内部のxレジスタで保持している(らしい)
PPUSTATUSのリード時にクリアされる。
→クリアするためにPPUSATUSを参照する必要がある?
→xレジスタが1bitなら特になにもしなくてよさそう
CHR-ROM→VRAM
DMA機能があり、スプライトを指定のVRAMアドレスに一撃で転送できる?要調査
クラスくちゃくちゃ...整理するかこのまま突っ込むかを決める
データバスクラス
-
バスは書き込み先のみ見えてればOK
-
IBus
みたいな感じで分離しておく - 読み取り・書き込み先デバイスの参照を保持する。バスは1方向のみを表現する。
- 双方向でのデータやり取りはそれぞれのインスタンスを作成する
- バスクラスの責務は
メモリマップの変換、書き込み先のwrite/fetchメソッドの呼び出し
-
-
write(uint16_t address,uint8_t data)
を持つ- バス書き込み先のアドレスは書き込み元のメモリマップとする
- 書き込み先のアドレスは、書き込み先メモリの絶対アドレスとする(CPU/PPUのメモリマップとしない)
- バス書き込み先のアドレスは書き込み元のメモリマップとする
-
uint8_t fetch(uint16_t address)
を持つ- バス読み込み先のアドレスは読み出し元のメモリマップとする
- 読み込み先のアドレスは、読み込み先メモリの絶対アドレスとする
-
各デバイスからは、以下のように見える
- バスからのデータ読み出し・バスへのデータ書き込みが可能
バスに指定するアドレスは自身のメモリマップでOK(CPUから0x2000を指定すると、PPUレジスタへアクセスできる)- メモリマップはデバイスが管理する。バスは指定された絶対アドレスに対しての操作を提供する。
-
バスが接続されるデバイスは
IBusAccessable
のようなインターフェースを持つ- ここでwrite/fetchを実装する。引数で指定されるのはデバイスの絶対アドレスとする
- 書き込み禁止・読み込み禁止は例外をスローすることで表現する
命名規則
- publicなものはパスカルケースにする
- privateなものは
_
を先頭につけ、キャメルケースにする
名前空間
グローバルに書きまくっているのをやめろ
-
Hardware
以下にハード関連のクラスを格納する-
Hardware
以下のものはHardware
同士でしか依存しない
-
-
Emulator
以下に、エミュレータ特有の処理を格納する- ハードウェアの状態を参照して画面に出力する、など
-
Logging
以下に、ログ出力周りの処理を格納する
前方参照
意味が分からないからやめろ
- ヘッダーファイルでは前方参照を積極的に行う。
→#include
を多重に行うとおかしくなるため - ソースファイルでは前方参照しない
- そのファイルのヘッダーと、使用するライブラリのインクルードで十分なはず
迷ったらとりあえずGoogleのコーディング規約を確認してみる
C#が恋しい...