前回のChiselのPeekPokeTester
にはInt
をBigInt
に変換するメソッドが合ったのでそrを紹介した。
今回も最近Chisel書いていて「あ、これ出来るんじゃん!」という気付きがあったので、それをまとめておこうと思う。ぶっちゃけScalaが分かってないというのも関係してるので「なんだそんなことかよ。。。」ってなるかも。
Bundle
で構造化したデータはエイリアスを作ってアクセスしやすく出来る
もうこの章の題名が今回の記事の全てだったりする。
Bundle
で構造化した際にめんどくさいなーーって思っていた点
とりあえずどういうことか??についての説明をするために、サンプルのわざとらしいコードを用意したのでそれを見てもらいたい。
class A(hasOptPort: Boolean = true) extends Bundle { val a = new Bundle { val bb = new Bundle { val ccc = new Bundle { val d = Input(Bool()) val e = Output(Bool()) val f = Input(UInt(32.W)) val g = if (hasOptPort) Some(Output(UInt(32.W))) else None } } } } class B(hasOptPort: Boolean = true) extends Module { val io = IO(new A) io.a.bb.ccc.e := io.a.bb.ccc.d // Bundleで構造化していくと if (hasOptPort) { io.a.bb.ccc.g.get := io.a.bb.ccc.f // どんどん深くなっていく } }
見てもらうとわかる通りでB
のIOはBundle
で多重にネストして作ったA
をインスタンスしたものになっている。
この機能は上位層でポートを接続する際や、構造化したデータをまとめてレジスタとして宣言する際に非常に有効なものだ(以前に書いた以下の記事も良ければご覧ください)
ただモジュールB
の実装部分のコメントに書いたとおり、ネストしている分接続した各種信号のフルパスが長くなりがち(g
とか特にね)で、実装する際に若干野暮ったいなーって思っていてg
とかにもっと短い名前でアクセスできないだろーか?という部分を調査したのが今回の記事なる。
Bundle
のオブジェクトを丸々受け取ればそれで良かった
で改めて今の自分のChiselの理解のもとに試してみたら思いの外自由になることが分かったのでそれをまとめておこうと思う。
結論としてはこの段落のタイトルのままでBundle
のオブジェクトごと丸々val
で受ければそれでオッケーだった。
早速サンプルで見てみよう。
インターフェース用のBundle
/** * AIf */ class AIf extends Bundle { val a = Input(Bool()) val aa = Output(UInt(32.W)) } /** * BIf */ class BIf extends Bundle { val b = Input(Bool()) val bb = Output(UInt(32.W)) } /** * CIf - BIfを継承しているのでb/bb/c/ccが変数として存在している */ class CIf extends BIf { val c = Input(Bool()) val cc = Output(UInt(32.W)) } /** * ABIf * @param useBIf BIf をインスタンスするかどうか */ class ABIf(useBIf: Boolean) extends Bundle { val a = new AIf val b = if (useBIf) Some(new BIf) else None override def cloneType: ABIf.this.type = new ABIf(useBIf).asInstanceOf[this.type] } /** * ABCIf * @param useBIf BIf をインスタンスするかどうか */ class ABCIf(useBIf: Boolean) extends Bundle { val a = new AIf val b = if (useBIf) Some(new BIf) else None val c = new CIf override def cloneType: ABCIf.this.type = new ABCIf(useBIf).asInstanceOf[this.type] }
最初のサンプルと同じで若干わざとらしいけど、基本となるA
/B
とB
を継承したC
、そしてこれらの基本のI/O用Bundle
を内包したAB
/ABC
を用意した。
これらのI/O用Bundle
を用いて以下のことを確認していく。
Bundle
ごとval
で受け取った際にBundle
の中の信号に正常に接続が出来るかどうかBundle
を受け取るメソッドを定義して、その中で意図どおりの接続が出来るか- スーパークラスを受け取るメソッドを用意して、それにサブクラスを渡した時に意図どおりの接続が出来るか
では早速見ていこう。
Bundle
ごとval
で受け取った際に、正常に接続が行われるかどうか
文章で書いてると若干わかりづらいなーーとは思っているが、サンプルのコードを見てもらえれば一目瞭然だと思うので、早速サンプルを見てもらう。こんなの↓。
/** * まずは別の変数に移して、設定が出来るかどうかを確認 * @param useBIf BIfを使用するかどうか */ class BundleTestModule1(useBIf: Boolean) extends Module { val io = IO(new ABIf(useBIf)) val a = io.a when (a.a) { a.aa := 0x12345678.U } .otherwise { a.aa := 0x87654321L.U } if (useBIf) { val b = io.b.get when (b.b) { b.bb := 0x12345678.U } .otherwise { b.bb := 0x87654321L.U } } }
確かめたかったのはval a = io.a
とval b = io.b.get
の部分。
実は単純に筆者のScala力が足りてないだけで、このようなコードを書いた時にデータが複製されているのか、リファレンスが渡っているのかを把握してなかったので試してみたという話。
val a
/val b
がリファレンス的な何か(Scalaでなんていうかを知らない。。)なのであればval a
/val b
はio.a
/io.b.get
のエイリアスとして動作してくれるということになるのだが、さてどうなるか早速実行してみる。
テストコードは以下のような感じにした(このケースだと、実はテストコードは無くても良くて、エラボレートが通るかだけで良かったり。)
import chisel3._ import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester} class BundleTestModule1Tester extends ChiselFlatSpec { val topName = "BundleTestModule1" behavior of topName val defaultArgs = Array("--generate-vcd-output=on") it should "a.aの値の変化に合わせてa.aaの値が変化する" in { val targetDir = s"test_run_dir/$topName/atest" val args = defaultArgs ++ Array( s"--top-name=$topName", s"--target-dir=$targetDir" ) Driver.execute(args, () => new BundleTestModule1(false)) { c => new PeekPokeTester(c) { reset() expect(c.io.a.aa, 0x87654321L.U) poke(c.io.a.a, true) step(1) expect(c.io.a.aa, 0x12345678.U) step(1) } } should be (true) } it should "b.bの値の変化に合わせてb.bbの値が変化する" in { val targetDir = s"test_run_dir/$topName/btest" val args = defaultArgs ++ Array( s"--top-name=$topName", s"--target-dir=$targetDir" ) Driver.execute(args, () => new BundleTestModule1(true)) { c => new PeekPokeTester(c) { reset() val b = c.io.b.get expect(b.bb, 0x87654321L.U) poke(b.b, true) step(1) expect(b.bb, 0x12345678.U) step(1) } } should be (true) } }
- 実行結果
見ての通りで正常にエラボレートが終わり、テストも期待値一致でPASSする。
ということでBundle
のオブジェクトごとval
で受け取ればそいつを使ってハードウェアの信号の接続が出来る。
[IJ]sbt:bundleAlias> testOnly BundleTestModule1Tester [info] [0.001] Elaborating design... [info] [0.088] Done elaborating. Total FIRRTL Compile Time: 1027.0 ms Total FIRRTL Compile Time: 101.3 ms file loaded in 0.157490638 seconds, 4 symbols, 1 statements [info] [0.002] SEED 1554535072076 test BundleTestModule1 Success: 2 tests passed in 8 cycles in 0.032030 seconds 249.76 Hz [info] [0.010] RAN 2 CYCLES PASSED [info] [0.001] Elaborating design... [info] [0.007] Done elaborating. Total FIRRTL Compile Time: 22.8 ms Total FIRRTL Compile Time: 23.4 ms file loaded in 0.029554977 seconds, 6 symbols, 2 statements [info] [0.000] SEED 1554535073731 test BundleTestModule1 Success: 2 tests passed in 8 cycles in 0.004579 seconds 1747.01 Hz [info] [0.003] RAN 2 CYCLES PASSED [info] BundleTestModule1Tester: [info] BundleTestModule1 [info] - should a.aの値の変化に合わせてa.aaの値が変化する [info] - should b.bの値の変化に合わせてb.bbの値が変化する [info] ScalaTest [info] Run completed in 1 second, 950 milliseconds. [info] Total number of tests run: 2 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [info] Passed: Total 2, Failed 0, Errors 0, Passed 2 [success] Total time: 2 s, completed 2019/04/06 16:17:53
Bundle
を受け取るメソッドを定義して、その中で意図どおりの接続が出来るか
その2。ここからは最初のケースの派生系なので少しあっさり目に紹介。
まずはテスト対象のモジュール。
変更点はそれぞれの端子設定用のメソッドを用意して、そこに所定のBundle
のオブジェクトを設定しているだけ。
/** * 設定用のメソッドを用意して、メソッド内で値を設定してみる * @param useBIf BIfを使用するかどうか */ class BundleTestModule2(useBIf: Boolean) extends Module { /** * AIf設定用のメソッド * @param a AIf */ def setA(a: AIf): Unit = { when (a.a) { a.aa := 0x12345678.U } .otherwise { a.aa := 0x87654321L.U } } /** * BIf設定用のメソッド * @param b BIf */ def setB(b: BIf): Unit = { when (b.b) { b.bb := 0x12345678.U } .otherwise { b.bb := 0x87654321L.U } } val io = IO(new ABIf(useBIf)) setA(io.a) if (useBIf) { setB(io.b.get) } }
- テスト結果
テストコードは先ほどとほぼ同じなので割愛して結果のみを記載してます。 結果はエラボレートは通り期待値も一致した。
[IJ]sbt:bundleAlias> testOnly BundleTestModule2Tester [info] [0.001] Elaborating design... [info] [0.070] Done elaborating. Total FIRRTL Compile Time: 241.8 ms Total FIRRTL Compile Time: 74.3 ms file loaded in 0.121192299 seconds, 4 symbols, 1 statements [info] [0.002] SEED 1554535426223 test BundleTestModule2 Success: 2 tests passed in 8 cycles in 0.025906 seconds 308.81 Hz [info] [0.007] RAN 2 CYCLES PASSED [info] [0.000] Elaborating design... [info] [0.009] Done elaborating. Total FIRRTL Compile Time: 18.7 ms Total FIRRTL Compile Time: 17.1 ms file loaded in 0.021556657 seconds, 6 symbols, 2 statements [info] [0.000] SEED 1554535426977 test BundleTestModule2 Success: 2 tests passed in 8 cycles in 0.004644 seconds 1722.79 Hz [info] [0.003] RAN 2 CYCLES PASSED [info] BundleTestModule2Tester: [info] BundleTestModule2 [info] - should a.aの値の変化に合わせてa.aaの値が変化する [info] - should b.bの値の変化に合わせてb.bbの値が変化する [info] ScalaTest [info] Run completed in 1 second, 26 milliseconds. [info] Total number of tests run: 2 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [info] Passed: Total 2, Failed 0, Errors 0, Passed 2 [success] Total time: 1 s, completed 2019/04/06 16:23:47
スーパークラスを受け取るメソッドを用意して、それにサブクラスを渡した時に意図どおりの接続が出来るか
これは2番目の例の派生系で以下のサンプル中のsetC
メソッドの動きが可能か、ということ確認するもの。
/** * スーパークラスを受け取る設定用のメソッドを用意して、まとめられる設定はまとめてみる * @param useBIf BIfを使用するかどうか */ class BundleTestModule3(useBIf: Boolean) extends Module { /** * AIf設定用のメソッド * @param a AIf */ def setA(a: AIf): Unit = { when (a.a) { a.aa := 0x12345678.U } .otherwise { a.aa := 0x87654321L.U } } /** * Bif設定用のメソッド * @param b BIf */ def setB(b: BIf): Unit = { when (b.b) { b.bb := 0x12345678.U } .otherwise { b.bb := 0x87654321L.U } } /** * CIf設定用のメソッド。 * CIfはBIfを継承しているので、中でsetBを呼び出しBifの設定を行う * @param c CIf */ def setC(c: CIf): Unit = { setB(c) when (c.c) { c.cc := 0x12345678.U } .otherwise { c.cc := 0x87654321L.U } } val io = IO(new ABCIf(useBIf)) setA(io.a) if (useBIf) { setB(io.b.get) } setC(io.c) }
- テスト実行結果
こちらもほぼ代わり映えしないテストコードができるのでコード自体は割愛します。 結果は先の2つのサンプルと同様に正常にエラボレートが終了し、所望の動きが確認できた。
[IJ]sbt:bundleAlias> testOnly BundleTestModule3Tester [info] [0.001] Elaborating design... [info] [0.069] Done elaborating. Total FIRRTL Compile Time: 255.1 ms Total FIRRTL Compile Time: 83.9 ms file loaded in 0.138724565 seconds, 8 symbols, 3 statements [info] [0.001] SEED 1554535661157 test BundleTestModule3 Success: 2 tests passed in 8 cycles in 0.028173 seconds 283.96 Hz [info] [0.008] RAN 2 CYCLES PASSED [info] [0.000] Elaborating design... [info] [0.006] Done elaborating. Total FIRRTL Compile Time: 32.3 ms Total FIRRTL Compile Time: 22.3 ms file loaded in 0.026841271 seconds, 10 symbols, 4 statements [info] [0.001] SEED 1554535662010 test BundleTestModule3 Success: 2 tests passed in 8 cycles in 0.005971 seconds 1339.87 Hz [info] [0.004] RAN 2 CYCLES PASSED [info] [0.000] Elaborating design... [info] [0.007] Done elaborating. Total FIRRTL Compile Time: 25.9 ms Total FIRRTL Compile Time: 20.8 ms file loaded in 0.023860718 seconds, 10 symbols, 4 statements [info] [0.000] SEED 1554535662099 test BundleTestModule3 Success: 4 tests passed in 10 cycles in 0.007320 seconds 1366.09 Hz [info] [0.004] RAN 4 CYCLES PASSED [info] BundleTestModule3Tester: [info] BundleTestModule3 [info] - should a.aの値の変化に合わせてa.aaの値が変化する [info] - should b.bの値の変化に合わせてb.bbの値が変化する [info] - should c.b/c.cの値の変化に合わせてc.b/c.cの値が変化する [info] ScalaTest [info] Run completed in 1 second, 195 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 [success] Total time: 2 s, completed 2019/04/06 16:27:42
ということでわざとらしいサンプルでBundle
のインスタンスを使ったエイリアスの作成(と勝手に呼ぶことにした)が出来ることが確認できた。
この考え方はBundle
だけじゃなくVec
で作ったもの(おそらくAggregate
を継承したデータ型)なら適用可能だ。そのためBundle
で束ねたデータ型をVec
で任意の個数分インスタンスした場合にも適用が出来る。
しっかり使えばコード自体がすっきりするので積極的に使っていこうと思った。