Open8

ファミコンエミュレータ作成

やねいもやねいも

CPUのエミュレーションのために、以下の内容を確認する

  • 実機で使用されている素子
    • CPU
    • レジスタ
    • メモリ

CPU

  • CPUが見える範囲はどこまで?
    • メモリ・レジスタ・カセットの3つ?
  • 実行するのはどこになる?
    • レジスタのPCの位置からオペコード・オペランドを読み取る?

レジスタ

  • 各レジスタはCPUの演算結果に応じて更新され続けるイメージ?
  • 各レジスタの使用用途

メモリ

  • RAMは2KiBのWRAMを使用する
    • 2KiB = 2 * 1024 (B) * 8 (bit)
  • ROMはカセット中に存在し、CPUから直接読み取ることができる?

参考

https://qiita.com/bokuweb/items/1575337bef44ae82f4d3

やねいもやねいも

原始的なCPUの処理サイクルとしては、以下の通り?

  1. 起動
  2. 初期化処理(PCに$80を設定・その他レジスタの初期化)
  3. 実行開始

実行

  • 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の番地が格納されている(たぶん)

オペコード・オペランドは順番に格納されている

https://kikb.web.fc2.com/6502/index.html

やねいもやねいも

カセット

以下のHello, world!カセットの読み取り・表示までいきたい
https://hp.vector.co.jp/authors/VA042397/nes/sample.html

  • カセットはプログラムROM・キャラクタROMに分かれてデータが格納されている
  • プログラムROMはCPU側から0x8000-0xBFFF0xC000-0xFFFFの範囲で参照ができる?

カセット・CPU・WRAMは8bitのバスで接続されている。
→1クロックごとに8bit単位でリードが可能?

やねいもやねいも

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つのパレットのうち、どれを使用するかを決定する
属性テーブルは画面左上のブロックから、順序どおりに配置される

グラフィック生成方式の解説
https://postd.cc/nes-graphics-part-1/

ソフト開発視点でのネームテーブル・属性テーブルの関連
http://gikofami.fc2web.com/nes/nes011.html

パレット

ブロックに適用されるパレットを設定できる

バックグラウンドパレット

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レジスタ経由での書き込みが可能。

  1. PPUのPPUADDRに2バイトでVRAMアドレスを書き込む
  2. 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以下に、ログ出力周りの処理を格納する

前方参照

意味が分からないからやめろ

迷ったらとりあえずGoogleのコーディング規約を確認してみる
https://ttsuki.github.io/styleguide/cppguide.ja.html