この記事はHDL (SystemVerilog/Verilog/VHDL/Chisel/etc.) Advent Calendarの20日目の記事です。 Chiselも含まれてたので、Chiselネタで何か、、、、と考えた結果、あんまりちゃんと調べて&まとめてなかったChiselTestについて書くことにしました。
ChiselTest
ChiselTestはchisel-testersの後継として
開発が進められているテストハーネスです。
初期はtesters2
として開発が進められていましたが、現在はChiselTestと名称が変更されています。
今回の記事執筆に当たって、使用したChisel関連のパッケージのバージョンは次のようになっています。
- Chisel : 3.4.4
- Firrtl : 1.4.4
- ChiselTest : 0.3.4
ChiselTestを試したい
ChiselTestを手っ取り早く試すにはchisel-template
を使うのが良いです。
現時点のmain
ブランチでは、ChiselTest
の設定が行われた build.sbt
が含まれており、テンプレートをそのまま使用するだけで試すことが可能です。(ちなみにchiselte-tamplateのリポジトリのmaster
ブランチは開発ブランチとして使用されてるようで、安定していない感じなので使用しないほうが良さそう)
またChiselTestサポート前のchisel-templateはGCD
計算回路とiotesters
を使用したテストが含まれていましたが、main
ブランチのテストはChiselTest
を使用した物に置き換えられています。
という事で単純に試すなら、以下を実行すればOK。
git clone https://github.com/freechipsproject/chisel-template.git cd chisel-template sbt "testOnly gcd.GcdTesters2"
次にようなログが表示されて、テストにパスすることが確認できるはずです。
sbt:%NAME%> testOnly gcd.GcdTesters2 [info] Compiling 1 Scala source to /home/diningyo/workspace/study/2000_chisel/801_diningyo-chisel-template/target/scala-2.12/test-classes ... Elaborating design... Done elaborating. test DecoupledGcd Success: 0 tests passed in 841 cycles in 0.638100 seconds 1317.98 Hz [info] GcdTesters2: [info] - Gcd should calculate proper greatest common denominator [info] ScalaTest [info] Run completed in 2 seconds, 73 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [info] Passed: Total 1, Failed 0, Errors 0, Passed 1 [success] Total time: 3 s, completed 2021/12/19 13:39:45
iotestersからの移行
ここからは従来のiotesters
を使用していた場合から、ChiselTest
への移行をする際に必要な変更を見ていきます。
大きく次の変更が必要になります。
- build.sbtの修正
- テストクラスの宣言
- テスト実行関数及び引数の変更
- テスト記述の変更
それぞれ個別に見ていきます。
build.sbtの修正
ChiselTest
を使うためにはbuild.sbt
のlibraryDependencies
に設定を追加すればOKです。
libraryDependencies ++= Seq( "edu.berkeley.cs" %% "chisel3" % "3.4.4", "edu.berkeley.cs" %% "chisel-iotesters" % "1.5.4", "edu.berkeley.cs" %% "chiseltest" % "0.3.4" // ChiselTestの設定を追加 ),
テストクラスの宣言
iotesters
ではテストクラスはChiselFlatSpec
クラスを継承して作っていました。下記はchisel-template
に含まれていたioteseters
を使用したテストクラスの宣言部分です。
class GCDTester extends ChiselFlatSpec {
このテストクラス宣言部分がChiselTest
では次のように変更になります。
class GcdTesters2 extends FreeSpec with ChiselScalatestTester {
FreeSpec
はテストハーネスであるScalaTest
のものです。iotesters
ではテストクラスは上記ChiselFlatSpec
クラス自体がScalaTest
のFlatSpec
クラスを継承していましたが、ChiselTest
ではライブラリ内部でScalaTest
に依存しない作りへと変更されました。
またChiselScalatestTester
にはChiselのテストに必要となる各種メソッド等が実装されています。
テストの実行:Driver -> test
次にテストの実行部分です。iotesters
では次のようにiotesters.Driver
のexecute
を呼び出す形になっていました。
iotesters.Driver.execute(Array(), () => new GCD) { c => new GCDUnitTester(c) } should be (true)
この時c => new GCDUnitTester(c)
の部分はPeekPokeTester
クラスを継承したクラスとなります。
この部分はChiselTest
ではChiselScalatestTester
のtest
メソッドを呼び出す形となり、多少スッキリしました。
test(new DecoupledGcd(16)) { dut =>
DUTの操作に使用していたPeekPokeTester
が無くなったため、各テストで共通して必要な記述はテストクラス内部にメソッドを定義して共通化を行うことになります。
class GcdTesters2 extends FreeSpec with ChiselScalatestTester { // 共通のテスト処理 def common_process(c: DUT): Unit = { // 処理を記載 } ... }
テスト実行時のオプション
iotesters
ではexecute
メソッドの第1引数にArray[String]
を渡すことで、オプションを指定することができました。
if(backendNames.contains("verilator")) { "using --backend-name verilator" should "be an alternative way to run using verilator" in { iotesters.Driver.execute( Array("--backend-name", "verilator"), () => new GCD) { c => new GCDUnitTester(c) } should be(true) } }
上記のコードで指定されている--backend-name
オプションは、シミュレーション実行時に使用するシミュレータを切り替えるためのオプションです。
この他にはテストの実行ディレクトリを変更する--target-dir
やVCDファイルを生成する--generate-vcd-output
など多くのオプションが存在しています。
ChiselTest
ではオプションの指定にwithAnnotation
メソッドとwithFlags
メソッドを使用します。
次のwithAnnotation
の例では、シミュレーターのバックエンドをVerilatorに変更しています。
import chisel3.experimental.BundleLiterals._ import chiseltest.experimental.TestOptionBuilder._ // VerilatorBackendAnnotationのimportが必要 import chiseltest.internal.VerilatorBackendAnnotation // 省略 // withAnnotationsにVerilatorBackendAnnotationを渡す test(new DecoupledGcd(16)). withAnnotations(Seq(VerilatorBackendAnnotation)) { dut =>
ChiselTest
ではデフォルトで使用されるテスト実行ディレクトリの名前がテスト名の文字列となっています。
chisel-templateの以下の例では、テスト実行ディレクトリはtest_run_dir/Gcd_should_calculate_proper_greatest_common_denominator
となります(長い)。
"Gcd should calculate proper greatest common denominator" in { test(new DecoupledGcd(16)). withAnnotations(Seq(VerilatorBackendAnnotation)) { dut =>
このテスト実行ディレクトリはwithFlags
メソッドに--target-dir
の指定を含むArray
を渡すことで変更可能です。
"Gcd should calculate proper greatest common denominator" in { test(new DecoupledGcd(16)). withAnnotations(Seq(VerilatorBackendAnnotation)). withFlags(Array("--target-dir=test_run_dir/Gcd")) { dut =>
なおVCDファイルのダンプについてはWriteVcdAnnotation
をwithAnnotation
に渡すことでもダンプできるようになっていますが、このアノテーションを有効にする仕組みがChiselScalatestTester
自体に組み込まれています。
そのため次のようにsbt
からのテスト実行時にwriteVcd
オプションを指定することで波形ファイルを生成可能です。
testOnly gcd.GcdTesters2 -- -DwriteVcd=1
信号の操作
最後にテスト対象のモジュールの信号に操作についてです。iotesters
ではPeekPokeTester
のpeek
/poke
/expect
といったメソッドを使用して、テスト対象モジュールの入出力信号を制御していました。次のコードはchisel-templateのGCDUnitTester
から抜粋したものです。
poke(gcd.io.value1, i)
poke(gcd.io.value2, j)
poke(gcd.io.loadingValues, 1)
ChiselTest
では先の記載したようにPeekPokeTester
が廃止されており、その代わりに各信号のpeek
/poke
等のメソッドを呼び出すことになります。
次のコードはChiselTest
に含まれるBasicTest.scala
から抜粋したものです。
it should "test reset" in { test(new Module { val io = IO(new Bundle { val in = Input(UInt(8.W)) val out = Output(UInt(8.W)) }) io.out := RegNext(io.in, 0.U) }) { c => c.io.out.expect(0.U) c.io.in.poke(42.U) c.clock.step() c.io.out.expect(42.U) c.reset.poke(true.B) c.io.out.expect(42.U) // sync reset not effective until next clk c.clock.step() c.io.out.expect(0.U) c.clock.step() c.io.out.expect(0.U) c.reset.poke(false.B) c.io.in.poke(43.U) c.clock.step() c.io.out.expect(43.U) } }
ChiselTestの新機能
最後にChiselTest
で新しくサポートされた機能について、簡単に紹介します。
fork-join
ChiselTest
ではfork-join
による並列処理がサポートされました。これについてはVerilog HDLのfork-join
をイメージしてもらえばOKです。
簡単な確認のために書いたのが次のコードです。
"Check fork" in { test(new Module { val io = IO(new Bundle { val in0 = Input(UInt(3.W)) val in1 = Input(UInt(2.W)) val in2 = Input(UInt(2.W)) val out0 = Output(UInt(3.W)) val out1 = Output(UInt(2.W)) val out2 = Output(UInt(2.W)) }) io.out0 := io.in0 io.out1 := io.in1 io.out2 := io.in2 }) { dut => for (i <- 0 to 4) { fork { dut.io.in0.poke(i.U) dut.clock.step(3) }.fork { dut.io.in1.poke(i.U) dut.clock.step(2) dut.io.in1.poke((i+1).U) dut.clock.step(1) }.fork { dut.io.in2.poke((i+1).U) dut.clock.step(1) dut.io.in2.poke(i.U) dut.clock.step(1) }.join } } }
上記のコードのように、並列で実行したいブロックを複数個fork
で連結した後、最後のjoin
を呼び出せばOKです。
このコードを実行した際に取得した波形は次のようになりました。処理が終わったブロックは値が0
になるようなので、この辺は少し注意がいるかも?(io.in2
の制御は2cycle分しか処理が存在していないが、その部分は波形では0.U
になっている。)
timescope
イマイチしっくりくる説明が思い浮かんでいませんが、これはtimescope
ブロック内で行った信号制御の影響を、ブロックから抜ける際に全て元に戻してくれるという機能のようです。分かりにくいのでサンプルコードを書いて実行してみました。
"Check timescope" in { test(new Module { val io = IO(new Bundle { val in0 = Input(UInt(3.W)) val out0 = Output(UInt(3.W)) }) io.out0 := io.in0 }) { dut => dut.clock.step(0) dut.io.in0.poke(2.U) dut.clock.step(1) // timescope内で実行した信号の変化は // ブロックを抜けると元に戻る timescope { dut.io.in0.poke(3.U) dut.clock.step(1) } dut.clock.step(3) } }
上記のコードを実行すると、timescope
で囲んだブロック内で行ったpoke(3.U)
の処理がブロック脱出時に破棄され、脱出後のio.in0
の値はブロック実行前の最終状態であるpoke(2.U)
に変化する結果となりました。
ready-valid信号を使ったハンドシェークのようにデフォルトが0
でvalid
が1
の時にトランザクションが有効、といったプロトコルを書くのに適している、との記載がREADMEにありました。
timeout
地味に便利だと感じたのがtimeout
です。。iotesters
ではfork
を使って、横でウォッチドッグタイマを回しておくといった事ができないので、テスト用のモジュールを書いて、中にカウンタを仕込んで回しておくような事を行っていました。
ChiselTest
ではsetTimeout
メソッドに所望のカウント値を設定しておくことで、そのサイクル数を過ぎたら自動的にテストをFAILさせてくれるようになっています。
"Check timeout" in { test(new Module { val io = IO(new Bundle { val in0 = Input(UInt(3.W)) val out0 = Output(UInt(3.W)) }) io.out0 := io.in0 }) { dut => dut.clock.setTimeout(10) dut.clock.step(11) } }
上記を実行すると、次のようなログが表示されてテストがFAILしました。
[info] - Check timeout *** FAILED *** [info] chiseltest.TimeoutException: timeout on Clock(IO clock in GcdTesters2_Anon) at 10 idle cycles [info] at chiseltest.backends.treadle.TreadleBackend.$anonfun$run$5(TreadleBackend.scala:191) [info] at chiseltest.backends.treadle.TreadleBackend.$anonfun$run$5$adapted(TreadleBackend.scala:187) [info] at scala.collection.mutable.HashMap.$anonfun$foreach$1(HashMap.scala:149) [info] at scala.collection.mutable.HashTable.foreachEntry(HashTable.scala:237) [info] at scala.collection.mutable.HashTable.foreachEntry$(HashTable.scala:230) [info] at scala.collection.mutable.HashMap.foreachEntry(HashMap.scala:44) [info] at scala.collection.mutable.HashMap.foreach(HashMap.scala:149) [info] at chiseltest.backends.treadle.TreadleBackend.run(TreadleBackend.scala:187) [info] at chiseltest.internal.Context$.$anonfun$run$1(Testers2.scala:158) [info] at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23)
という事でざっとChiselTest
について調べた内容を簡単にまとめてみました。他にもカバレッジ取得の機能なども入ってるようなので追ってそのあたりも確認したい所です。
もっと詳しく知りたいよーと思った方がいらっしゃれば、ChiselTestのテストコードを読んでみるのが良いかと思います。(そして教えてください。)