前回のChiselの記事ではchisel-templateをIntellij IDEAのプロジェクトにしてChiselの開発環境を構築する方法についてを紹介した。
今回は前回の終わりに書いたとおり、構築したchisel-templateプロジェクトに自前のモジュールを追加してテストを実行してVCDダンプによる波形デバッグまでの一連の流れをまとめてみようと思う。
Chisel-tempalteプロジェクトを使ったハードウェア開発
もともとのChisel-templateのgithub環境にはGCDのChiselサンプルデザインと、それに対応したテストが実装されている。
なのでこのGCDのコードをコピーして、必要な部分だけど書き換えると簡単にChisel-templateが準備している各種テストの環境をそのまま流用しながら、オリジナルのChiselデザインを開発することが可能だ。
早速もとのGCDのコードを修正して、ごく簡単なオリジナルのChiselデザインをChisel-template環境上で開発してみよう。
実装するモジュール - "MyReg"
これは極力簡単にするためなんの変哲もない8bitのレジスタにすることにした。
あえてverilogで書くとこんなの↓
module MyReg ( input wire clk ,input wire rst ,input wire wren ,input wire [7:0] wrdata ,output reg out ); always @(posedge clk) begin if (rst) begin out <= {8{1'b0}}; end else if (wren) begin out <= wrdata; end else begin out <= out; end end // always @ (posedge clk) endmodule // MyReg
単純にwren
が上がったサイクルのみwrdata
がval
に入るというだけ。スケマにするとこんな感じ。
Chiselで書くと??
では上記のverilogに対応するコードをChiselで書いてみよう。
こんな感じ。
class MyReg extends Module { val io = IO(new Bundle { val in_val = Input(UInt(8.W)) val in_wren = Input(Bool()) val out = Output(UInt(8.W)) }) val reg = RegInit(0.U(8.W)) when (io.in_wren) { reg := io.in_val } io.out := reg }
もうねChiselの旨味も何もあったもんじゃないっていう。
ただ今回はChisel-template上でテストまで含めて実装を行い、一連の流れを把握するのが目的なのでこれでいいのです。
Chisel-templateのフォルダ構成
さて、Chiselで所望のモジュールの実装は出来た。問題は「このファイルをChisel-template環境上のどこに置くのか」ということだ。
そんなわけでここでChisel-templateのフォルダ構成を少し詳細に見ておこう。ダウンロードした直後の構成は以下になる。
. ├── 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
筆者はScalaやsbtをゴリゴリ使っていたわけではなく、ChiselのためにScalaとsbtを触り始めた状態なのでsbtを使った開発には全くと言っていいくらい馴染みが無い。というわけで、上記の構成がなんとなくsbtの開発環境の基本構成なのかな?くらいの認識である。このへんは後日もう少し突っ込んで調べて記事にする予定。
そうは言っても、まあ大体は推測が出来る感じになっているのがありがたいところで、Chiselに関するデータは全てsrc
フォルダに含まれているようだ。
このうち、src/main/scala
以下にChiselのハードウェアデザインを実装したファイルが配置され、対応したテストはsrc/test/scala
以下に配置される構成になる。
Chiselのハードウェアデザインファイル
ここで、サンプルのsrc/main/scala/gcd/GCD.scala
を見てみよう。とは言ってもこれはなんの変哲も無いChiselのハードウェアデザインが実装されたファイルだ。違いはきちんとパッケージの宣言(package
)が行われていることくらい。
// See README.md for license details. package gcd import chisel3._ /** * Compute GCD using subtraction method. * Subtracts the smaller from the larger until register y is zero. * value in register x is then the GCD */ class GCD extends Module { val io = IO(new Bundle { val value1 = Input(UInt(16.W)) val value2 = Input(UInt(16.W)) val loadingValues = Input(Bool()) val outputGCD = Output(UInt(16.W)) val outputValid = Output(Bool()) }) val x = Reg(UInt()) val y = Reg(UInt()) when(x > y) { x := x - y } .otherwise { y := y - x } when(io.loadingValues) { x := io.value1 y := io.value2 } io.outputGCD := x io.outputValid := y === 0.U }
回路の中身は置いといて、ざっくり構造化すると以下の構成になる。
// ライセンスの宣言 package <パッケージ名> import chisel3._ # ライブラリのインポート class <モジュール名> extends Module { # モジュールの実装 }
Chiselのハードウェアテスト環境
では対応するテストはどうだろう。以下の様に2つのファイルが定義されている。
└── test └── scala └── gcd ├── GCDMain.scala └── GCDUnitTest.scala
それぞれ中身は以下のようになっている。
GCDMain.scala
// See README.md for license details. package gcd import chisel3._ /** * This provides an alternate way to run tests, by executing then as a main * From sbt (Note: the test: prefix is because this main is under the test package hierarchy): * {{{ * test:runMain gcd.GCDMain * }}} * To see all command line options use: * {{{ * test:runMain gcd.GCDMain --help * }}} * To run with verilator: * {{{ * test:runMain gcd.GCDMain --backend-name verilator * }}} * To run with verilator from your terminal shell use: * {{{ * sbt 'test:runMain gcd.GCDMain --backend-name verilator' * }}} */ object GCDMain extends App { iotesters.Driver.execute(args, () => new GCD) { c => new GCDUnitTester(c) } } /** * This provides a way to run the firrtl-interpreter REPL (or shell) * on the lowered firrtl generated by your circuit. You will be placed * in an interactive shell. This can be very helpful as a debugging * technique. Type help to see a list of commands. * * To run from sbt * {{{ * test:runMain gcd.GCDRepl * }}} * To run from sbt and see the half a zillion options try * {{{ * test:runMain gcd.GCDRepl --help * }}} */ object GCDRepl extends App { iotesters.Driver.executeFirrtlRepl(args, () => new GCD) }
App
を継承した2つのobject
が2つ定義されており、その中で実際に動作させたいテスターのインスタンスとその実行execute
が行わている形式になる。このファイルに定義された
- GCDMain
- GCDRepl
が、ScalaのMain関数になりうるものなので、sbt
コマンド使って以下の様に実行が可能になる、、ということのようだ。
# この辺は筆者の現状の理解。。。
sbt 'test:runMain gcd.GCDMain'
sbt 'test:runMain gcd.GCDRepl'
GCDMain
の方のコメントには何やら色々なオプションが定義されているようだが、この辺もおって調査予定。
とりあえず、ただ単に実行できればいい!!くらいのスタンスなら、以下のような形で自分のモジュール用のメイン関数用のScalaファイルを用意すれば良さそう。
- test/sacal/<パッケージ名>/<モジュール名.scala>
// See README.md for license details. package <パッケージ名> import chisel3._ object <モジュール名>Main extends App { iotesters.Driver.execute(args, () => new <モジュール名>) { c => new <モジュール名>UnitTester(c) } } object <モジュール名>Repl extends App { iotesters.Driver.executeFirrtlRepl(args, () => new <モジュール名>) }
まあ、Scala的にパッケージ名やモジュール名の解決が出来る様にすればいいだけだろうから、この限りでは無いけど同じにしといたほうがいろいろ混乱せずには済みそう。
GCDUnitTest.scala
こちらが実際のGCDモジュールに対するユニットテストの定義ファイルになる。
// See README.md for license details. package gcd import java.io.File import chisel3.iotesters import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester} class GCDUnitTester(c: GCD) extends PeekPokeTester(c) { /** * compute the gcd and the number of steps it should take to do it * * @param a positive integer * @param b positive integer * @return the GCD of a and b */ def computeGcd(a: Int, b: Int): (Int, Int) = { var x = a var y = b var depth = 1 while(y > 0 ) { if (x > y) { x -= y } else { y -= x } depth += 1 } (x, depth) } private val gcd = c for(i <- 1 to 40 by 3) { for (j <- 1 to 40 by 7) { poke(gcd.io.value1, i) poke(gcd.io.value2, j) poke(gcd.io.loadingValues, 1) step(1) poke(gcd.io.loadingValues, 0) val (expected_gcd, steps) = computeGcd(i, j) step(steps - 1) // -1 is because we step(1) already to toggle the enable expect(gcd.io.outputGCD, expected_gcd) expect(gcd.io.outputValid, 1) } } } /** * This is a trivial example of how to run this Specification * From within sbt use: * {{{ * testOnly gcd.GCDTester * }}} * From a terminal shell use: * {{{ * sbt 'testOnly gcd.GCDTester' * }}} */ class GCDTester extends ChiselFlatSpec { // Disable this until we fix isCommandAvailable to swallow stderr along with stdout private val backendNames = if(false && firrtl.FileUtils.isCommandAvailable(Seq("verilator", "--version"))) { Array("firrtl", "verilator") } else { Array("firrtl") } for ( backendName <- backendNames ) { "GCD" should s"calculate proper greatest common denominator (with $backendName)" in { Driver(() => new GCD, backendName) { c => new GCDUnitTester(c) } should be (true) } } "Basic test using Driver.execute" should "be used as an alternative way to run specification" in { iotesters.Driver.execute(Array(), () => new GCD) { c => new GCDUnitTester(c) } should be (true) } "using --backend-name verilator" should "be an alternative way to run using verilator" in { if(backendNames.contains("verilator")) { iotesters.Driver.execute(Array("--backend-name", "verilator"), () => new GCD) { c => new GCDUnitTester(c) } should be(true) } } "running with --is-verbose" should "show more about what's going on in your tester" in { iotesters.Driver.execute(Array("--is-verbose"), () => new GCD) { c => new GCDUnitTester(c) } should be(true) } /** * By default verilator backend produces vcd file, and firrtl and treadle backends do not. * Following examples show you how to turn on vcd for firrtl and treadle and how to turn it off for verilator */ "running with --generate-vcd-output on" should "create a vcd file from your test" in { iotesters.Driver.execute( Array("--generate-vcd-output", "on", "--target-dir", "test_run_dir/make_a_vcd", "--top-name", "make_a_vcd"), () => new GCD ) { c => new GCDUnitTester(c) } should be(true) new File("test_run_dir/make_a_vcd/make_a_vcd.vcd").exists should be (true) } "running with --generate-vcd-output off" should "not create a vcd file from your test" in { iotesters.Driver.execute( Array("--generate-vcd-output", "off", "--target-dir", "test_run_dir/make_no_vcd", "--top-name", "make_no_vcd", "--backend-name", "verilator"), () => new GCD ) { c => new GCDUnitTester(c) } should be(true) new File("test_run_dir/make_no_vcd/make_a_vcd.vcd").exists should be (false) } }
このユニットテストファイルは以下の2つのテストクラスで構成されている。
GCDUnitTester
GCDTester
このうちGCDTester
はテスト本体ではなくテストをどうのように実施するかという各種設定を行うためのクラスになっており、”自分でカスタマイズを行いたい”というようなニーズが出てこない限りに置いては、対象モジュールの書き換え程度の軽微な修正で済む(パッケージで階層化されているので、それすらサボることも可能な気もするが、わかりにくくなりそうなのでやめておいた)。ここで使われている各種ScalaやChiselの文法・モジュールについてもおって調査して記事にしたい。
一方でGCDUnitTester
はサンプルデザインのGCD
モジュールのユニットテストを定義したクラスになっているので、これについては各モジュールごとに、設計したデザインの仕様に対してのテストを実装する必要がある。
では改めて、上記2つのテスト用クラスを一般化してみよう。
UnitTester
ユニットテスターは実装するモジュールに合わせて実装する必要があるので、中身はそれに応じて全部変更することになる。
class <モジュール名>UnitTester(c: <モジュール名>) extends PeekPokeTester(c) { # ユニットテストの実装 }
Tester
先ほど書いたとおり、こちらは実行時にどのような設定でテストを実行するかのオプションを提供しているクラスなので、サンプルのGCDの機能と等価な機能で良ければ、モジュール名の変更のみでOKとなる。
class <モジュール名>Tester extends ChiselFlatSpec { // Disable this until we fix isCommandAvailable to swallow stderr along with stdout private val backendNames = if(false && firrtl.FileUtils.isCommandAvailable(Seq("verilator", "--version"))) { Array("firrtl", "verilator") } else { Array("firrtl") } for ( backendName <- backendNames ) { "<モジュール名>" should s"calculate proper greatest common denominator (with $backendName)" in { Driver(() => new <モジュール名>, backendName) { c => new <ユニットテストクラス(==<モジュール名>UnitTester)>(c) } should be (true) } } "Basic test using Driver.execute" should "be used as an alternative way to run specification" in { iotesters.Driver.execute(Array(), () => new <モジュール名>) { c => new <ユニットテストクラス(==<モジュール名>UnitTester)>(c) } should be (true) } "using --backend-name verilator" should "be an alternative way to run using verilator" in { if(backendNames.contains("verilator")) { iotesters.Driver.execute(Array("--backend-name", "verilator"), () => new <モジュール名>) { c => new <ユニットテストクラス(==<モジュール名>UnitTester)>(c) } should be(true) } } "running with --is-verbose" should "show more about what's going on in your tester" in { iotesters.Driver.execute(Array("--is-verbose"), () => new <モジュール名>) { c => new <ユニットテストクラス(==<モジュール名>UnitTester)>(c) } should be(true) } /** * By default verilator backend produces vcd file, and firrtl and treadle backends do not. * Following examples show you how to turn on vcd for firrtl and treadle and how to turn it off for verilator */ "running with --generate-vcd-output on" should "create a vcd file from your test" in { iotesters.Driver.execute( Array("--generate-vcd-output", "on", "--target-dir", "test_run_dir/make_a_vcd", "--top-name", "make_a_vcd"), () => new <モジュール名> ) { c => new <ユニットテストクラス(==<モジュール名>UnitTester)>(c) } should be(true) new File("test_run_dir/make_a_vcd/make_a_vcd.vcd").exists should be (true) } "running with --generate-vcd-output off" should "not create a vcd file from your test" in { iotesters.Driver.execute( Array("--generate-vcd-output", "off", "--target-dir", "test_run_dir/make_no_vcd", "--top-name", "make_no_vcd", "--backend-name", "verilator"), () => new <モジュール名> ) { c => new <ユニットテストクラス(==<モジュール名>UnitTester)>(c) } should be(true) new File("test_run_dir/make_no_vcd/make_a_vcd.vcd").exists should be (false) } }
MyReg
をchisel-templateに組み込んでデバッグ
長くなったがなんとなく変更すべき部分が見えたので、早速先ほどのMyReg
をchisel-tamplate環境に組み込んでみる。
組み込み後のフォルダ構成は以下の様になった。
└── src ├── main │ └── scala │ └── reg │ └── MyReg.scala └── test └── scala └── reg ├── MyRegMain.scala └── MyRegUnitTest.scala
ハードウェアが実装された"MyReg.scala"は既に紹介したとおりのものなので、テスト用のファイルのみをいかに記載する。
MyRegMain.scala
パッケージ名と呼び出すテスタークラスがMyReg用に実装したものになっただけ。
// See README.md for license details. package reg import chisel3._ object MyRegMain extends App { iotesters.Driver.execute(args, () => new MyReg) { c => new MyRegUnitTester(c) } } object GCDRepl extends App { iotesters.Driver.executeFirrtlRepl(args, () => new MyReg) }
MyRegUnitTest.scala
テストは適当にwren
信号をランダムで生成して、その際の値が保持されていることを確認するだけの簡単なテストにしてある。
// See README.md for license details. package reg import java.io.File import scala.math.{random, floor} import chisel3.iotesters import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester} class MyRegUnitTester(c: MyReg) extends PeekPokeTester(c) { var exp_val = 0 /** * update the register value * * @param wren write enable * @param wrdata positive integer * @return the Register value */ def updateMyReg(wren: Boolean, wrdata: Int): (Int) = { if (wren) { exp_val = wrdata } exp_val } private val r = c for(i <- 1 to 40 by 3) { val wren = floor(random * 2).toInt == 1 val wrdata = floor(random * 10).toInt poke(r.io.in_wren, wren) poke(r.io.in_val, wrdata) step(1) val expected_val = updateMyReg(wren, wrdata) expect(r.io.out, expected_val) println(f"(wren, wrdata, io.out) = ($wren%5s, 0x$wrdata%02x, 0x${peek(r.io.out).toInt}%02x)") } } /** * This is a trivial example of how to run this Specification * From within sbt use: * {{{ * testOnly gcd.GCDTester * }}} * From a terminal shell use: * {{{ * sbt 'testOnly gcd.GCDTester' * }}} */ class MyRegTester extends ChiselFlatSpec { // Disable this until we fix isCommandAvailable to swallow stderr along with stdout private val backendNames = if(false && firrtl.FileUtils.isCommandAvailable(Seq("verilator", "--version"))) { Array("firrtl", "verilator") } else { Array("firrtl") } for ( backendName <- backendNames ) { "MyReg" should s"calculate proper greatest common denominator (with $backendName)" in { Driver(() => new MyReg, backendName) { c => new MyRegUnitTester(c) } should be (true) } } "Basic test using Driver.execute" should "be used as an alternative way to run specification" in { iotesters.Driver.execute(Array(), () => new MyReg) { c => new MyRegUnitTester(c) } should be (true) } "using --backend-name verilator" should "be an alternative way to run using verilator" in { if(backendNames.contains("verilator")) { iotesters.Driver.execute(Array("--backend-name", "verilator"), () => new MyReg) { c => new MyRegUnitTester(c) } should be(true) } } "running with --is-verbose" should "show more about what's going on in your tester" in { iotesters.Driver.execute(Array("--is-verbose"), () => new MyReg) { c => new MyRegUnitTester(c) } should be(true) } /** * By default verilator backend produces vcd file, and firrtl and treadle backends do not. * Following examples show you how to turn on vcd for firrtl and treadle and how to turn it off for verilator */ "running with --generate-vcd-output on" should "create a vcd file from your test" in { iotesters.Driver.execute( Array("--generate-vcd-output", "on", "--target-dir", "test_run_dir/make_a_vcd", "--top-name", "make_a_vcd"), () => new MyReg ) { c => new MyRegUnitTester(c) } should be(true) new File("test_run_dir/make_a_vcd/make_a_vcd.vcd").exists should be (true) } "running with --generate-vcd-output off" should "not create a vcd file from your test" in { iotesters.Driver.execute( Array("--generate-vcd-output", "off", "--target-dir", "test_run_dir/make_no_vcd", "--top-name", "make_no_vcd", "--backend-name", "verilator"), () => new MyReg ) { c => new MyRegUnitTester(c) } should be(true) new File("test_run_dir/make_no_vcd/make_a_vcd.vcd").exists should be (false) } }
実行結果は以下の様になる。wren
がtrue
の時にのみ、io.out
の値が更新されているのがわかると思う。
[info] Running reg.MyRegMain [info] [0.003] Elaborating design... [info] [1.179] Done elaborating. Total FIRRTL Compile Time: 332.0 ms Total FIRRTL Compile Time: 117.0 ms file loaded in 0.180685728 seconds, 8 symbols, 3 statements [info] [0.001] SEED 1546755379651 [info] [0.003] (wren, wrdata, io.out) = ( true, 0x04, 0x04) [info] [0.003] (wren, wrdata, io.out) = (false, 0x09, 0x04) [info] [0.003] (wren, wrdata, io.out) = ( true, 0x01, 0x01) [info] [0.004] (wren, wrdata, io.out) = (false, 0x03, 0x01) [info] [0.004] (wren, wrdata, io.out) = ( true, 0x05, 0x05) [info] [0.004] (wren, wrdata, io.out) = (false, 0x00, 0x05) [info] [0.005] (wren, wrdata, io.out) = ( true, 0x00, 0x00) [info] [0.005] (wren, wrdata, io.out) = (false, 0x06, 0x00) [info] [0.005] (wren, wrdata, io.out) = (false, 0x02, 0x00) [info] [0.005] (wren, wrdata, io.out) = ( true, 0x07, 0x07) [info] [0.006] (wren, wrdata, io.out) = ( true, 0x06, 0x06) [info] [0.006] (wren, wrdata, io.out) = ( true, 0x07, 0x07) [info] [0.006] (wren, wrdata, io.out) = (false, 0x05, 0x07) [info] [0.007] (wren, wrdata, io.out) = (false, 0x06, 0x07) test MyReg Success: 14 tests passed in 19 cycles in 0.017862 seconds 1063.73 Hz [info] [0.006] RAN 14 CYCLES PASSED [success] Total time: 3 s, completed 2019/01/06 15:16:21
VCDダンプによる波形デバッグ
今回やったようにテスターモジュールをそのまんま流用してテストを作成した際のメリットとしてVCDによる波形ダンプが可能な点が挙げられる。詳しくは追っていないがテスタークラスの以下の部分でVCDダンプに関する引数が追加されているようなのでiotesters.Driver
にVCDダンプに関する機能がもともと入っているっぽく見える。
"running with --generate-vcd-output on" should "create a vcd file from your test" in { iotesters.Driver.execute( Array("--generate-vcd-output", "on", "--target-dir", "test_run_dir/make_a_vcd", "--top-name", "make_a_vcd"), () => new MyReg ) { c => new MyRegUnitTester(c) } should be(true) new File("test_run_dir/make_a_vcd/make_a_vcd.vcd").exists should be (true) }
使い方も簡単で実行時にオプションで--generate-vcd-output on
を追加するだけである。今回のMyReg
の場合だと以下の様になる。
sbt 'test:runMain reg.MyRegMain --generate-vcd-output on'
上記で実行すると、テスト結果が出力されるフォルダに"<モジュール名>.vcd"というダンプファイルが生成される。これは拡張子の示す通りVCD形式の波形データファイルになっているので、波形ビューワーで開くことが可能だ。筆者はGTKWaveを使用した。Windowsの場合はビルド済みのバイナリデータが公開されているのでそれを使うと楽。
io_in_wren
に応じてreg
の値が更新されているのがわかるかと思う。
これでIntelliJ IDEA上に構築したchisel-template環境上で波形デバッグまでを含んだ開発が行えることが確認できた。波形が見えるようになるだけで随分デバッグがやりやすくなるな、、やっぱり。
今回実施した各種変更はchisel-templateをforkした自分のgithubリポジトリに上げてあるので、興味があればそちらも合わせてどうぞ。 #これforkしないでリポジトリ新規作成したほうが良かったな。。
まだまだ色々機能がありそうなのでChisel-bootcampが落ち着いたタイミングにでももう少しこちらのテンプレートについても深堀してみたいと思う。