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

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

iotestersからChiselTestへの移行を考える

スポンサーリンク

この記事はHDL (SystemVerilog/Verilog/VHDL/Chisel/etc.) Advent Calendar20日目の記事です。 Chiselも含まれてたので、Chiselネタで何か、、、、と考えた結果、あんまりちゃんと調べて&まとめてなかったChiselTestについて書くことにしました。

ChiselTest

ChiselTestchisel-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への移行をする際に必要な変更を見ていきます。 大きく次の変更が必要になります。

  1. build.sbtの修正
  2. テストクラスの宣言
  3. テスト実行関数及び引数の変更
  4. テスト記述の変更

それぞれ個別に見ていきます。

build.sbtの修正

ChiselTestを使うためにはbuild.sbtlibraryDependenciesに設定を追加すれば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クラス自体がScalaTestFlatSpecクラスを継承していましたが、ChiselTestではライブラリ内部でScalaTestに依存しない作りへと変更されました。 またChiselScalatestTesterにはChiselのテストに必要となる各種メソッド等が実装されています。

テストの実行:Driver -> test

次にテストの実行部分です。iotestersでは次のようにiotesters.Driverexecuteを呼び出す形になっていました。

    iotesters.Driver.execute(Array(), () => new GCD) {
      c => new GCDUnitTester(c)
    } should be (true)

この時c => new GCDUnitTester(c) の部分はPeekPokeTesterクラスを継承したクラスとなります。 この部分はChiselTestではChiselScalatestTestertestメソッドを呼び出す形となり、多少スッキリしました。

    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ファイルのダンプについてはWriteVcdAnnotationwithAnnotationに渡すことでもダンプできるようになっていますが、このアノテーションを有効にする仕組みがChiselScalatestTester自体に組み込まれています。 そのため次のようにsbtからのテスト実行時にwriteVcdオプションを指定することで波形ファイルを生成可能です。

testOnly gcd.GcdTesters2 -- -DwriteVcd=1

信号の操作

最後にテスト対象のモジュールの信号に操作についてです。iotestersではPeekPokeTesterpeek/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になっている。)

f:id:diningyo-kpuku-jougeki:20211219230828j:plain

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)に変化する結果となりました。

f:id:diningyo-kpuku-jougeki:20211219230842j:plain

ready-valid信号を使ったハンドシェークのようにデフォルトが0valid1の時にトランザクションが有効、といったプロトコルを書くのに適している、との記載が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のテストコードを読んでみるのが良いかと思います。(そして教えてください。)