ゲームボーイを作るその7。前回は最初のテストコードの準備をしたので、今回はハードウェアの検証環境を整備していく。
CPUのテストベンチ作成
最終的にはGameBoyのパーツ全部入りのテスト環境を起こす予定だが、まずはCPUを実装するためのテストベンチを作ろうと思う。
これまで解析してきたようにblargg-gb-testsは、GameBoy全体を使ってテストするような環境で、各ペリフェラルの設定などもテスト用バイナリに含まれていた。 しかし、CPUのテストベンチ、しかも初期のレベルで各種命令の基本的な動作を見るだけのテスト環境なので、次のようにテスト用のメモリとDUTとして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モジュールのみをサポートした。XilinxのBlackBoxは次のようなもの。
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.sbt
のlibraryDependencies
に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.scalatest
とchiseltest
パッケージ以下を一式インポートしておけば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() } } } }
次回はテストデータにもう一細工いるので、そのあたりの準備とレジスタあたりの実装をやっていく。