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

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

ChiselFlatSpecを使った振る舞い駆動開発(BDD)によるハードウェア実装とテストの例

スポンサーリンク

前回の記事でChiselFlatSpecを使うための前段階として、ベースとなっているScalaのテストハーネスScalaTestについて気になっていた部分を調査していった。

www.tech-diningyo.info

前回の終わりに書いた通り、今日は簡単な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の状態)。

f:id:diningyo-kpuku-jougeki:20190323225132p:plain

この波形を箇条書きで書くと、以下の4点になる。

  • wrenを0x1にするとその時のwrDataFIFOに書き込まれ、countが1増える
  • rdenを0x1にするとFIFO内部の次のデータがrdDataに出力され、countが1減る
  • wrenrdenが同時に0x1になるとcountはそのままでrdDataが更新される
  • emptyFIFO内部にデータがある場合にLowになる
  • fullFIFO内部の全バッファにデータが入るとHighになり、それ以外はLowのままになる。

またハードウェア・ジェネレータとしての機能要件を定義しておく。

  • データのビット幅が可変に出来ること(0 < dataBits)
  • FIFO内部のバッファ段数を可変に出来ること(0 < fifoStage)

まあ、すごく単純にデータビット幅と内部のFIFO段数がいじれる、というだけのもの。

プロジェクトの作成

では早速作成していこう。 まずはプロジェクトを作るところからだが、プロジェクトはChisel-templateをベースにする。

github.com

ここのデータをgit cloneするか、zipファイルとしてダウンロードして適当な名前で展開する。

$ wget https://github.com/freechipsproject/chisel-template/archive/release.zip
$ unzip release.zip
$ mv chisel-fifo

上記の様に作成した"chisel-fifo"の中に含まれているbuild.sbtIntellij IDEAで取り込んでプロジェクトを作成する。方法は以前に書いた記事があるのでこちらを参照のこと。

www.tech-diningyo.info

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にセットする

pushAndPoppush/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にするとその時のwrDataFIFOに書き込まれ、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

FIFOTesterargsで波形を取る設定にしているので、シミュレーションの実行ディレクトリにはVCDの波形が生成されているのでこれを見てみよう。

f:id:diningyo-kpuku-jougeki:20190323225152p:plain

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のスタイルでハードの検証が実行可能なのはとても魅力的に感じた。