前回の記事でChiselFlatSpec
を使うための前段階として、ベースとなっているScalaのテストハーネスScalaTestについて気になっていた部分を調査していった。
前回の終わりに書いた通り、今日は簡単なChiselのモジュールを作成し、それをChiselFlatSpec
を使ったBDDスタイルのテスト環境でテストしてみようと思う。
かなり長くなりましたがよければ読んでみてくださいm(_ _)m
ChiselFlatSpec
を使ったChiselモジュールの検証環境構築
ということで早速環境を整えていこう。 進めている作業の関係で簡単なFIFOが必要になったので、それを試験する環境を構築することにする。
作成するFIFOモジュールの仕様
まずはものすごく簡単に以下のようなものにする。
入出力端子
端子名 | 方向 | ビット幅 | 説明 |
---|---|---|---|
clock | I | 1 | クロック |
reset | I | 1 | リセット |
wren | I | 1 | ライトイネーブル |
wrData | I | dataBits | ライトデータ |
rden | I | 1 | リードイネーブル |
rdData | O | dataBits | リードデータ |
count | O | dataBitsに合わせて生成 | 内部のデータ数 |
empty O | 1 | FIFOが空の状態を示す | |
full | O | 1 | FIFOフル |
※1 dataBitsはChiselで作成するクラスのパラメタライズ出来るようにする。
動作波形
動作は以下のような感じ(波形書きやすくするためにFIFOの段数は2の状態)。
この波形を箇条書きで書くと、以下の4点になる。
wren
を0x1にするとその時のwrData
がFIFOに書き込まれ、count
が1増えるrden
を0x1にするとFIFO内部の次のデータがrdData
に出力され、count
が1減るwren
とrden
が同時に0x1になるとcount
はそのままでrdData
が更新されるempty
はFIFO内部にデータがある場合にLowになるfull
はFIFO内部の全バッファにデータが入るとHighになり、それ以外はLowのままになる。
またハードウェア・ジェネレータとしての機能要件を定義しておく。
- データのビット幅が可変に出来ること(0 < dataBits)
- FIFO内部のバッファ段数を可変に出来ること(0 < fifoStage)
まあ、すごく単純にデータビット幅と内部のFIFO段数がいじれる、というだけのもの。
プロジェクトの作成
では早速作成していこう。
まずはプロジェクトを作るところからだが、プロジェクトはChisel-templateをベースにする。
ここのデータをgit clone
するか、zipファイルとしてダウンロードして適当な名前で展開する。
$ wget https://github.com/freechipsproject/chisel-template/archive/release.zip $ unzip release.zip $ mv chisel-fifo
上記の様に作成した"chisel-fifo"の中に含まれているbuild.sbt
をIntellij IDEAで取り込んでプロジェクトを作成する。方法は以前に書いた記事があるのでこちらを参照のこと。
diningyo@diningyo-pc:~/prj/study/2000_chisel/chisel-fifo$ tree . ├── README.md ├── build.sbt <-- これをインポートする ├── project │ ├── build.properties │ └── plugins.sbt ├── scalastyle-config.xml ├── scalastyle-test-config.xml └── src ├── main │ └── scala │ └── gcd │ └── GCD.scala └── test └── scala └── gcd ├── GCDMain.scala └── GCDUnitTest.scala
Chisel-templateにサンプルで付属しているGCDは不要なので削除してOK。
テスト環境の作成
テスト対象のFIFOクラスの作成
前回調査したとおりChiselFlatSpec
のベースになっているFlatSpec
はBDDを行うためのものだった。
ということでその理念に則り作成するFIFOのテスト環境から作成していく。
とはいえ、テスト対象となるFIFOの皮くらいは無いといけないので、実装するFIFOクラスを作成する。
// src/main/scala/Fifo.scala package fifo import chisel3._ class SimpleIO(dataBits: Int, fifoStage: Int) extends Bundle { val wren = Input(Bool()) val wrData = Input(UInt(dataBits.W)) val rden = Input(Bool()) val rdData = Output(UInt(dataBits.W)) val empty = Output(Bool()) val full = Output(Bool()) override def cloneType: SimpleIO.this.type = new SimpleIO(dataBits, fifoStage).asInstanceOf[this.type] } class Fifo(dataBits: Int, fifoStage: Int) extends Module { val io = IO(new SimpleIO(dataBits, fifoStage)) io := DontCare }
とりあえずFifo
というまんまの名前で最初に書いたIOをSimpleIO
として定義した。
これは後でIOのプロトコルを変えることも想定したためだ。
io
のみを定義して中身が何もない状態だと、エラボレートすら通らないので、それを避けるためにDontCare
を指定している。
テスト用クラスの作成
次はChiselFlatSpec
を継承したFIFOTester
クラスとユニットテスト用のクラスであるFifoUnitTester
の2つのクラスを作成する。
とりあえずこんな感じ。
// src/test/scala/FIFOTester.scala package fifo import chisel3.iotesters.{Driver, PeekPokeTester, ChiselFlatSpec} class FifoUnitTester(c: Fifo) extends PeekPokeTester(c) { } class FifoTester extends ChiselFlatSpec { // シミュレーション時の引数 val args = Array( "--generate-vcd-output", "on", // 波形取る設定 "--target-dir", "test_run_dir/fifo_test", // 実行ディレクトリ "--top-name", "dtm_top" // テスト環境のトップモジュール名 ) val testDataBits = 32 // パラメタライズ試験以外で使用するデータのビット幅 val testFifoStage = 16 // パラメタライズ試験以外で使用するFIFOの段数 "Fifo" should "set io.wrData to fifo buffer when io.wren is high" in { Driver.execute(args, () => new Fifo(testDataBits, testFifoStage)) { c => new FifoUnitTester(c) { fail } } should be (true) } }
とりあえず動作波形の部分に書いたwren
に関する仕様をテストするためのテストケースを作成しておき、テストを問答無用でFAILさせるようにfail
を書いておく。
この時点でとりあえずテストを実行してみよう。をIntellij IDEAのsbt shellに"test"を入れて実行する。
test [info] [0.001] Elaborating design... [info] [0.058] Done elaborating. Total FIRRTL Compile Time: 220.1 ms Total FIRRTL Compile Time: 72.3 ms file loaded in 0.121189074 seconds, 8 symbols, 3 statements [info] [0.001] SEED 1553177064516 org.scalatest.exceptions.TestFailedException at org.scalatest.Assertions$class.newAssertionFailedException(Assertions.scala:528) 〜中略〜 at java.lang.Thread.run(Thread.java:748) [info] FifoTester: [info] fifo.Fifo [info] - should set io.wrData to fifo buffer when io.wren is high *** FAILED *** [info] org.scalatest.exceptions.TestFailedException was thrown. (FifoTester.scala:24) [info] ScalaTest [info] Run completed in 898 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 0, failed 1, canceled 0, ignored 0, pending 0 [info] *** 1 TEST FAILED *** <-- ちゃんとFAILした。 [error] Failed: Total 1, Failed 1, Errors 0, Passed 0 [error] Failed tests: [error] fifo.FifoTester [error] (Test / test) sbt.TestsFailedException: Tests unsuccessful [error] Total time: 1 s, completed 2019/03/21 23:04:25
FifoUnitTester
のメソッドの実装
ここからのやり方は人それぞれになるとは思うが、今回はPeekPokeTester
を継承して作ったFifoUnitTester
クラスにFifoに対してのアクションをメソッドとして実装して、それを使ってテストを構築し、実装を進めていくことにする。
早速テスト用のメソッドを実装していこう。
実装するのは以下の5つのメソッド
- push : Fifoにデータを書き込む
- pop : Fifoのデータを読み取る
- pushAndPop : Fifoにデータをセットしつつ、データを読み取る
- expectOutput : Fifoの出力ポートの期待値比較を行う読み取る
- setListData : 引数に指定したListのデータをFifoにセットする
pushAndPop
はpush
/pop
の作り方を変えればなんとかなる気はするんだけど、いまいちしっくりくるやり方が無いのでとりあえず作る。
class FifoUnitTester(c: Fifo) extends PeekPokeTester(c) { /** * Fifoにデータをセット * @param wrData: 設定するデータ */ def push(wrData: BigInt): Unit = { poke(c.io.wren, true) poke(c.io.wrData, wrData) step(1) poke(c.io.wren, false) } /** * Fifoのデータを読み取る * @return Fifoの出力データ */ def pop(): BigInt = { val rdData = peek(c.io.rdData) // 現在のリードデータを取得 poke(c.io.rden, true) // rdDataを更新 step(1) poke(c.io.rden, false) rdData } /** * Fifoにデータをセットしつつデータを読み取る * @param wrData: 設定するデータ * @return Fifoの出力データ */ def pushAndPop(wrData: BigInt): BigInt = { poke(c.io.wren, true) poke(c.io.wrData, wrData) val rdData = peek(c.io.rdData) // 現在のリードデータを取得 poke(c.io.rden, true) // rdDataを更新 step(1) poke(c.io.wren, false) poke(c.io.rden, false) rdData } /** * 出力ポートの期待値を比較 * @param expEmpty io.emptyの期待値 * @param expFull io.fullの期待値 * @param expCount io.countの期待値 * @param expRdData io.rdDataの期待値 * @return */ def expectOutput( expEmpty: Boolean, expFull: Boolean, expCount: Int, expRdData: BigInt): Boolean = { expect(c.io.empty, expEmpty) expect(c.io.full, expFull) expect(c.io.count, expCount) expect(c.io.rdData, expRdData) } /** * Fifoにデータをセット * @param data Fifoにセットするデータ */ def setListData(data: List[BigInt]): Unit = data.foreach { d => push(d) } }
テストの作成
BDDのスタイルだと、失敗するテストケース書く→そこを実装→テスト書く→実装→リファクタリング→テスト書く→実装・・・ってなるみたいだけど、ハードだとなかなか粒度が難しい。。
とりあえず最初に記載した波形図のとこに箇条書した仕様を振る舞いとして、テストケースを作成していく。
こんな感じ↓。 とりあえずひとつ目の
wren
を0x1にするとその時のwrData
がFIFOに書き込まれ、count
が1増える
についてテストを作成していく。
"Fifo" should "wrenを0x1にするとその時のwrDataがFIFOに書き込まれる" in { Driver.execute(args, () => new Fifo(dataBits, fifoStage)) { c => new FifoUnitTester(c) { val testVal = List(BigInt("deadbeaf", 16), BigInt("a0a0a0a0", 16)) // test reset() testVal.zipWithIndex.foreach { case(d: BigInt, i: Int) => { push(d) expectOutput( expEmpty = false, expFull = false, expCount = i + 1, expRdData = testVal(0)) } } step(1) expectOutput( expEmpty = false, expFull = false, expCount = testVal.length, expRdData = testVal(0)) } } should be(true) }
これを実行すると当然FAILになるので、これをPASSするようにFifo
クラスを実装していく。
やっぱりこれくらいの簡単なハードだと、それなりに作っちゃわないと動かないんだよな。
ということで大体必要な機能は実装しておく。
けどどんな感じでメッセージが出るかを確認するために、バグありの状態(emptyの動き)
package fifo import chisel3._ import chisel3.util.{MuxCase, log2Ceil} class SimpleIO(dataBits: Int, fifoStage: Int) extends Bundle { val wren = Input(Bool()) val wrData = Input(UInt(dataBits.W)) val rden = Input(Bool()) val rdData = Output(UInt(dataBits.W)) val count = Output(UInt((log2Ceil(fifoStage) + 1).W)) val empty = Output(Bool()) val full = Output(Bool()) override def cloneType: SimpleIO.this.type = new SimpleIO(dataBits, fifoStage).asInstanceOf[this.type] } class Fifo(dataBits: Int, fifoStage: Int) extends Module { val io = IO(new SimpleIO(dataBits, fifoStage)) require(0 < dataBits) require(0 < fifoStage) // log2Ceil(1) == 0 になるのでその場合だけ1に固定 val addrBits = if (fifoStage == 1) 1 else log2Ceil(fifoStage) /** * アドレスを更新 * @param currAddr * @return 更新後のアドレス */ def updataAddr(currAddr: UInt): UInt = { val wrap = currAddr === (fifoStage - 1).U return Mux(wrap, 0.U, currAddr + 1.U) } // データカウント val fifoCount = RegInit(0.U((addrBits + 1).W)) fifoCount := MuxCase(fifoCount, Seq( (io.rden && io.wren) -> fifoCount, io.rden -> (fifoCount - 1.U), io.wren -> (fifoCount + 1.U) )) // リード/ライトアドレス val rdAddr = RegInit(0.U(addrBits.W)) val wrAddr = RegInit(0.U(addrBits.W)) when (io.rden) { rdAddr := updataAddr(rdAddr) } when (io.wren) { wrAddr := updataAddr(wrAddr) } // 内部バッファ val fifoBuf = RegInit(VecInit(Seq.fill(fifoStage)(0.U(dataBits.W)))) when (io.wren) { fifoBuf(wrAddr) := io.wrData } // 出力ポートの接続 io.rdData := fifoBuf(rdAddr) io.empty := fifoDataNum =/= 0.U // バグ:emptyの動きが逆 io.count := fifoCount io.full := fifoCount === fifoStage.U }
この状態でテストを実行すると以下のようなメッセージが出てFAILする。
[IJ]sbt:chisel-module-template> test [info] Compiling 1 Scala source to /home/diningyo/prj/study/2000_chisel/chisel-fifo/target/scala-2.11/test-classes ... [info] Done compiling. [info] [0.001] Elaborating design... [info] [0.087] Done elaborating. Total FIRRTL Compile Time: 363.8 ms Total FIRRTL Compile Time: 187.6 ms file loaded in 0.295566402 seconds, 112 symbols, 88 statements [info] [0.002] SEED 1553343576097 [info] [0.009] EXPECT AT 1 io_empty got 1 expected 0 FAIL <-- リセット後にemptyがLowになっている [info] [0.010] EXPECT AT 2 io_empty got 1 expected 0 FAIL <-- データを入れたあとにemptyがHighになる [info] [0.010] EXPECT AT 3 io_empty got 1 expected 0 FAIL test Fifo Success: 13 tests passed in 9 cycles in 0.050373 seconds 178.67 Hz [info] [0.016] RAN 3 CYCLES FAILED FIRST AT CYCLE 1 [info] FifoTester: [info] Fifo [info] - should wrenを0x1にするとその時のwrDataがFIFOに書き込まれる *** FAILED *** [info] false was not true (FifoTester.scala:87) [info] ScalaTest [info] Run completed in 1 second, 323 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 0, failed 1, canceled 0, ignored 0, pending 0 [info] *** 1 TEST FAILED *** [error] Failed: Total 1, Failed 1, Errors 0, Passed 0 [error] Failed tests: [error] fifo.FifoTester [error] (Test / test) sbt.TestsFailedException: Tests unsuccessful [error] Total time: 3 s, completed 2019/03/23 21:19:37
FIFOTester
のargs
で波形を取る設定にしているので、シミュレーションの実行ディレクトリにはVCDの波形が生成されているのでこれを見てみよう。
fifoDataNum
が0x1になるとempty
が上がるっているのが確認できた。
これを修正してもう一度実行してみる。
io.empty := fifoDataNum === 0.U // 修正後
[IJ]sbt:chisel-module-template> test [info] Compiling 1 Scala source to /home/diningyo/prj/study/2000_chisel/chisel-fifo/target/scala-2.11/test-classes ... [info] Done compiling. [info] [0.001] Elaborating design... [info] [0.085] Done elaborating. Total FIRRTL Compile Time: 379.5 ms Total FIRRTL Compile Time: 175.0 ms file loaded in 0.273672674 seconds, 112 symbols, 88 statements [info] [0.001] SEED 1553344235965 test Fifo Success: 16 tests passed in 9 cycles in 0.044172 seconds 203.75 Hz [info] [0.012] RAN 3 CYCLES PASSED [info] FifoTester: [info] Fifo [info] - should wrenを0x1にするとその時のwrDataがFIFOに書き込まれる [info] ScalaTest [info] Run completed in 1 second, 296 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: 2 s, completed 2019/03/23 21:30:37
今度はPASSした。
残りのテストの作成
続いて、他の仕様に対してのテストも作成していこう。
it should "rdenを0x1にするとFIFO内部の次のデータがrdDataに出力され、countが1減る" in { Driver.execute(args, () => new Fifo(dataBits, fifoStage)) { c => new FifoUnitTester(c) { val testVal = List(BigInt("deadbeaf", 16), BigInt("a0a0a0a0", 16)) // リセット後の端子の確認 reset() // テストデータをセット setListData(testVal) // popの試験 testVal.zipWithIndex.foreach { case(exp: BigInt, i: Int) => { val expCount = testVal.length - i val expEmpty = expCount == 0 expectOutput( expEmpty, expFull = false, expCount = expCount, expRdData = exp) pop() } } step(1) expectOutput( expEmpty = true, expFull = false, expCount = 0x0, expRdData = BigInt("0", 16)) } } should be(true) } it should "wrenとrdenが同時に0x1になるとcountはそのままでrdDataが更新される" in { Driver.execute(args, () => new Fifo(dataBits, fifoStage)) { c => new FifoUnitTester(c) { val testVal = List(BigInt("deadbeaf", 16), BigInt("a0a0a0a0", 16)) val hasData = true val expCount = 1 reset() push(testVal(0)) expectOutput( expEmpty = false, expFull = false, expCount = 1, expRdData = testVal(0)) pushAndPop(testVal(1)) expectOutput( expEmpty = false, expFull = false, expCount = 1, expRdData = testVal(1)) } } should be(true) } it should "emptyはFIFO内部にデータがある場合にLowになる" in { Driver.execute(args, () => new Fifo(dataBits, fifoStage)) { c => new FifoUnitTester(c) { reset() expect(c.io.empty, true) for (i <- 0 until fifoStage) { push(i + 1) expect(c.io.empty, false) } step(1) } } should be(true) } it should "fullはFIFO内部の全バッファにデータが入るとHighになり、それ以外はLowのままになる。" in { Driver.execute(args, () => new Fifo(dataBits, fifoStage)) { c => new FifoUnitTester(c) { reset() for (i <- 0 until fifoStage) { expect(c.io.full, false) push(i + 1) } step(1) expect(c.io.full, true) step(1) } } should be(true) } it should "データのビット幅を可変に出来る(0 < dataBits)" in { for (i <- 1 to 32) { println(s"dataBits = $i") Driver.execute(args, () => new Fifo(i, dataBits)) { c => new FifoUnitTester(c) { val wrData = BigInt("1") << (i - 1) reset() push(wrData) expect(c.io.rdData, wrData) step(1) } } should be(true) } } it should "FIFO内部のバッファ段数を可変に出来ること(0 < fifoStage)" in { for (i <- 1 to 32) { println(s"fifoStage = $i") Driver.execute(args, () => new Fifo(dataBits, i)) { c => new FifoUnitTester(c) { reset() for (j <- 0 until i) { expect(c.io.full, false) push(j + 1) } step(1) expect(c.io.full, true) step(1) } } should be(true) } }
テスト結界についてはレポートのみを記載。
[info] FifoTester: [info] Fifo [info] - should wrenを0x1にするとその時のwrDataがFIFOに書き込まれ、countが1増える [info] - should rdenを0x1にするとFIFO内部の次のデータがrdDataに出力され、countが1減る [info] - should wrenとrdenが同時に0x1になるとcountはそのままでrdDataが更新される [info] - should emptyはFIFO内部にデータがある場合にLowになる [info] - should fullはFIFO内部の全バッファにデータが入るとHighになり、それ以外はLowのままになる。 [info] - should データのビット幅を可変に出来る(0 < dataBits) [info] - should FIFO内部のバッファ段数を可変に出来ること(0 < fifoStage) [info] ScalaTest [info] Run completed in 6 seconds, 288 milliseconds. [info] Total number of tests run: 7 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 7, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [info] Passed: Total 7, Failed 0, Errors 0, Passed 7 [success] Total time: 6 s, completed 2019/03/23 22:40:37
ということで、とりあえず簡単な回路を使ってChiselFlatSpec
を使ったテストの構築の手順をまとめてみた。
ChiselFlatSpec
を使うことで、Saclaで書いたテストをシームレスに実行できるし、実行に際しても特定のパターンのみの実行や、sbtと連携した自動テスト実行なども可能になるので慣れてくれば開発の効率を挙げれるようになると感じた。個人的にはScalaTestのテストハーネスとしての機能を使ってBDDのスタイルでハードの検証が実行可能なのはとても魅力的に感じた。