前回の続きでChiselの文法入門編その7
今回はChiselにおける拡張型の続きとしてBundle
についてを見ていきます
Chisel入門編〜その7:Bundle〜
前回の冒頭にも書いたとおりBundle
はChiselにおける拡張型とも言える概念です。
Vec
は同一の型を複数個まとめて扱うことができるためVerilog HDLにおけるメモリ配列に相当するものでしたが、Bundle
は関連のある要素を混在してひとつの新しい型を作り出すことが出来ます。これはSystem Verilogで言う構造体やクラスに相当する概念になります。
このように同一もしくは複数の型を集めて新しい型を作るため、Chiselにおけるクラス構造ではAggregate
型を継承した形になっています。
既にモジュールのIO宣言に使われているので、イメージはしやすいかと思います。
なおBundle
についてはこのブログでも何度か取り上げていますので、ご興味アレばそちらもご一読くださいm(_ _)m
- ChiselのBundleの使い方をまとめてみる - ハードウェアの気になるあれこれ
- ChiselのBundleをvalで受けるといろいろ便利だった話 - ハードウェアの気になるあれこれ
- ChiselのBundleをRegInitに渡すときに各フィールドを任意の値で初期化する方法(1) - ハードウェアの気になるあれこれ
- ChiselのBundleをRegInitに渡すときに各フィールドを任意の値で初期化する方法(2) - ハードウェアの気になるあれこれ
- 自分で作ったBundleでレジスタ作ったらハマった話 - ハードウェアの気になるあれこれ
Bundle
の宣言
早速Bundle
の使い方を見て行きたいと思います。
まずは宣言ですが、これは単純にBundle
を継承するだけでOKです。
class <所望のクラス名> extends Bundle { // ここに集約したい変数を宣言していく // Chiselのデータ型がインスタンス時にWire/Reg // といったハードウェア要素のラップ対象になる }
上記のようにBundle
クラスを継承したScalaのクラス宣言なのでクラス内には変数宣言やメソッド宣言を含むことが可能になっています。
コメント部分に記載したように、インスタンスして使用する際にハードウェア要素でくるんだ場合にハードウェア化されるのはChiselのデータ型のみになっています。
そのため”計算してデータのビット幅を決めたい”と言ったような場合にはScalaのInt
等でビット幅の計算を行い、それを使ってChiselのデータ型を宣言すればOKです。
例で見るBundle
の使い方
使うときには自分で作ったBundle
をnew
でインスタンス化し、必要に応じて適切なハードウェア要素でくるんで回路として実体化させます。
以下は簡単な例です。
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で論理回路を設計する上での基本的な文法はカバーしたかな、、、という感じです。
あとはこれらの要素を組み合わせてやることによって効率的に回路を記述できる感じになっていきます。
次回はクラスパラメータを使った回路のパラメタライズについてを例を交えて紹介しようと思います。