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

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

ChiselのBundleをvalで受けるといろいろ便利だった話

スポンサーリンク

前回のChiselのPeekPokeTesterにはIntBigIntに変換するメソッドが合ったのでそrを紹介した。

www.tech-diningyo.info

今回も最近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インスタンスしたものになっている。
この機能は上位層でポートを接続する際や、構造化したデータをまとめてレジスタとして宣言する際に非常に有効なものだ(以前に書いた以下の記事も良ければご覧ください)

www.tech-diningyo.info

www.tech-diningyo.info

ただモジュール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/BBを継承したC、そしてこれらの基本のI/O用Bundleを内包したAB/ABCを用意した。
これらのI/O用Bundleを用いて以下のことを確認していく。

  1. Bundleごとvalで受け取った際にBundleの中の信号に正常に接続が出来るかどうか
  2. Bundleを受け取るメソッドを定義して、その中で意図どおりの接続が出来るか
  3. スーパークラスを受け取るメソッドを用意して、それにサブクラスを渡した時に意図どおりの接続が出来るか

では早速見ていこう。

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.aval b = io.b.getの部分。
実は単純に筆者のScala力が足りてないだけで、このようなコードを書いた時にデータが複製されているのか、リファレンスが渡っているのかを把握してなかったので試してみたという話。
val a/val bがリファレンス的な何か(Scalaでなんていうかを知らない。。)なのであればval a/val bio.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で任意の個数分インスタンスした場合にも適用が出来る。
しっかり使えばコード自体がすっきりするので積極的に使っていこうと思った。