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

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

ゲームボーイを作る(7) - CPUのテストベンチ作成

スポンサーリンク

ゲームボーイを作るその7。前回は最初のテストコードの準備をしたので、今回はハードウェアの検証環境を整備していく。

CPUのテストベンチ作成

最終的にはGameBoyのパーツ全部入りのテスト環境を起こす予定だが、まずはCPUを実装するためのテストベンチを作ろうと思う。

これまで解析してきたようにblargg-gb-testsは、GameBoy全体を使ってテストするような環境で、各ペリフェラルの設定などもテスト用バイナリに含まれていた。 しかし、CPUのテストベンチ、しかも初期のレベルで各種命令の基本的な動作を見るだけのテスト環境なので、次のようにテスト用のメモリとDUTとしてCPUを直結した環境をつくる。

f:id:diningyo-kpuku-jougeki:20210719223125p:plain
CPU用テストベンチのブロック図

内部のレジスタの値を期待値比較したいので、DUT内部のレジスタ一式を直接テストベンチのIOまで引き上げて、Chisel側のテスト記述でチェックすることにした。

メモリ

メモリはごく一般的なシングルポートRAMになるが、テスト用のHEXデータを読み込むとかの処理を付けて、テスト実行時にクラスパラメータ経由で初期化を行う。 メモリのインタフェースは、シンプルに次のような感じ。

class MemIO extends Bundle {
  val addr = Output(UInt(16.W))
  val wen = Output(Bool())
  val wrdata = Output(UInt(8.W))
  val rddata = Input(UInt(8.W))
}

今の所、アドレス/データのビット幅を可変にするニーズはないので、そのまま定数値で宣言している。

Chiselのメモリ宣言Memもあるのだが、初期化などを行う場合、メモリはBlackBoxを使ってVerilogのラッパーを作ったほうが、融通が利くので個人的には好み。後々あるかもしれないケースを考えて、直接BlackBoxで作ったモジュールをテストベンチ上で使うのでは無く、メモリのラッパーモジュールをChiselで実装して、それをインスタンスして使う形にした。 多分先々の実装の都合で変更されると思うけど、今の所こんな感じ。

sealed trait MemType
case object ChiselMem extends MemType
case object Xilinx extends MemType
case object Intel extends MemType

class Mem(
  val hexPath: String = "",
  val memType: MemType = Xilinx,
) extends Module {

  val io = IO(Flipped(new MemIO()))

  io := DontCare

  memType match {
    case ChiselMem => throwException("Not support yet")
    case Xilinx =>
      val mem = Module(new xilinx_mem(hexPath))
      mem.io.clk  := clock
      mem.io.addr := io.addr
      io.rddata   := mem.io.q
      mem.io.ren  := !io.wen
      mem.io.wen  := io.wen
      mem.io.data := io.wrdata

    case Intel => throwException("Not support yet")
  }
}

最初に動かすFPGAは自分の持っているデバイスの都合上Xilinxになるので、初期段階ではXilinxのRAMモジュールのみをサポートした。XilinxBlackBoxは次のようなもの。

import chisel3._
import chisel3.util._
import chisel3.experimental.{IntParam, StringParam}

class xilinx_mem(val hexPath: String) extends BlackBox(
  Map(
    "p_ADDR_BITS" -> IntParam(16),
    "p_DATA_BITS" -> IntParam(8),
    "p_MEM_ROW_NUM" -> IntParam(0x10000),
    "p_INIT_HEX_FILE" -> StringParam(hexPath)
  )) with HasBlackBoxResource {
  val io = IO(new Bundle {
    // external
    val clk = Input(Clock())

   // memory
    val addr = Input(UInt(16.W))
    val q = Output(UInt(8.W))
    val ren = Input(Bool())
    val wen = Input(Bool())
    val data = Input(UInt(8.W))
  })

  addResource("/mem/xilinx_mem.sv")
}

CPU

CPUはwikiなどにも記載があるが、SharpのLR35902というCPUになる。Intelの8080と多くの共通部分を持つが、より強力なCPUであるZilogのZ80をベースにしたもの、らしい。

CPU:シャープ製のLR35902がサウンドなどの機能と共に組み込まれている。動作クロック周波数は、4.19MHz。Intel 8080に近似した機能を持つカスタムプロセッサである。但しIntel 8080から一部の命令が削減され、Z80のフラグ処理の一部と電源制御に使われる独自命令及び仕様が追加されていることや、シャープがZ80のセカンドソースメーカーであることから、カスタムZ80とも表記される。

後々、端子の追加も出てくるが、今の所メモリへのIOポートさえ存在していれば事足りるので、次のようにCPUのIOを定義した。

class CpuIO extends Bundle {
  val mem = new MemIO()
}

class Cpu extends Module {
  val io = IO(new CpuIO())

  io := DontCare
}

このCpuIOは今後の実装に応じて、適宜端子を追加していく。CPU内部のレジスタ記述についてはBoringUtils使って一式CPUトップのIOまで引き上げてくる予定。

CpuTb

現時点ではCPUのレジスタすら存在していない状態なので、テストベンチモジュールでやることは、ただ単にCPUとメモリをインスタンスしてつなぐだけになる。 テスト用のROMデータはクラスパラメータのtestRomに渡す形にした。

class CpuTb(val testRom: String) extends Module {

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

  io := DontCare

  val mem = Module(new Mem(testRom))

  val dut_cpu = Module(new Cpu())

  dut_cpu.io.mem <> mem.io
}

今までの経験からtimeout端子をつけたが後述する理由により、不要な事が判明した。。。 最終的にはblargg-gb-testsの終了判定を、何らかの論理で判定してfinishに接続して、Chiselのテスト記述側で終了する形に持っていく予定。

テストクラス

最後にChiselのテストクラスを作っていく。しばらくはChiselのテスト機能を使って、モジュール毎のテスト環境を構築していく。最終的なテストは試してみたいこともあるから、Chiselのテスト機能使わない形になるかも?

ChiselTest

今回のゲームボーイを作ろうプロジェクトからは、iotestersの使用を止めてchiseltestを使っていく。存在自体は認識してて、ちょくちょく何ができるのかとかを追ってはいたが、このブログで取り扱うのは初なはず。 リポジトリ名は、開発当初の名前であるchisel-testers2になっているが、2019年の12月あたりにChiselTestへと変更された。iotestersではかなりテスト記述に制限があったが、ChiselTestではそのあたりが改善されている(たとえばfork-joinとかが使えるようになってたり)。

使用する際にはbuild.sbtlibraryDependenciesにChiselTestの設定が必要になる。

  • build.sbt
    libraryDependencies ++= Seq(
      "edu.berkeley.cs" %% "chisel3" % "3.4.3",
      "edu.berkeley.cs" %% "chisel-iotesters" % "1.5.3",
      "edu.berkeley.cs" %% "chiseltest" % "0.3.3" % "test" // これが必要
    ),

基本的に使う際には、org.scalatestchiseltestパッケージ以下を一式インポートしておけばOK。iotestersとは異なり、テストクラスを作る際にはscalatestのクラスを直接継承する形になった。Chiselの要素はChiselScalatestTesterというトレイトに実装されており、これをミックスインすることで、テストに必要なメソッド等がクラス内で使用可能となる。

import org.scalatest._
import chiseltest._

class MyModule extends Module {
  val io = IO(new Bundle {
    val in = Input(Bool())
    val out = Output(Bool())
  })

  io.out := io.in
}

class BasicTest extends FlatSpec with ChiselScalatestTester with Matchers {
  behavior of "MyModule"

  // FlatSpec使ってるので、この辺はiotestersと一緒。
  it should "do something" in {
    // testの書き方がちょっと変わる。
    test(new MyModule) { c =>
      // データのやり取りに使うpeek/poke/expectメソッドは
      // 各端子のメソッドとして呼び出す。
      c.io.in.poke(true.B)
      c.io.in.expect(true.B)
    }
  }
}

CPUのテストクラス

ということで、本題。とりあえず今は自分で作った、バイナリデータを渡せればOKなので、次のような感じになる。

import org.scalatest._
import chiseltest._

class CpuTest extends FlatSpec with ChiselScalatestTester with Matchers {
  behavior of "Cpu"

  val test_name = "test.hex"

    it should f"be passed ${test_name} tests" in {
      test(new CpuTestTb(test_name)) { c =>
        // テストを実装していく。
        // 最初はステップ単位で実行して、レジスタの値を比較する感じだが、
        // 今は中身がないのでfail()を呼んでおく。
        fail()
      }
    }
  }
}

次回はテストデータにもう一細工いるので、そのあたりの準備とレジスタあたりの実装をやっていく。