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

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

Chiselで作ったモジュールをPeekPokeTesterでテストするまでの流れのまとめ

スポンサーリンク

前回のChiselの記事では今更ではあるがChiselでデザインしたハードウェアをVerilogのRTLに変換するためにやることについてをまとめた。

www.tech-diningyo.info

今日はついでなので、前回の続きというかその前の段階とかに位置しているはずの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モジュールのテストを実施することが可能だ。

github.com

このテスト・ハーネスは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がElaborateTestの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)
}
テストを改良

続いてテストの改良だ。作成したTopTesterPeekPokeTesterを継承して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の設定ファイル

ChiselのRTL生成&テストの実装サンプル