ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

ゲームボーイを作る(10) - CPUのレジスタ実装

スポンサーリンク

ゲームボーイを作るその10。前回でCPUレジスタの仮実装を行ったので、テスト環境側の期待値比較処理を作っていく。

CPUレジスタの期待値比較

CpuTbの修正

第7回で作ったCpuTbというテストベンチのモジュールに処理を追加していく。

追加するのはBoringUtilsに関する記述で、CPUレジスタCpuモジュールクラスのコードを修正せずに、テストベンチ側で参照できるようにする。イメージはVerilog HDLとかで階層間参照をするような形だ。

BoringUtilsについては、以前に記事を書いているのでそちらも参照してもらえるとありがたい。

BoringUtilsによるCPUレジスタの参照を追加したCpuTbモジュールのコードは次のようになる。

class CpuTestTb(val testRom: String) extends Module {

  val io = IO(new Bundle {
    val finish = Output(Bool())
    val is_success = Output(Bool())
    val timeout = Output(Bool())
    val regs = Output(new CpuReg)
  })

  io := DontCare

  val mem = Module(new Mem(testRom))

  val dut_cpu = Module(new Cpu)

  dut_cpu.io.mem <> mem.io

  // BoringUtilsでCpuクラスのregsへのパスを通す
  val w_regs = WireDefault(0.U.asTypeOf(new CpuReg))
  BoringUtils.bore(dut_cpu.r_regs, Seq(w_regs))

  // w_regsをIOポートに接続して、テストから参照できるようにする
  io.regs := w_regs
}

このようにしておくことで、Cpuモジュールのコードには手を入れずに、期待値比較対象であるCPUレジスタをテスト側から参照できるようになった。

CpuTestに01_ld.s用のテストを実装

次にテストクラス側。とは言っても、テストベンチモジュールのIOポートまで、CPUレジスタを引き出したので、後はテスト記述側でクロックを進めつつ期待値比較をするだけになる。

まだCPUの各命令の仕様があやふやなので、期待値側に間違いがあるかも。。。

class CpuTest extends FlatSpec with ChiselScalatestTester with Matchers {

  val annos = Seq(VerilatorBackendAnnotation)

  behavior of "Cpu"

  import BlarggGbTests._

  def compareReg(
    a: Int, b: Int, c: Int, d: Int, e: Int,
    h: Int, l: Int, sp: Int, pc: Int,
    f_z: Boolean, f_n: Boolean, f_h: Boolean, f_c: Boolean
  )(implicit dut: CpuTestTb): Unit = {
    println(s"reg = ${dut.io.regs.peek}")

    dut.io.regs.a.read.expect(a.U)
    dut.io.regs.b.read.expect(b.U)
    dut.io.regs.c.read.expect(c.U)
    dut.io.regs.d.read.expect(d.U)
    dut.io.regs.e.read.expect(e.U)
    dut.io.regs.h.read.expect(h.U)
    dut.io.regs.l.read.expect(l.U)
    dut.io.regs.sp.read.expect(sp.U)
    dut.io.regs.pc.read.expect(pc.U)

    // flagはとりあえず各ビット単位で比較
    dut.io.regs.f.z.expect(f_z.B)
    dut.io.regs.f.n.expect(f_n.B)
    dut.io.regs.f.h.expect(f_h.B)
    dut.io.regs.f.c.expect(f_c.B)
  }

  val testName = "01_ld.s"
  it should f"be passed ${testName} tests" in {
    val testHexFilePath = s"src/test/resources/cpu/${testName}.gb.hex"
    test(new CpuTestTb(testHexFilePath)).withAnnotations(annos) { c =>

      implicit val dut = c
      c.clock.setTimeout(10)

      compareReg(0, 0, 0, 0, 0, 0, 0, 0, 0x100, false, false, false, false)
      c.clock.step(100)
      // 1cycleごとに期待値を比較していく
      // NOTE: 初期値どうしよう。。bgbの値に合わせても良いのかも。
      // ld a, $a5                  ; a = $a5
      // ld a, imm -> need 2 cycles
      //            a     b     c     d     e     h     l    sp     pc    f_z    f_n    f_h    f_c
      compareReg(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x101, false, false, false, false)
      c.clock.step(1)
      compareReg(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x102, false, false, false, false)
      c.clock.step(1)
      compareReg(0xa5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x103, false, false, false, false)
      c.clock.step(1)

        // ld b, a
      compareReg(0xa5, 0xa5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x102, false, false, false, false)
      c.clock.step(1)

        // ld c, b
      compareReg(0xa5, 0xa5, 0xa5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x100, false, false, false, false)
      c.clock.step(1)

        // ld d, c
      compareReg(0xa5, 0xa5, 0xa5, 0xa5, 0x00, 0x00, 0x00, 0x00, 0x100, false, false, false, false)
      c.clock.step(1)

        // ld e, d
      compareReg(0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0x00, 0x00, 0x00, 0x100, false, false, false, false)
      c.clock.step(1)

        // ld l, e
      compareReg(0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0x00, 0x00, 0x00, 0x100, false, false, false, false)
      c.clock.step(1)

        // ld e, l
      compareReg(0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0x00, 0x00, 0x00, 0x100, false, false, false, false)
    }
  }
}

テスト部分を実装した以外に第7回のコードから変更した点に、Verilatorのバックエンドを指定するアノテーションの記述を追加した点がある(以下の部分)。

  // 使用時には次の2つのインポートが必要
  // import chiseltest.internal.VerilatorBackendAnnotation
  // import chiseltest.experimental.TestOptionBuilder._
  val annos = Seq(VerilatorBackendAnnotation)

  // test()の後に.withAnnotations(annos)が追加された
  it should f"be passed ${testName} tests" in {
    val testHexFilePath = s"src/test/resources/cpu/${testName}.gb.hex"
    test(new CpuTestTb(testHexFilePath)).withAnnotations(annos) { c =>
    //                                  ^^^^^^^^^^^^^^^^^^^^^^^

これを指定しておくことで、テスト実行時のシミュレーターにVerilatorを使用してくれるようになる。

ここまでで、テストが実行できるようになったがもちろんテスト自体はFAILする。ChiselTestでは、expectメソッドでの比較に失敗すると、次のようなメッセージが出た後に、シミュレーション自体がそのサイクルで終了するようになった。(個人的には、その次のサイクルくらいで落として欲しい。。)

bt:chisel-dmg> testOnly CpuTest -- -DwriteVcd=1
[info] compiling 1 Scala source to /home/diningyo/workspace/gameboy/chisel-dmg/target/scala-2.12/test-classes ...
Elaborating design...
Done elaborating.
~中略~
Exit Code: 0
[info] CpuTest:
[info] Cpu
[info] - should be passed 01_ld.s tests *** FAILED ***
[info]   CpuTestTb.io_regs_pc_data=256 (0x100) did not equal expected=257 (0x101) (lines in CpuTest.scala: 147, 135) (CpuTest.scala:123)

そろそろ、CPUの実装を本格的に進められそう。