前回の記事でChisel BootcampのModule2.1の最初のモジュールの作成が終わった。
今回はModule2.1の最初のモジュールの2つ目の例からスタートしていく。いよいよChiselで作るジェネレータに関しての話題だ。
Module 2.1: 最初のChiselモジュール
例題:モジュール・ジェネレータ
”ジェネレータ”となんだか大層な名前も付いているが、やるのは至ってシンプルなので、早速例を見てみよう。
// Chisel Code, but pass in a parameter to set widths of ports class PassthroughGenerator(width: Int) extends Module { val io = IO(new Bundle { val in = Input(UInt(width.W)) val out = Output(UInt(width.W)) }) io.out := io.in } // Let's now generate modules with different widths println(getVerilog(new PassthroughGenerator(10))) println(getVerilog(new PassthroughGenerator(20)))
先ほどの”最初のモジュール”との違いは以下になる。
- モジュール名:
Passthrough
→PassthroughGenerator
- クラスのパラメータを追加:
(width: Int)
- 追加したパラメータを
Input
/Output
に指定
これだけの修正だがパラメータを指定できるようになったことによって、このモジュールの入力/出力のビット幅をインスタンス時に可変に出来るようになった。
実際にgetVerilog
でエラボレーション後に出力されたものを見てみると以下のようになる。
[info] [0.001] Elaborating design... [info] [0.709] Done elaborating. Total FIRRTL Compile Time: 242.0 ms module cmd3HelperPassthroughGenerator( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [9:0] io_in, // @[:@6.4] output [9:0] io_out // @[:@6.4] ); assign io_out = io_in; endmodule [info] [0.000] Elaborating design... [info] [0.004] Done elaborating. Total FIRRTL Compile Time: 14.8 ms module cmd3HelperPassthroughGenerator( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [19:0] io_in, // @[:@6.4] output [19:0] io_out // @[:@6.4] ); assign io_out = io_in; endmodule
見ての通り、ひとつ目のgetVerilog
ではビット幅を”10”に指定したため、Verilogのコード上のio_in
/io_out
のビット幅が10-bitになっている。同様に2つ目は20-bitになっていることがわかる。
この例だけだとVerilog-2001のgenerate
構文でも同じようなことは出来るのだが、Chiselの場合はScalaの構文をフルに使えるのでこちらのほうが全然自由度は高そう。
Bootcampの説明から大事そうな部分をピックアップしておく。一部、上記の記述と被るが以下のようになる。
- 生成されたVerilogコードの入力/出力が
width
に与えた値になる - このパラメタライズの処理はChiselの機能ではなく、ChiselのモジュールがScalaのクラスで実装されていることよるものである
- Chiselとしてはパラメタライズに関する特別なAPIを持っているわけではないが、設計者はScalaのパラメタライズ機能を簡単に利用できる
PassthroughGenerator
はもはや単一のモジュールではなく、width
によってパラメタライズされたモジュールであり、このようなモジュールをジェネレータと呼ぶ。
ハードウェアのテスト
まずはBootcampの本文を引用させていただく。
ハードウェアのモジュールやジェネレータはテスター無しでは完成すべきでない。Chiselはビルトインのテスト機能を持っており、このブートキャンプを通してそれらについては学んでいくことになる。次のコードはChiselのテストハーネスを使ったテストの例で、
Passthrough
のインスタンスの入力ポートin
に入れたものと同じ値が、出力ポートout
に出てくるかをチェックしている。
例題:テスター
// Scala Code: Calling Driver to instantiate Passthrough + PeekPokeTester and execute the test. // Don't worry about understanding this code; it is very complicated Scala. // Think of it more as boilerplate to run a Chisel test. val testResult = Driver(() => new Passthrough()) { c => new PeekPokeTester(c) { poke(c.io.in, 0) // Set our input to value 0 expect(c.io.out, 0) // Assert that the output correctly has 0 poke(c.io.in, 1) // Set our input to value 1 expect(c.io.out, 1) // Assert that the output correctly has 1 poke(c.io.in, 2) // Set our input to value 2 expect(c.io.out, 2) // Assert that the output correctly has 2 } } assert(testResult) // Scala Code: if testResult == false, will throw an error println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
上記の例には色々な”見知らぬ”Scalaのコードが出てきているが、"poke
/expect
以外はテンプレートみたいなものと思って置いといて"ってことなので、こちらでも特に触れないでおく。
上記のコードを実行すると以下のような出力を得られる。
[info] [0.000] Elaborating design... [info] [0.100] Done elaborating. Total FIRRTL Compile Time: 10.1 ms Total FIRRTL Compile Time: 5.3 ms End of dependency graph Circuit state created [info] [0.001] SEED 1539499360981 test cmd2HelperPassthrough Success: 3 tests passed in 5 cycles taking 0.012104 seconds [info] [0.004] RAN 0 CYCLES PASSED SUCCESS!!
上記コードでテストに使われているものをまとめると以下のようになる。
poke
: 入力に値をセットするexpect
: 出力の値をチェックするpeek
: 出力の値の比較がいらない場合にexpect
の代わりに使用する
PeekPokeTester
のブロック式内のコードのexpect
が全てtrueだった場合に、testResult
にtrueが設定され、その結果assert(testResult)
が発火せずに最後の"SUCESS!!"の表示に辿り着く。
エクササイズ
本モジュールには練習問題が付いていてそれは以下のようなものだ。
PassthroughGenerator
に対して以下の2つのテストを実装せよ。
- bit幅が10-bitのケース
- bit幅が20-bitのケース
テストの中身としては最低限以下の2つの値に対してのチェックを実行すること
- 0の場合
- 特定のbit幅における最大値
なお、下記コード中の
???
はScalaでは特別な意味を持っていて、この後のBootcampにおいても度々見かけることになる。この???
を評価するとScalaはNotImplementError
を返すので、この部分を自分で実装したテストに置き換えて実行せよ。
val test10result = ??? val test20result = ??? assert((test10result == true) && (test20result == true)) println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
なるほど、???
にはそんな意味が。。。
ということでとりあえずそのまま実行してみる。
scala.NotImplementedError: an implementation is missing scala.Predef$.$qmark$qmark$qmark(Predef.scala:230) ammonite.$sess.cmd12$Helper.<init>(cmd12.sc:1) ammonite.$sess.cmd12$.<init>(cmd12.sc:7) ammonite.$sess.cmd12$.<clinit>(cmd12.sc:-1)
ほんとだ、エラー出た。
以下は自分が試した解答。見たくない場合は展開しないようにご注意を。
解答
val test10result = Driver(() => new PassthroughGenerator(10)) {
c => new PeekPokeTester(c) {
poke(c.io.in, 0)
expect(c.io.out, 0)
poke(c.io.in, (1 << 10) - 1)
expect(c.io.out, (1 << 10) - 1)
}
}
val test20result = Driver(() => new PassthroughGenerator(20)) {
c => new PeekPokeTester(c) {
poke(c.io.in, 0)
expect(c.io.out, 0)
poke(c.io.in, (1 << 20) - 1)
expect(c.io.out, (1 << 20) -1)
}
}
assert((test10result == true) && (test20result == true))
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
[info] [0.000] Elaborating design...
[info] [0.003] Done elaborating.
Total FIRRTL Compile Time: 7.4 ms
Total FIRRTL Compile Time: 7.3 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1539501141285
test cmd3HelperPassthroughGenerator Success: 2 tests passed in 5 cycles taking 0.002805 seconds
[info] [0.002] RAN 0 CYCLES PASSED
[info] [0.000] Elaborating design...
[info] [0.004] Done elaborating.
Total FIRRTL Compile Time: 11.0 ms
Total FIRRTL Compile Time: 10.8 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1539501141319
test cmd3HelperPassthroughGenerator Success: 2 tests passed in 5 cycles taking 0.002964 seconds
[info] [0.002] RAN 0 CYCLES PASSED
SUCCESS!!
これでModule2.1の中身はだいたい終わりなのだが、残りの内容としてデバッグの際に”何をどのように確認するか”という話が残っている。
これについては次の記事で見ることにして、今日はここまで。