CPU-2 ステップ動作とテスト

前回は CPU を実装したので、今回は CPU をステップ動作できるようにします。
また、ステップ動作を利用して CPU の動作テストプログラム nestest.nes を使ってテストを行います。

ステップ機能

ViewController に Step ボタンを付けました。

ROM ファイルを読み込みこのボタンを押すことで CPU を 1命令実行できるようにしました。
同時に実行したマシン語とそのアセンブラ表記、そして CPU の状態をコンソールに表示します。

C000  4C F5 C5    JMP $C5F5          A:00 X:00 Y:00 P:24 SP:FD
C5F5  A2 00       LDX #$00           A:00 X:00 Y:00 P:24 SP:FD
C5F7  86 00       STX $00            A:00 X:00 Y:00 P:26 SP:FD
C5F9  86 10       STX $10            A:00 X:00 Y:00 P:26 SP:FD
C5FB  86 11       STX $11            A:00 X:00 Y:00 P:26 SP:FD
C5FD  20 2D C7    JSR $C72D          A:00 X:00 Y:00 P:26 SP:FD
C72D  EA          NOP                A:00 X:00 Y:00 P:26 SP:FB

CPU の状態は命令表示されている次の行で反映されます。上の例だと 2行目の LDX #$00 の結果、Zero フラグが立って P レジスタが $26 になるのは次の行です。この仕様、個人的には微妙ですが nestest.log(後述)の表記がこうなっているので合わせてあります。(自分はどうしても左の処理の結果が右に出るって考えてしまう…)

ステップ動作させるためのコードを順に見ていきます。
まず Step ボタンが押されると cpu.run(true) として CPU の run 関数が引数 true で呼ばれます。
この引数は 1命令分のステップ情報(StepInfo)を表示して Debugger に保存するかどうかを指定します。
false を渡す、もしくは何も渡さなければステップ情報は生成されません。

StepInfo は CPU+Debugger.swift で定義されています。
これには CPU の実行 1ステップ分の情報が入ります。

    struct StepInfo: CustomStringConvertible {
        let cpu: CPU.CPUState
        let opcode: Byte
        let operand: Word
        let mode: CPU.AddressingMode
        let jump: Word
        
        var description: String {  
            let opcodeString = DisAsm.addressAndCodeString(pc: cpu.PC, opcode: opcode, operand: operand, mode: mode)
            let disAsmString = DisAsm.stepString(self)
            return opcodeString + disAsmString + cpu.description
        }
    }

内容は CPU の情報として CPUState、実行する命令の opcode、operand、アドレッシング・モード、そしてジャンプ命令用にジャンプ先のアドレスです。

すぐ下に struct CPUState が定義されています。

    struct CPUState: CustomStringConvertible {
        let PC: Word
        let SP: Byte
        let A: Byte
        let X: Byte
        let Y: Byte
        let P: Byte

        var description: String {
            return "A:\(A.hexString()) X:\(X.hexString()) Y:\(Y.hexString()) P:\(P.hexString()) SP:\(SP.hexString())"
        }
    }

これはまんま CPU のレジスタです。表示する際にはそれぞれを並べて表示するだけです。
今更ですが CPU のレジスタを宣言するのはこの CPUState を使った方が良かったのかも…

ちなみに CPUState の PC には実行する opcode のアドレスが入っています。そのために CPU の run 関数では命令をフェッチする前に let pc = PC としてプログラムカウンタの値を保存して用いています。
また CPUState のそのほかの値は先に書いたように命令を実行する前の値が入っています。

各ステップ毎にこの StepInfo を Debugger.swift で定義されている Debugger に保存していきます。Debugger に全部保存しているのは、後で cpu の動作を再現できるようにと思ったのですが今の所は実装していません。思いの外サクっと nestest 通ってしまったので…

また、StepInfo を保存する際に StepInfo をコンソールに表示もしています。
StepInfo の表示部分では DisAsm.swift を使ってマシン語をディスアセンブルしています。DisAsm.stepString() は StepInfo を渡してその命令のアセンブラ表記を返します。
ちなみに relative jump 命令はジャンプ先が 1byte の相対値で表示されるのですが、それだと分かりにくいのでジャンプ先の実アドレスも表示するようにしています。
こんな感じです。

C72F  B0 04       BCS $04 = $C735    A:00 X:00 Y:00 P:27 SP:FB
C735  EA          NOP                A:00 X:00 Y:00 P:27 SP:FB

ディスアセンブルのコードは簡単で、OpCode.opcodeTable に opcode を渡してアセンブル命令(baseName)を取り出し、アドレッシング・モードに合わせて operand を取得&表示用にフォーマットするだけです。operand のフォーマットは事前にアドレッシング・モードに合わせて String のフォーマット文字列を辞書 operandStringFormat に用意してあります。

    private static let operandStringFormat: Dictionary<CPU.AddressingMode, String> = [
        .implied: "", .accumulator: "",
        .immediate: "#$%02X", .zeroPage: "$%02X", .relative: "$%02X", .zeroPageX: "$%02X, X", .zeroPageY: "$%02X, Y",
        .preIndexedIndirect:"($%02X, X)", .postIndexedIndirect: "($%02X), Y", .absolute: "$%02X%02X", .absoluteX: "$%02X%02X, X",
        .absoluteY: "$%02X%02X, Y", .indirectAbsolute: "($%02X%02X)"
    ]

ステップ動作はの説明は以上です。
次はこのステップ機能を使って CPU のテストを行います。

nestest.nes の実行

NesDev に Emulator tests というページがあり、そこの CPU Tests の項に nestest があります。これは CPU テスト用の ROM で CPU の機能を一通りテストすることができます。

エミュレータでこの ROM を起動すると上の画像のように UI 付きの画面でテストを行えますが、StNesEmu にはまだ PPU がないので画面表示ができません。そのような場合にはnestest.txtにもあるように 0xC000 から起動することで UI なしに自動で全てのテストを行うことができます。この場合テスト結果は 0xC002, 0xC003 に保存されて、両方が 0x00 であれば問題ありません。問題があった場合はそれぞれに問題に応じた値が保存されます。また、その値については nestest.txt に詳しく載っています。

この nestest.nes には実行時のステップ毎のログが nestest.log として提供されているので、今回実装した CPU のステップ情報と比べることによって動作の確認が行えます。
nestest.log の内容はこんな感じです。

C000  4C F5 C5  JMP $C5F5                       A:00 X:00 Y:00 P:24 SP:FD CYC:  0 SL:241
C5F5  A2 00     LDX #$00                        A:00 X:00 Y:00 P:24 SP:FD CYC:  9 SL:241
C5F7  86 00     STX $00 = 00                    A:00 X:00 Y:00 P:26 SP:FD CYC: 15 SL:241
C5F9  86 10     STX $10 = 00                    A:00 X:00 Y:00 P:26 SP:FD CYC: 24 SL:241
C5FB  86 11     STX $11 = 00                    A:00 X:00 Y:00 P:26 SP:FD CYC: 33 SL:241
C5FD  20 2D C7  JSR $C72D                       A:00 X:00 Y:00 P:26 SP:FD CYC: 42 SL:241

これが 8991行(ステップ)もあります!
ちなみに CYC と SL はそれぞれ PPU の Cycle と ScreenLine(だと思うの)で CPU の機能テストでは使用しません。

Xcode で nestest.nes を実行してテスト(nestest.log との比較)をできるようにしたのが NesTest.swift の testNesTest です。
テスト部分はこんな感じです。

            for state in log.dropLast() {
                cpu.run(true)
                if let stepInfo = Debugger.lastStep() {
                    XCTAssertEqual(stepInfo.cpu.A, state.a)
                    XCTAssertEqual(stepInfo.cpu.X, state.x)
                    XCTAssertEqual(stepInfo.cpu.Y, state.y)
                    XCTAssertEqual(stepInfo.cpu.P, state.p)
                    XCTAssertEqual(stepInfo.cpu.SP, state.sp)
                    if stepInfo.cpu.A != state.a ||
                        stepInfo.cpu.X != state.x ||
                        stepInfo.cpu.Y != state.y ||
                        stepInfo.cpu.P != state.p ||
                        stepInfo.cpu.SP != state.sp {
                        print("Error at step: \(stepNum)")
                        break
                    }
                }
                else {
                    print("stepInfo is not provided at step: \(stepNum)")
                }
                stepNum += 1
            }

nestest.log を 1行読み込み nestest.nes を 1ステップ実行し、CPU の情報(A, X, Y, P, SP)を比較して違いがあればテスト失敗で終了します。

            let v1: Byte = cpu.read(0x0002)
            let v2: Byte = cpu.read(0x0003)
            XCTAssertEqual(v1, 0x00)
            XCTAssertEqual(v2, 0x00)

問題なく全てのログを比較し終わったら 0xC002, 0xC003 が 0x00 かどうか確認してテスト終了です。

実際にテストしてみた結果は…やはりいくつか問題が見つかりました。
テスト失敗時にはコンソールに以下のように表示されます。

C7E7  08          PHP                A:00 X:00 Y:00 P:6F SP:FB
C7E8  68          PLA                A:00 X:00 Y:00 P:7F SP:FA
/Volumes/Data/NesEmu/StNesEmu/StNesEmuTests/NesTest.swift:60: error: -[StNesEmuTests.NesTest testNesTest] : XCTAssertEqual failed: ("127") is not equal to ("111") - 
Error at step: 72

ログの 72行目、PHP 命令での P レジスタの結果が間違っていました。
本来なら 6F のままのはずが B フラグが立って 7F になっています。

これを修正してテストを通して、また新しい問題が見つかって修正して…と繰り返していきます。
結果、他にもいくつかの問題が見つかりましたが、無事修正もできテストも問題なく通るようになりました!

これで nestest も通ったので CPU の実装は終了です。

まとめ

今回は CPU にステップ実行機能を実装して、それを使い nestest.nes で CPU のテストを行いました。

Debugger.swift を実装する際には、レジスタの値を変えたり前の処理に戻したり、いろいろな機能を実装するつもりでいたのですが、nestest が思いの外サクっと通ってしまったので、今のほぼなんにもしない Debugger になっています。気が向いたら格好いいコントロールパネルを作って実装したいと思います。

nestest ですが、これのおかげで本当に辛い思いをしないで CPU のテストが行えました。ま、もともと移植なので自分がヘマをしない限りは問題が起こりようがないのですが、それなりに問題がでてきても nestest のおかげでサクっと解決です。

というわけで、次回はいよいよ PPU 周りを実装していきます。