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

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

Chiselの文法 - 入門編 〜その10:Chiselのテスト機構〜

スポンサーリンク

Chiselの文法入門の続きで今回は第10回目&最後
前回の終わりに書いたとおり、Chiselのテストの仕組みについてを解説します。

Chisel入門編〜その10:Chiselのテスト機構〜

Chiselには生成したモジュールをテストするためのパッケージが含まれています。
そのパッケージが

  • chisel3.iotesters

になります。
このパッケージ以下に含まれてるテスト用のクラスを使うことで自分が作成したChiselのモジュールに対してテストを作成&実行できるようになっています。
早速簡単なテストを作成してみましょう。
なお、テストの説明するにあたって以下を前提としていますのでご注意ください。

  • chisel-templateを使用したプロジェクトの構成を使用していること

テスト対象として前回の記事で作製したメモリを少し変更したモジュールを使用します(以下)
変更点は以下の3点です

  • メモリのサイズ/データのビット幅指定を可能にした
  • byte単位の書き込みのためのストローブ信号を追加
  • デバッグポートを削除

  • src/main/scala/MyMem.scala

class MyMem (numOfWords: Int, dataBits: Int) extends Module {
  val io = IO(new Bundle {
    val addr = Input(UInt(log2Ceil(numOfWords).W))
    val wren = Input(Bool()) // 1'b1でライト
    val wrstrb = Input(UInt((dataBits / 8).W))
    val wrdata = Input(UInt(dataBits.W))
    val rddata = Output(UInt(dataBits.W))
  })
  // メモリはUInt(8.W)x4で1wordのメモリ
  val m = Mem(numOfWords, Vec(io.wrstrb.getWidth(), UInt(8.W)))

  // 32bitを8bitx4のVecに変換
  val wrdata = Wire(Vec(io.wrstrb.getWidth(), UInt(8.W)))

  // 8bit毎にVecに格納
  for (i <- 0 until wrdata.length) {
    wrdata(i) := io.wrdata(i + 7, i)
  }

  when (io.wren) {
    m.write(io.addr, wrdata, io.wrstrb.toBools.reverse)
  }

  // rddataはMemがVec(4, UInt(8.W))なので
  // その形で返却される
  val rddata = m.read(io.addr)
  io.rddata := RegNext(Cat(rddata))
}

PeekPokeTesterを使ったテスト用クラスの作成

テストはiotestersに含まれるPeekPokeTesterを使って作ったものを同じパッケージに含まれるDriverを使って実行することで実行可能です。
iotesters.{Driver, PeekPokerTester}を使った最小のテストは以下のような形になります。

import chisel3.iotesters.{Driver, PeekPokeTester}

object Test extends App {
  Driver.execute(Array[String](<executeの引数>), () => new <テスト対象のモジュール>) {
    c => new PeekPokeTester(c) {
      // テスト記述(後述)
    }
  }
}

上記のテスト用オブジェクトを以下のように実行するとPeekPokeTesterの"テスト記述"部分がテストとして実行されます。

sbt "runMain Test"

PeekPokeTesterのテスト用メソッド

テストのシナリオはPeekPokeTesterに実装されている以下のメソッドを用いて、各端子を制御することで構築していきます。

メソッド名(引数リスト) 説明
reset([<リセットのサイクル数>]) リセットの発行
poke(<モジュールの入力端子>, <設定する値>) 入力端子に値をセット
<格納先変数> = peek(<モジュールの出力端子>) 出力端子の値を取得
step(<ステップ数>) <ステップ数>に指定した分だけシミュレーションのサイクルを進める
expect(<モジュールの出力端子>, <期待値>, [<エラー時のメッセージ>]) <出力端子>を<期待値>と比較する

上記のメソッドを使用して、冒頭に紹介したメモリモジュールの簡単なライト&リードの試験を実施してみましょう。

import chisel3.iotesters.{Driver, PeekPokeTester}

object Test extends App {
  Driver.execute(args, () => new MyMem(256, 32)) {
    c => new PeekPokeTester(c) {
      reset(10)
      // ライトに関連した端子に値を設定
      poke(c.io.addr, 0x10)
      poke(c.io.wren, true)
      poke(c.io.wrstrb, 0xf)
      poke(c.io.wrdata, 0x12345678)

      // stepで1cycle進める
      step(1)

      // ライトの制御端子をfalseに設定
      poke(c.io.wren, false)

      // リード
      poke(c.io.addr, 0x10)

      // 1cycle後にリードデータが出てくるので1cycle進める
      step(1)
      expect(c.io.rddata, 0x12345678)
    }
  }
}

簡単なテストを作成したので、実行してみましょう。
"src/main/scala"の下にメインオブジェクトが存在するのでsbtのrunMainを使って実行することが可能です。

sbt "runMain Test"
[info] Done compiling.
[info] Packaging /home/diningyo/prj/study/2000_chisel/500_learning-chisel3/subprj/tt/target/scala-2.11/tt_2.11-2.0.jar ...
[info] Done packaging.
[info] Running Test
[info] [0.002] Elaborating design...
[info] [1.071] Done elaborating.
Total FIRRTL Compile Time: 440.3 ms
Total FIRRTL Compile Time: 160.7 ms
file loaded in 0.29610121 seconds, 83 symbols, 56 statements
[info] [0.001] SEED 1560954789798
[info] [0.004] EXPECT AT 2   io_rddata got 2017238735 expected 305419896 FAIL
test MyMem Success: 0 tests passed in 17 cycles in 0.020079 seconds 846.64 Hz
[info] [0.005] RAN 2 CYCLES FAILED FIRST AT CYCLE 2

ログの部分をご覧いただくとわかる通り、FAILしてしまいました。
表示が10進数でわかりづらいので、ログを自分で追加してみましょう。
テストのexpectの直後にpeekメソッドを使った以下のようなプリント文を追加して再度実行してみましょう

  • 追加するプリント文
    • expectで取得したrddata端子の値を16進数で表示
println(f"c.io.rddata = 0x${peek(c.io.rddata)}%08x")

実行結果は以下のようになります。

[info] [0.001] SEED 1560955198929
[info] [0.004] EXPECT AT 2   io_rddata got 2018915346 expected 305419896 FAIL
[info] [0.005] c.io.rddata = 0x78563412
test MyMem Success: 0 tests passed in 17 cycles in 0.017396 seconds 977.21 Hz
[info] [0.005] RAN 2 CYCLES FAILED FIRST AT CYCLE 2
[success] Total time: 4 s, completed 2019/06/19 23:40:01

もうお気づきとは思いますがライトしたデータ0x12345678のデータのエンディアンがひっくり返っています。
結果が一致するように回路を修正します。
- src/main/scala/MyMem.scala

// ライトデータをVecに格納する部分を以下のように修正
  for (i <- 0 until wrdata.length) {
    //wrdata(i) := io.wrdata((i * 8) + 7, i * 8)
    wrdata(wrdata.length - i - 1) := io.wrdata((i * 8) + 7, i * 8)
  }

修正後の回路でテストを再実行すると以下のログのようにPASSすることが確認できました。

file loaded in 0.280343317 seconds, 83 symbols, 56 statements
[info] [0.002] SEED 1560955781414
[info] [0.005] c.io.rddata = 0x12345678
test MyMem Success: 1 tests passed in 17 cycles in 0.020041 seconds 848.25 Hz
[info] [0.006] RAN 2 CYCLES PASSED

Driver.executeの引数

先ほどの一般形を示した時にArray[String](<executeの引数>と書いた部分について補足しておきます。
この引数にはexecuteに指定可能な引数をArrayに格納して、それを渡すことで幾つかのパラメータを変更することが出来ます。
どのような引数が使えるかはDriverをsbtシェル上で実行すると確認することが可能です。

runMain chisel3.iotesters.Driver

実行した結果はかなり長いので以下のgistに載っけましたので、ご興味あればご覧になってください。

筆者が主に使うのは以下のオプションです。オプションには"short-name"が定義されているので、こっち使ったほうが楽です。
ご覧いただくわかる通り、バックエンドのシミュレータには"vcs"や"iverilog"も使用できる仕組みになっているようです。
一度も指定したこと無いから使えるかは不明ですが、少なくともvcsは普通に動いてるんじゃないかと思ってます。その理由ですがRocket-chipの環境でvcsが動かせる作りになってるからです。 #今度こっそり試してみよう。。

オプション デフォルト値 説明
-tn / --top-name <top-level-circuit-name> - 作成する回路のトップモジュールの名前を指定
-td / --target-dir <target-directory> - 出力先のディレクトリを指定
-tiv / --is-verbose - シミュレーション時に詳細なログを出力
-tbn / --backend-name <firrtl|treadle|verilator|ivl|vcs> treadle バックエンドのシミュレータを指定
-tgvo / --generate-vcd-output off on指定時に波形をダンプする

Driver.executeの引数に渡す際には、以下のようにオプションと設定値を別の要素で指定するか、同じ要素に書く場合には=での連結が必要な点にご注意ください。

Driver.execute(Array("--top-name", "SimDTM")) // 別々の要素にする
Driver.execute(Array("--top-name=SimDTM"))    // "="で連結

専用のテストクラスの作成

先ほど紹介した方法は、とりあえず簡単にでいいからテストを作りたい時には良いのですが、本格的に検証をする場合には不便です。
なのでPeekPokeTesterを継承したテスト対象用のモジュールを作ったほうがより、使い勝手が良くなります。
先ほどのPeekPokeTesterのボディの部分をベースにしてテスト用のクラスを作成してみましょう。
例えば以下のような感じです。

class MyMemTester(c: MyMem) extends PeekPokeTester(c) {

  def idle(cycle: Int): Unit = {
    poke(c.io.wren, false)
    step(cycle)
  }

  def write(addr: BigInt, data: BigInt, strb: BigInt): Unit = {
    // ライトに関連した端子に値を設定
    poke(c.io.addr, addr)
    poke(c.io.wren, true)
    poke(c.io.wrstrb, strb)
    poke(c.io.wrdata, data)
    // stepで1cycle進める
    step(1)
  }

  def read(addr: BigInt, exp: BigInt): BigInt = {
    // リード
    poke(c.io.addr, addr)

    // 1cycle後にリードデータが出てくるので1cycle進める
    step(1)
    expect(c.io.rddata, exp)
  }
}

使うときはDriver.executeを使って実行するのですが、先ほどと異なるのはPeekPokeTesterインスタンスする代わりに作成したMyMemTesterインスタンスする点です。
今度はテスト制御用のメソッドを定義しているため、そのメソッドベースでテストのシナリオ作成が可能になります。

object Test extends App {
  Driver.execute(args, () => new MyMem(256, 32)) {
    c => new MyMemTester(c) {
      reset(10)
      // ライトに関連した端子に値を設定
      write(0x10, 0x12345678, 0xf)
      idle(1)

      // リード
      read(0x10, 0x12345678)
    }
  }
}
  • 実行結果
    • 先ほどと同様に試験がPASSすることが確認できました。
Total FIRRTL Compile Time: 166.3 ms
file loaded in 0.302528292 seconds, 83 symbols, 56 statements
[info] [0.001] SEED 1561040583277
test MyMem Success: 1 tests passed in 18 cycles in 0.018231 seconds 987.33 Hz
[info] [0.004] RAN 3 CYCLES PASSED

ChiselFlatSpecを使ったテスト

PeekPokeTesterを使えば、自分の作成したモジュールの外部端子を制御して、テストを実行することが出来ることがおわかりいただけたと思います。
ただ、実際にしっかりした検証をやるには他にも様々検証を行い、それらをまとめて実行し、結果について確認したくなるかと思います。
ScalaにはScalaTestというテスティングフレームワークが存在しており、ChiselではScalaTestの中のFlatSpecを継承したChiselFlatSpecという振る舞い駆動開発用のクラスが用意されています。
これを使用することでテスト項目と実際のテストを紐付けて、個々のテストの結果を管理することが可能です。
使い方自体はScalaTestのFlatSpecそのままなので、使用されたことがある方には特に難しい点は無いと思います。
ChiselFlatSpecを使って作るテストは以下のような形が一般的かと思います。

  • src/test/scala/MyMemTest.scala
    • sbtからテストを制御する関係上、"src/test/scala"以下にファイルを配置する点に注意
    • 先ほど実装したMyMemTesterをこちらのファイルに移動してます。
class <テストクラス名> extends ChiselFlatSpec {
  behavior of "テストモジュール名" // 必須ではない

  // 以下がひとつのテスト項目に対するテスト記述
  it should "期待する振る舞いの記述" in {
    Driver.execute(Array[String](<Driver.executeの引数(前述)>),
    () => new <テストモジュール>) {
      c => new <テスタークラス> {
        // テスト記述
      }
    } should be (true)
  }
}
  • src/test/scala/MyMemTest.scala
    • sbtからテストを制御する関係上、"src/test/scala"以下にファイルを配置する点に注意してください。
    • コレに伴い先ほど上で実装したMyMemTesterをこちらのファイルに移動してます。
    • テストが超適当ですがご容赦ください。。
class MyMemTest extends ChiselFlatSpec {
  behavior of "MyMem"

  it should "指定したアドレスにデータがライト出来る" in {
    Driver.execute(Array(""),
      () => new MyMem(256, 32)) {
      c => new MyMemTester(c) {
        write(0x10, 0x12345678, 0xf)
        idle(1)

        // リード
        read(0x10, 0x12345678)
      }
    } should be (true)
  }

  it should "指定したアドレスからデータがリード出来る" in {
    Driver.execute(Array(""),
      () => new MyMem(256, 32)) {
      c => new MyMemTester(c) {
        write(0x10, 0x12345678, 0xf)
        idle(1)

        // リード
        read(0x10, 0x12345678)
      }
    } should be (true)
  }

  it should "指定したアドレスにストローブを使ったデータ・ライト出来る" in {
    Driver.execute(Array(""),
      () => new MyMem(256, 32)) {
      c => new MyMemTester(c) {
        val wrdata = 0x12345678
        for (strb <- 0 to 16) {
          val addr = strb
          write(addr, wrdata, strb)
        }

        idle(1)

        for (strb <- 0 to 16) {
          val addr = strb
          val mask = Range(0, 4).map(i => {
            val a = if (((strb >> i) & 0x1) == 0x1) 0xff else 0x0
            a << (i * 8)
          }).reduce(_|_)
          val exp = wrdata & mask
          read(addr, exp)
        }
      }
    } should be (true)
  }
}

上記テストを実行するにはsbtのtestコマンドかtestOnlyコマンドを使用します。
testコマンドは現在作業中のプロジェクト以下の全てのChiselFlatSpecを継承して作成されたテストクラスのテストを実行します。リグレッションの際にはこのコマンドを使用すると良いど思います。
testOnlyコマンドは実行するテストクラスを指定して実行する際に使用するコマンドです。

sbt "testOnly <テストクラス>

<テストクラス>はパッケージ名も含んだ形で指定する必要があります。
上記のテストクラスを実行すると以下のようPASSすることが確認できました。

testOnly MyMemTest
[info] Compiling 1 Scala source to /home/diningyo/prj/study/2000_chisel/500_learning-chisel3/subprj/tt/target/scala-2.11/test-classes ...
[info] Done compiling.
[info] [0.002] Elaborating design...
[info] [0.134] Done elaborating.
Total FIRRTL Compile Time: 386.8 ms
Total FIRRTL Compile Time: 161.1 ms
file loaded in 0.275520294 seconds, 83 symbols, 56 statements
[info] [0.001] SEED 1561127213913
test MyMem Success: 1 tests passed in 8 cycles in 0.078703 seconds 101.65 Hz
[info] [0.065] RAN 3 CYCLES PASSED
[info] [0.000] Elaborating design...
[info] [0.051] Done elaborating.
Total FIRRTL Compile Time: 46.5 ms
Total FIRRTL Compile Time: 45.5 ms
file loaded in 0.062866345 seconds, 83 symbols, 56 statements
[info] [0.000] SEED 1561127215121
test MyMem Success: 1 tests passed in 8 cycles in 0.001611 seconds 4966.48 Hz
[info] [0.001] RAN 3 CYCLES PASSED
[info] [0.000] Elaborating design...
[info] [0.004] Done elaborating.
Total FIRRTL Compile Time: 34.1 ms
Total FIRRTL Compile Time: 43.7 ms
file loaded in 0.056955448 seconds, 83 symbols, 56 statements
[info] [0.000] SEED 1561127215296
test MyMem Success: 17 tests passed in 40 cycles in 0.012395 seconds 3227.11 Hz
[info] [0.011] RAN 35 CYCLES PASSED
[info] MyMemTest:
[info] MyMem
[info] - should 指定したアドレスにデータがライト出来る
[info] - should 指定したアドレスからデータがリード出来る
[info] - should 指定したアドレスにストローブを使ったデータ・ライト出来る
[info] ScalaTest
[info] Run completed in 1 second, 727 milliseconds.
[info] Total number of tests run: 3
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 3, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 3, Failed 0, Errors 0, Passed 3

ということでChiselに組み込まれているテストフレームワークを使ったテストの実行の仕方についてを紹介しました。

入門編のまとめ

そこそこ長いシリーズになりましたが、これで自分的にはChiselを学ぶにあたって必要(と思ってる)話は書いたつもりです。
今のところ感じているChiselを使うメリットとして大きいのは以下の2点です。

パラメタライズ機能は使いこなせば、1つのChiselソースが何十倍/何百倍のRTLを生み出せることになります。まだまだ使いこなせているとは言いがたいので、この辺は精進あるのみ、、です。
またScalaTestをベースにしたテストフレームワークが用意されておりtreadleやverilatorまでの一連のシミュレーションパスをChisel側で勝手にやってくれるのは非常に楽だと思っています。
あとは、何より色々試しがいがあって楽しい!!!これにつきます(笑)

デメリットは、、、、

  • 言語的な難しさ

これが一番大きい気がします。。自分の場合だとJavaはまっっっったくと言っていいほど触ったこと無い、、、という状態から始めたこともあってScalaが生成するJavaのコードを読んで理解と言ったことも出来ませんでした。もちろん関数型言語も触ったことなかったので、しっくり来るまでにかなーーり時間がかかりました。

あとはまだ非同期リセットが使えないとかシミュレーション向けの記述でforkが使えないとか細々したのもあります。これら2つはいまサポートに向けていろいろ開発が進んでいるようなので今後に期待。

Chisel流行れ!!とは思っていますが、別にこれが既存のHDLレベルの言語を置き換える決定打と言うつもりも無いです。
モチベーションとしては「もっと楽に書けるならそうしたい」って気持ちが一番大きいので、HDL比で楽にハードウェアを実装出来そうなネタは追いかけていきたいです。既に他にも気になるものが出てきているので、そのうちブログで取り上げたいと思います。
とはいえ、今のところChiselにはかなり魅力を感じているので、引き続き色々調査していく予定です。

ということで、Chisel入門編でした。 ここまでお付き合い下さった方がいた場合は、読んでいただきありがとうございました。

今後は今絶賛解析中のRocket-Chipについての垂れながしの記事が多くなる、、気がしてます。気になる方は引き続きよろしくお願いします。