今回は 6502 CPU を実装していきます。
CPU の詳細については参考にしたページがわかりやすいのでそちらを参照してください。
ここでは実際のコードがどのように実装されているかを説明していきます。
レジスタは CPU.swfit で以下のように定義されています。
var PC: Word = 0x0000 var SP: Byte = 0xFD var A: Byte = 0x00 var X: Byte = 0x00 var Y: Byte = 0x00 var P: Byte = 0x24
スタックポインタ(SP)は Byteで定義され、実際にアクセスする場合には上位 8ビットが 0x01 となり 16ビットのアドレスになります。
状態レジスタの P は各状態とビットの対応が struft F で定義されています。
struct F { static let CARRY = 0 static let ZERO = 1 static let INTERRUPT = 2 static let DECIMAL = 3 static let BREAK = 4 static let RESERVED = 5 static let OVERFLOW = 6 static let NEGATIVE = 7 }
これと UInt8+Bit.swift で定義した添字によるアクセスを使って各状態にアクセスします。
例えばキャリーフラグをONにする場合は以下のようにできます。
P[F.CARRY] = true
他に定義されているプロパティは、条件ジャンプ命令でジャンプの有無を判断する hasBranched です。
これは BNE などの条件ジャンプ命令でジャンプが発生した場合にサイクル数が +1 されるので、それを確認するためのフラグです。
後は CPU_BUS と Interrupts ですが、これはメモリと割り込みの状態を他デバイスとやり取りするために CPU オブジェクト作成時に渡されます。
参考にしたページの「実装イメージ」の項で説明されている繰り返す手順は以下の通りです。
- PC(プログラムカウンタ)からオペコードをフェッチ(PCをインクリメント)
- 命令とアドレッシング・モードを判明
- (必要であれば)オペランドをフェッチ(PCをインクリメント)
- (必要であれば)演算対象となるアドレスを算出
- 命令を実行
- 1に戻る
この手順の 1-5 が 1命令の実行となり、動作し続ける限りこれを繰り返します。
実際のコードの中でどのように記述されているのかを順に見ていきます。
CPU の 1命令の実行は関数 run で行われており、これは CPU.swift で実装されています。
この run は命令を 1つ実行してそれにかかったサイクル数を返す関数です。返り値のサイクル数は CPU(とその他のデバイス)の実行スピードを制御するのに使われます。ちなみに CPU が 1サイクル動作する間に PPU は 3サイクル動作します。
ところで、実は関数 run の中で最初に行うのは割り込みのチェックです。先の手順にはありませんが、6502 は命令実行の前に割り込みのチェックを行います。今回の実装では割り込みの通知は Interrupts を通じて行われるので、これを確認して割り込みがあった場合にはそれぞれの割り込み処理を行います。
if interrupts.isNMIAssert() { processNMI() } if interrupts.isIRQAssert() { processIRQ() }
次に行われるのが先の手順 1-5 です。
以下の 12行分のコードになります。
let code: Byte = fetch(PC) if let opcode = OpCode.opcodeTable { if let modeName = opcode["mode"] as? String, let mode = AddressingMode(rawValue: modeName), let baseName = opcode["baseName"] as? String, let cycle = opcode["cycle"] as? Int { let (addrOrData, additionalCycle) = getAddrOrDataWithAdditionalCycle(mode) execInstruction(baseName, addrOrData, mode) return cycle + additionalCycle + (hasBranched ? 1 : 0) } }
まず 1行目で手順 1の命令のフェッチを fetch 関数で行なっています。これは前回実装した CPU_BUS を介して指定アドレスにアクセスしていて内部で PC も +1 されます。
命令をフェッチしたら 2の「命令とアドレッシング・モードを判明」を行います。
これに使うのが OpCode.swift の opcodeTable です。
この辞書は Dictionary< Byte: Dictionary< String, Any>> と定義されています。
[Byte: [String: Any]] = [169: ["fullName": "LDA_IMM", "baseName": "LDA", "mode": "immediate", "cycle": 2], ...
外側の辞書の Byte の部分は OpCode で、取り出せる辞書には "baseName", "mode", "cycle",(使わないけど "fullName" も)がそれぞれ String, String, Int で入っており、これが各命令(OpCode)の名前とアドレッシングモード、そしてかかるサイクル数となります。
これで手順 1でフェッチした命令から opcodeTable を使って命令の名前とアドレッシングモード、サイクル数が取得できました。
次に 3, 4の「(必要であれば)オペランドをフェッチ」「(必要であれば)演算対象となるアドレスを算出」を行います。
これは関数 getAddrOrDataWithAdditionalCycle で行い、別ファイル CPU+AddressingMode.swift に実装されています。
この関数では命令の "mode"(アドレッシング・モード)に対応したデータもしくはアドレス、そしてその命令を実行する際に余計にかかるサイクル数を返します。(アドレッシングモードが accumulator や implied の場合は何もしません(0, 0 を返します))
例えば Aレジスタに値を入れる LDA の場合、アドレッシング・モードが immediate ならオペランドをフェッチしてそれを A レジスタに入れるデータとして返します。これが absoluteX の場合には、オペランドをフェッチしてそれに X レジスタの値を加えた値がアドレスとなりこれを返します。
additinalCycle はページを跨いだアクセスやジャンプを行なった場合にサイクル数が +1 されるので、それを返す値です。
例えば absoluteX の場合、オペランドで示された値が 0x03F0 で X の値が 0x30 だとすると、参照されるアドレスは 0x0420 になります。X を加える前の 0x03F0 と X を加えた後の 0x0420 ではページの値が 0x03 と 0x04 で違うので、サイクル数が +1 されます。
let additionalCycle = addr.page != (addr &+ Address(X)).page ? 1 : 0
最後に 5の命令の実行ですが、これは execInstrunction 関数として CPU+Instruction.swift に実装されています。
2, 3, 4, で求めた命令の名前、データまたはアドレス、アドレッシング・モードの 3つを引数として取り、命令の名前で switch してそれぞれの処理を指定されたアドレッシング・モードでデータまたはアドレスを使って行います。
これはもうただひたすら命令が何をするかを実装しているだけです。単純にレジスタに値を入れるものから、キャリーを考慮した足し算など様々です。6502 はシンプルで命令数が少ない CPU ですが、それでも結構な数になります。
また、ここではフラグのオン/オフを操作する処理が多いので、Negative と Zero フラグについては別関数を用意してそこで操作するようにしてあります。
ちなみに、動作をきちんと移植できているかどうか心配だったのでテストコードも書いてみましたが、これまたテストがきちんと書けているか怪しいので本当に正しい動作をしているかどうかは微妙です。
以上で CPU の実装と動作の説明は終了です。
CPU のエミュレートでは OpCodeTable に当たる辞書を事前に用意できると面倒が減っていいですね。JavaScript のソースコードからサクっと作れて本当に良かった。これを自分で作るとなると面倒だろうことは容易に想像できるから…。
テストコードも簡単にですが書いてみましたが、先にも書いた通りテストコードが正しいかどうか?が怪しいです。特に割り込み周りが心配です。が、BRK は NES のソフトではほぼ使われていないってどこかで読んだ気がするので、まぁいいでしょう。
次回は CPU-2 です。寄り道ですが、ステップ動作など何かデバッグに便利なものを実装してみようと思います。この先なにか問題が起きた時にステップ動作できると何かと便利そうなので。