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

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

Chiselの文法 - 入門編 〜その7:ChiselのBundle〜

スポンサーリンク

前回の続きでChiselの文法入門編その7
今回はChiselにおける拡張型の続きとしてBundleについてを見ていきます

Chisel入門編〜その7:Bundle〜

前回の冒頭にも書いたとおりBundleはChiselにおける拡張型とも言える概念です。
Vecは同一の型を複数個まとめて扱うことができるためVerilog HDLにおけるメモリ配列に相当するものでしたが、Bundleは関連のある要素を混在してひとつの新しい型を作り出すことが出来ます。これはSystem Verilogで言う構造体やクラスに相当する概念になります。
このように同一もしくは複数の型を集めて新しい型を作るため、Chiselにおけるクラス構造ではAggregate型を継承した形になっています。
既にモジュールのIO宣言に使われているので、イメージはしやすいかと思います。 なおBundleについてはこのブログでも何度か取り上げていますので、ご興味アレばそちらもご一読くださいm(_ _)m

Bundleの宣言

早速Bundleの使い方を見て行きたいと思います。
まずは宣言ですが、これは単純にBundleを継承するだけでOKです。

class <所望のクラス名> extends Bundle {
  // ここに集約したい変数を宣言していく
  // Chiselのデータ型がインスタンス時にWire/Reg
  // といったハードウェア要素のラップ対象になる
}

上記のようにBundleクラスを継承したScalaのクラス宣言なのでクラス内には変数宣言やメソッド宣言を含むことが可能になっています。
コメント部分に記載したように、インスタンスして使用する際にハードウェア要素でくるんだ場合にハードウェア化されるのはChiselのデータ型のみになっています。
そのため”計算してデータのビット幅を決めたい”と言ったような場合にはScalaInt等でビット幅の計算を行い、それを使ってChiselのデータ型を宣言すればOKです。

例で見るBundleの使い方

使うときには自分で作ったBundlenewインスタンス化し、必要に応じて適切なハードウェア要素でくるんで回路として実体化させます。
以下は簡単な例です。

class MyMod extends Module {
  val io = IO(new Bundle {})

  // Wireに直接Bundleのインスタンスを渡して組み合わせ論理として
  // 回路を実体化
  val wireBundle = Wire(new Bundle {
    val a = Bool()
    val b = UInt(8.W)
  })

  // 参照する際は通常のScalaのクラスと同様に"."で
  // Bundle内の要素にアクセス
  wireBundle.a := false.B
  wireBundle.b := 1.U

  // BundleをScalaのクラスインスタンスとして作成
  val bundle = new Bundle {
    val a = UInt(8.W)
    val b = UInt(8.W)

    // Scalaのクラスなのでメソッド作ってもOK
    def connect(inA: UInt, inB: UInt): Unit = {
      a := inA
      b := inB
    }
  }

  // 作成したクラスインスタンスを渡して順序回路して実体化
  val regBundle = Reg(bundle)

  regBundle.connect(0x10.U, 100.U)

  printf("wireBundle.a     = 1'b%x\n", wireBundle.a)
  printf("wireBundle.b     = 8'h%x\n", wireBundle.b)
  printf("regBundle.a      = 8'h%x\n", regBundle.a)
  printf("regBundle.b      = 8'h%x\n", regBundle.b)
}
  • 上記サンプルの実行結果
[info] [0.000] Elaborating design...
[info] [0.006] Done elaborating.
Total FIRRTL Compile Time: 21.4 ms
Total FIRRTL Compile Time: 8.8 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1559984715304
[info] [0.000] STEP 0 -> 2
wireBundle.a     = 1'b0
wireBundle.b     = 8'h1
// regBundleはRegで宣言しており、値を設定してないので
// リセット後の値がランダムになる
regBundle.a      = 8'h10
regBundle.b      = 8'h64
wireBundle.a     = 1'b0
wireBundle.b     = 8'h1
// connectメソッドによりリセット後に値が設定される
regBundle.a      = 8'h10
regBundle.b      = 8'h64
test cmd18HelperMyMod Success: 0 tests passed in 7 cycles taking 0.004176 seconds
[info] [0.002] RAN 2 CYCLES PASSED

IOで使うBundle

Chiselで論理回路を設計するにあたり、まず最初にBundleを見るのはまず間違いなく、モジュールのポート宣言だと思います。
これもモジュールの”ポート”を”集約”していますよね。

class MyModule extends Bundle {
  val io = IO(new Bundle {
    val addr = Input(UInt(8.W))
    val wren = Input(Bool())
    val rden = Input(Bool())
    val wrdata = Input(UInt(8.W))
    val rddata = Outut(UInt(8.W))
  })

  // 省略
}

このようにIOポートにBundleを使用した際に<>によって、モジュール間の接続を簡単に行うことが出来るようになります。
ということで上記のMyModuleのポート宣言部分を独立させて、書き直したのが以下のコードです。

// Bundle宣言を独立させる
class RegIO extends Bundle {
  val bits = 8 // これはScalaのIntなのでハードウェア化されない
  val addr = Input(UInt(8.W))
  val wren = Input(Bool())
  val rden = Input(Bool())
  val wrdata = Input(UInt(bits.W))
  val rddata = Output(UInt(bits.W))
}

class RegMod extends Module {
  // IO部分はRegIOをインスタンスする
  val io = IO(new RegIO)

  // 中身は適当なので気にしないでください。
  val reg = RegInit(0.U(8.W))

  val wrSel = (io.addr === 0.U) && io.wren
  val rdSel = (io.addr === 0.U) && io.rden

  when (wrSel) {
    reg := io.wrdata
  }

  io.rddata := Mux(rdSel, reg, 0.U)

  printf("reg = 8'h%x\n", reg)
}

// こちらはRegModへの書き込みを行うモジュール
class Ctrl extends Module {
  val io = IO(Flipped(new RegIO))

  val wren = Reg(Bool())
  val wrdata = RegInit(1.U(8.W))
  val rden = Reg(Bool())

  wrdata := wrdata + 1.U

  wren := !wren
  rden := !wren

  printf("rddata = 8'h%x\n", io.rddata)

  io.addr := 0.U
  io.wren := wren
  io.rden := rden
  io.wrdata := wrdata
}

// 上記2つのモジュールを接続する階層
class Top extends Module {
  val io = IO(new Bundle {})

  val reg = Module(new RegMod)
  val ctrl = Module(new Ctrl)

  // "<>" だけで接続完了!
  reg.io <> ctrl.io
}
  • 実行結果
    • 正常にエラボレートが終わりテストが実行できます。
[info] [0.000] Elaborating design...
[info] [0.011] Done elaborating.
Total FIRRTL Compile Time: 22.2 ms
Total FIRRTL Compile Time: 13.4 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1559987291056
[info] [0.001] STEP 0 -> 10
reg = 8'h0
rddata = 8'h0
reg = 8'h2
rddata = 8'h0
reg = 8'h2
rddata = 8'h2
reg = 8'h4
rddata = 8'h0
reg = 8'h4
rddata = 8'h4
reg = 8'h6
rddata = 8'h0
reg = 8'h6
rddata = 8'h6
reg = 8'h8
rddata = 8'h0
reg = 8'h8
rddata = 8'h8
reg = 8'ha
rddata = 8'h0
test cmd22HelperTop Success: 0 tests passed in 15 cycles taking 0.012225 seconds
[info] [0.012] RAN 10 CYCLES PASSED

上記で見てもらったようにIO用に共通化したクラスを使ってIOを宣言しておくと、そのクラス同士を上位階層で接続する場合には<>演算子を使う事で一行で接続が可能です。
また上記の例の様に接続の関係がMaster-Slaveになるようなケースにおいては対向のポートは端子の入出力が反転することになりますがChiselではFlippedオブジェクトを使うことで、端子の入出力を反転させたBundleインスタンスを生成することが可能になっています。
なお<>演算子を使って接続する場合は同じ型かつ接続されるポート同士の極性が全く同じ or 全部反転している必要があります。それぞれ全く同じ場合は更に上位階層に信号を引き上げるようなケースで、後者はMaster-Slaveの対向に接続する場合です。
そのためこれまで見てきた様なIOに直接new Bundleでポート宣言を記載するような場合には、接続する対向のモジュールにおいては適切にポートの極性を反転する必要があることにご注意ください。
まあそれをやるくらいならBundleクラスを継承してIO用のクラスを作ったほうが楽ですよね。
また場合によっては”殆どのポートはmoduleAとつなぐんだけど一部のポートだけ別のモジュールからもらう信号をつなぐ必要がある”といったケースもあるかと思います。
この場合は最初に<>で接続を行っておき、そのあとで再接続の必要のある信号を繋ぎ直せばOKです。(これはswitchのとこで触れた”Chiselの接続は後に行ったものが有効になる”という仕組みを利用したものです)

ここまででChiselで論理回路を設計する上での基本的な文法はカバーしたかな、、、という感じです。
あとはこれらの要素を組み合わせてやることによって効率的に回路を記述できる感じになっていきます。
次回はクラスパラメータを使った回路のパラメタライズについてを例を交えて紹介しようと思います。