前回のChiselの記事では今更ではあるがChiselでデザインしたハードウェアをVerilogのRTLに変換するためにやることについてをまとめた。
今日はついでなので、前回の続きというかその前の段階とかに位置しているはずのChiselで作っているハードウェアをそのままChiselのテスト機能でテストする方法をまとめてみる。
Chiselで作ったハードウェアデザインをテストする方法
自分的な位置づけは前回の続きのネタなので、前回作った加算器のプロヘクトをそのまま流用することにする。
diningyo@diningyo-pc:/home/diningyo/workspace/make_rtl$ tree . ├── build.sbt └── src └── main └── scala └── Top.scala
変換するChiselのコードが書かれたファイル
上記に記載したChiselのソースコードは説明の都合上、ちょっと変化させたものにしようと思う。 ということで以下のようなモジュールにした。 変更点はただ単に演算結果が入力に対して1サイクル遅れて出力されるようにレジスタを追加しただけ。
import chisel3._ class Top(in0Bits: Int, in1Bits: Int) extends Module { val io = IO(new Bundle { val in0 = Input(UInt(in0Bits.W)) val in1 = Input(UInt(in0Bits.W)) val out = Output(UInt((in0Bits+1).W)) }) io.out := RegNext(io.in0 +& io.in1) } object Elaborate extends App { chisel3.Driver.execute(args, () => new Top(32, 32)) }
とりあえず上記の回路を前回同様にsbtで実行すると回路のエラボレートが走り、以下のようなVerilogのRTLが生成される。
$ sbt "run" [info] Loading global plugins from /home/diningyo/.sbt/1.0/plugins [info] Loading project definition from /home/diningyo/prj/test_chisel_hw/project [info] Loading settings for project test_chisel_hw from build.sbt ... [info] Set current project to test_chisel_hw (in build file:/home/diningyo/prj/test_chisel_hw/) [warn] Multiple main classes detected. Run 'show discoveredMainClasses' to see the list [info] Running Elaborate [info] [0.003] Elaborating design... [info] [0.083] Done elaborating. Total FIRRTL Compile Time: 239.8 ms [success] Total time: 1 s, completed 2019/02/23 19:00:53
- 生成されたRTL
`ifdef RANDOMIZE_GARBAGE_ASSIGN `define RANDOMIZE `endif `ifdef RANDOMIZE_INVALID_ASSIGN `define RANDOMIZE `endif `ifdef RANDOMIZE_REG_INIT `define RANDOMIZE `endif `ifdef RANDOMIZE_MEM_INIT `define RANDOMIZE `endif module Top( input clock, input reset, input [31:0] io_in0, input [31:0] io_in1, output [32:0] io_out ); wire [32:0] _T_5; reg [32:0] _T_7; reg [63:0] _RAND_0; assign _T_5 = io_in0 + io_in1; assign io_out = _T_7; `ifdef RANDOMIZE integer initvar; initial begin `ifndef verilator #0.002 begin end `endif `ifdef RANDOMIZE_REG_INIT _RAND_0 = {2{$random}}; _T_7 = _RAND_0[32:0]; `endif // RANDOMIZE_REG_INIT end `endif // RANDOMIZE always @(posedge clock) begin _T_7 <= _T_5; end endmodule
Chiselのテスト・ハーネスの設定
ChiselにはChisel本体とは別にテスト・ハーネスのライブラリが存在しており、これを使うことでテスト・ハーネスが提供する機能とScalaのコードを使って作成したChiselモジュールのテストを実施することが可能だ。
このテスト・ハーネスはChisel3本体とパッケージ空間自体は一緒だが、別のライブラリとして提供されているため、使用する際にはsbtの設定の追加が必要になる。
ということで前回使ったbuild.sbt
を変更していく。
scalaVersion := "2.11.12" resolvers ++= Seq( Resolver.sonatypeRepo("snapshots"), Resolver.sonatypeRepo("releases") ) libraryDependencies += "edu.berkeley.cs" %% "chisel3" % "3.0-SNAPSHOT" // 以下の行を追加 libraryDependencies += "edu.berkeley.cs" %% "chisel-iotesters" % "[1.2.5,1.3-SNAPSHOT["
なんとなくわかるとは思うがsbtではlibraryDependencies
にライブラリを追加すると、そのライブラリをsbtコマンド実行前に自動的にダウンロードしてくれるようになっている。
またbuild.sbt
の修正内容はsbtコマンド実行時にチェックが入り、必要に応じてアップデートがかかるようになっている。
そのため上記のように今回使用したいchisel-iotesters
とそのバージョン指定を追加すればそれでOKだ。
テスト用のメイン関数の追加
これでchisel.iotesters
を使用する準備が完了したので次はこれを使ってテストを作っていく。
1ファイルに収まっていたほうが、わかりやすい気がするのでそのままTop.scala
にテスト用のメイン関数を追加してそれをsbtで選択して実行することにする。
追加するテスト用のコード
iotesters
のインポート
まずはiotesters
のインポート処理から。Top.scala
の先頭のimport
宣言に追加する形で、以下の宣言を追加する。
import chisel3.iotesters import chisel3.iotesters.PeekPokeTester
テスト用メイン関数の作成
次はテスト用のメイン関数を作成していく。Scalaでは適当な名前でobject
を作りその中にdef main(args: Array[String])
という関数を作るとそれがメイン関数となる。
それをtrain App
をミックスインすることで省略したのが、前回作ったElaborate
オブジェクトとなっている。
ということで前回と同じくtrait App
をミックスインしてテスト用のオブジェクトTest
を作成しよう。
object Test extends App { iotesters.Driver.execute(args, () => new Top(32, 32)) { c => new PeekPokeTester(c) { poke(c.io.in0, 100) // pokeで入力端子に値を入力出来る poke(c.io.in1, 10) step(1) // step(N)で指定したサイクルが経過 expect(c.io.out, 110) // expectで期待値と比較 println(s"io.out = ${peek(c.io.out)}") // peekで指定した端子の値が取得できる } } }
テストの際の一般形は以下のようなものになる。
iotesters.Driver.execute(args, () => <テスト対象のChiselモジュールをインスタンス>) { c => new PeekPokeTester(c) { // peek/poke/expectを使ってテストを記述 } }
これでものすごく簡単なテストは完成だ。
早速実行してみよう。
同一のsbtプロジェクト内に複数のメイン関数が存在する状態になっているので、実行時にはrunMain
を指定して実行する。
なお前回と同様にrun
のみを実行することも可能だが、その際にはsbtがElaborate
とTest
の2つを検出するため、どちらのメインを実行するかを聞かれるようになる。
$ sbt "runMain Test" [info] Loading global plugins from /home/diningyo/.sbt/1.0/plugins [info] Loading project definition from /home/diningyo/prj/test_chisel_hw/project [info] Loading settings for project test_chisel_hw from build.sbt ... [info] Set current project to test_chisel_hw (in build file:/home/diningyo/prj/test_chisel_hw/) [info] Compiling 1 Scala source to /home/diningyo/prj/test_chisel_hw/target/scala-2.11/classes ... [warn] there were 9 feature warnings; re-run with -feature for details [warn] one warning found [info] Done compiling. [warn] Multiple main classes detected. Run 'show discoveredMainClasses' to see the list [info] Packaging /home/diningyo/prj/test_chisel_hw/target/scala-2.11/test_chisel_hw_2.11-0.1.0-SNAPSHOT.jar ... [info] Done packaging. [info] Running Test [info] [0.002] Elaborating design... [info] [0.809] Done elaborating. Total FIRRTL Compile Time: 235.3 ms Total FIRRTL Compile Time: 73.5 ms file loaded in 0.12866593 seconds, 7 symbols, 2 statements [info] [0.000] SEED 1550930781605 [info] [0.001] io.in0 = 100 // peekで取得した値が表示された [info] [0.001] io.in1 = 10 [info] [0.002] io.out = 110 test Top Success: 1 tests passed in 6 cycles in 0.012042 seconds 498.25 Hz [info] [0.002] RAN 1 CYCLES PASSED // テスト・ハーネス内で1cycleが経過して、テストにPASSした [success] Total time: 6 s, completed 2019/02/23 23:06:23
- テストにFAILした場合 成功すると、”成功した”といってあっさり終わるので比較のためにFAILした時のログも一緒に載せておく。
file loaded in 0.116302628 seconds, 7 symbols, 2 statements [info] [0.001] SEED 1550930904598 [info] [0.002] EXPECT AT 1 io_out got 110 expected 111 FAIL // 期待値が111なのに110が返ってきた [info] [0.002] io.in0 = 100 [info] [0.002] io.in1 = 10 [info] [0.002] io.out = 110 test Top Success: 0 tests passed in 6 cycles in 0.011300 seconds 530.96 Hz [info] [0.003] RAN 1 CYCLES FAILED FIRST AT CYCLE 1 [success] Total time: 6 s, completed 2019/02/23 23:08:26
テストの改良
ここまでで基本的なテストの形を見たので、少しテストを改良してみよう。 やることは以下の2つだ。
Top
モジュール用のテスターを作成してTest
オブジェクトからテストコードを分離Top
モジュールのテストをScala側で実装した期待値関数との比較に変更
Top
モジュール用のテスター
専用のテストクラスを作成する場合にはPeekPokeTester
を継承してクラスを作成すればOKだ。
class TopTester(c: Top) extends PeekPokeTester(c) { poke(c.io.in0, 100) // pokeで入力端子に値を入力出来る poke(c.io.in1, 10) step(1) // step(N)で指定したサイクルが経過 expect(c.io.out, 111) // expectで期待値と比較 println(s"io.in0 = ${peek(c.io.in0)}") println(s"io.in1 = ${peek(c.io.in1)}") println(s"io.out = ${peek(c.io.out)}") }
Top
用のテスターを作ったので、Test
オブジェクトで呼び出すテスターを作成したものに変更する。
iotesters.Driver.execute(args, () => new Top(32, 32)) { c => new TopTester(c) }
テストを改良
続いてテストの改良だ。作成したTopTester
はPeekPokeTester
を継承してChisel用のテスト関数が使えるだけ、通常のScalaのクラスなので、クラス内に各種関数を実装することも可能だ。
ということで関数を定義して、テストをランダム化してみよう。
先ほど定義したTopTester
を以下のように変更する。
class TopTester(c: Top, in0bits: Int, in1bits: Int) extends PeekPokeTester(c) { import scala.util.Random // データを入力 def feedData(in0: BigInt, in1: BigInt): Unit = { poke(c.io.in0, in0) poke(c.io.in1, in1) } def getData(): (BigInt, BigInt, BigInt) = { val in0Mask = (BigInt(1) << in0bits) - 1 val in1Mask = (BigInt(1) << in1bits) - 1 val in0 = BigInt(r.nextLong()) & in0Mask val in1 = BigInt(r.nextLong()) & in1Mask val eMask = (BigInt(1) << (in0bits + 1)) - 1 val e = (in0 + in1) & eMask (in0, in1, e) } val r = new Random for (i <- 0 until 100) { println(s"- ${i} -") val (in0, in1, exp) = getData() feedData(in0, in1) step(1) println(s"io.in0 = 0x${peek(c.io.in0).toLong.toHexString}") println(s"io.in1 = 0x${peek(c.io.in1).toLong.toHexString}") println(s"io.out = 0x${peek(c.io.out).toLong.toHexString}") println(s"exp = 0x${exp.toLong.toHexString}") expect(c.io.out, exp) } }
TopTester
のパラメータを増やしたので、テストの呼び出し側も合わせて変更
object Test extends App { val (in0Bits, in1Bits) = (32, 32) iotesters.Driver.execute(args, () => new Top(in0Bits, in1Bits)) { c => new TopTester(c, in0Bits, in1Bits) } }
そして実行する。 100回実行されるため、最初と最後だけ抜粋。
[info] Compiling 1 Scala source to /home/diningyo/prj/test_chisel_hw/target/scala-2.11/classes ... [warn] there were 9 feature warnings; re-run with -feature for details [warn] one warning found [info] Done compiling. [warn] Multiple main classes detected. Run 'show discoveredMainClasses' to see the list [info] Packaging /home/diningyo/prj/test_chisel_hw/target/scala-2.11/test_chisel_hw_2.11-0.1.0-SNAPSHOT.jar ... [info] Done packaging. [info] Running Test [info] [0.002] Elaborating design... [info] [0.979] Done elaborating. Total FIRRTL Compile Time: 291.3 ms Total FIRRTL Compile Time: 87.1 ms file loaded in 0.156067517 seconds, 7 symbols, 2 statements [info] [0.001] SEED 1550935273988 [info] [0.002] - 0 - [info] [0.003] io.in0 = 0x9705f492 [info] [0.003] io.in1 = 0x828ec96f [info] [0.003] io.out = 0x11994be01 [info] [0.003] exp = 0x11994be01 〜略〜 [info] [0.028] - 99 - [info] [0.028] io.in0 = 0x3b17ee79 [info] [0.028] io.in1 = 0x986016e8 [info] [0.028] io.out = 0xd3780561 [info] [0.028] exp = 0xd3780561 test Top Success: 100 tests passed in 105 cycles in 0.041823 seconds 2510.61 Hz [info] [0.029] RAN 100 CYCLES PASSED [success] Total time: 3 s, completed 2019/02/24 0:21:15
まとめ
結構長くなりましたが、まとめを。。。
- Chiselのテスト作る場合は
chisel.iotesters
ライブラリが必要 - 専用のテスターを作るには
PeekPokeTester
を継承して作成 - テスト実装には以下の3つの関数を使用する
- poke : 入力を与える
- peek : テストモジュールの端子の値を取得
- expect : 期待値と端子の情報を比較
最後に今回作ったコードを全部貼っておきますので興味があれば試してみてください。
- biuld.sbt
ChiselのRTL生成&テスト実行の際のsbtの設定ファイル
- Top.scala