前回のChiselの記事では出力ポートに0xdeadbeaf
を定数で入れたらエラーが出てハマったので解決方法について調べた。
今回はChisel-Bootcampではサラッと流されているChiselのBundle
を使ったデータの構造化についてをまとめてみる。
Bundle
は”データを構造化”するもの
ChiselはBundle
を使うことで、データを構造化出来る(system verilogとCの構造体みたいもの)。
今回はこのBundle
の使い方について簡単なサンプルで見ていこうと思う。
Bundle
の使い方
これは特に解説することもあんまりないが、基本形は以下のような形。
class <適当な名前> extends Bundle { <まとめたいデータの定義を並べて書いてく> }
使用例
Chiselを触り始めた時に一番最初に目にするBundle
の使われ方は、まず間違いなくIOの宣言部分だと思う。
例えば以下のような感じ。
class MyRegister extends Module { // よく見るBundleの使い方 val io = IO(new Bundle { val addr = Input(UInt(32.W)) val wren = Input(Bool()) val rden = Input(Bool()) val wrdata = Input(UInt(32.W)) val rddata = Input(UInt(32.W)) }) val reg = RegInit(0.U(32.W)) when (io.wren) { reg := wrdata } io.rddata := Mux(0.U(32.W), io.rden, reg) }
最初はよくわからずに"Chiselでモジュール書くときはIOをこんな感じで定義する”という一種のおまじないみたいな感じで捉えていた。。。
でも別にBundle
はIOの時にだけ使うものではなく、冒頭に書いたようにデータを構造化するという役割を持っていた。
そのあたりはChiselのWikiにも書いてあるので、興味があれば見てみてほしい。
改めて”Bundle
はデータを構造化するもの”
”改めて”と書いたのは"Bundle
を使える場所は、別にIOの宣言部分に限ったものではない"ということを強調するため。
例えばちょっと凝ったレジスタを用意したい場合とかにもBundle
は使える。
先ほどのレジスタモジュールの仕様をちょっと変更して、以下のように幾つかの設定ビットからなるレジスタを作る場合のことを考えてみよう。
#コマンドレジスタなら、stat
がbusyになった時に対向にアクセス出せよ、、って話ですがめんどいので割愛。
bit | mnemonic | attr | detail |
---|---|---|---|
31 | stat | r | 1'b0 : idle / 1'b1 : busy |
24 | cmd | r/w | 1’b0 : read / 1'b1 : write |
15:8 | addr | r/w | アクセス先アドレス |
7:0 | data | r/w | リード時:読みだした値/ライト時:書き込む値 |
Verilogで書くと以下のような感じのはず(コンパイルしてない)。
module MyReg ( input clk ,input rst ,input [31:0] addr ,input wren ,input rden ,input [31:0] wrdata ,output [31:0] rddata ); reg r_stat; // 1'b0 : idle / 1'b1 : busy reg r_cmd; // 1'b0 : read / 1'b1 : write reg [7:0] r_addr; // アクセス先アドレス reg [7:0] r_data; // リード時:読みだした値/ライト時:書き込む値 assign rddata = {r_stat, {7{1'b0}}, r_cmd, {7{1'b0}}, r_addr, r_data}; always @(posedge clk) begin if (rst) begin r_stat <= 1'b0; r_cmd <= 1'b0; r_addr <= 8'h0; r_data <= 8'h0; end else if ((!r_stat) && wren) begin r_stat <= wrdata[31]; r_cmd <= wrdata[24]; r_addr <= wrdata[15:8]; r_data <= wrdata[ 7:0]; end end endmodule // MyReg
これをChiselでBundle
使って書くと以下のようになる。簡単な例ではあるが、ちょっとだけすっきりした、、、かな??
class CmdReg extends Bundle { val stat = Bool() val cmd = Bool() val addr = UInt(8.W) val data = UInt(8.W) } class MyRegister extends Module { val io = IO(new Bundle { val addr = Input(UInt(32.W)) val wren = Input(Bool()) val rden = Input(Bool()) val wrdata = Input(UInt(32.W)) val rddata = Output(UInt(32.W)) }) // CmdRegのメンバをRegに確保して0で初期化 val reg = RegInit(0.U.asTypeOf(new CmdReg)) when (!reg.stat && io.wren) { reg.stat := true.B reg.cmd := io.wrdata(24) reg.addr := io.wrdata(15, 8) reg.data := io.wrdata(7, 0) } .otherwise { reg.stat := false.B } io.rddata := Mux(io.rden, Cat(Seq( reg.stat, 0.U(7.W), reg.cmd, 0.U(7.W), reg.addr, reg.data)), 0.U) }
因みにBundle
は宣言を見てわかる通り、Scalaのクラスなので必要に応じて中にメソッドを定義しても問題ないです。上記の例ではCmdReg
側にライト/リードのタスクを定義するのもOK(やるかは別として)。
class CmdReg extends Bundle { // メンバは一緒なので省略 // とりあえずリードだけ追加 def read(rden: Bool): UInt = { val regData = Cat(Seq( reg.stat, 0.U(7.W), reg.cmd, 0.U(7.W), reg.addr, reg.data)) val rdData = Mux(rden, regData, 0.U) rdData } } class MyRegister extends Module { val io = IO(new Bundle { val addr = Input(UInt(32.W)) val wren = Input(Bool()) val rden = Input(Bool()) val wrdata = Input(UInt(32.W)) val rddata = Output(UInt(32.W)) }) val reg = RegInit(0.U.asTypeOf(new CmdReg)) // ライトは同じなので省略 io.rddata := reg.read(io.rden) }
I/Oポートで使うのはいろいろと便利
I/Oポートの構造化
Bundle
を使うとI/Oポートレベルでも構造化が出来て見た目スッキリさせることが可能。
以下の例はすごく適当なready-valid
のハンドシェイクを使ったI/Fの定義をBundle
を使って書いてみたもの。
class AddrChannel extends Bundle { val valid = Output(Bool()) val ready = Input(Bool()) val cmd = Input(Bool()) val addr = Output(UInt(32.W)) } class DataChannel extends Bundle { val valid = Output(Bool()) val ready = Input(Bool()) val data = Output(UInt(32.W)) } class MyIO extends Bundle { val ach = new AddrChannel val dch = new DataChannel } class MyModule extends Module { val io = IO(new MyIO) io.ach.valid := false.B io.ach.addr := 0x12345678.U io.dch.valid := false.B io.dch.data := 0.U }
これだけだとあんまりメリット感じないかもしれないがBundle
で構造化したポートは以下のような形で1行で接続が可能になる。
class MyTop extends Module { val io = IO(new MyIO) val mod = Module(new MyModule) io <> mod.io }
上記の例では更にチャネルごとにBundle
で束ねているので以下のような別々に接続することも可能。
class MyTop extends Module { val io = IO(new MyIO) val mod = Module(new MyModule) io.ach <> mod.io.ach io.dch <> mod.io.dch }
上記の2つは以下のようなRTLが生成される。
module cmd48HelperMyTop( // @[:@13.2] input clock, // @[:@14.4] input reset, // @[:@15.4] output io_ach_valid, // @[:@16.4] input io_ach_ready, // @[:@16.4] input io_ach_cmd, // @[:@16.4] output [31:0] io_ach_addr, // @[:@16.4] output io_dch_valid, // @[:@16.4] input io_dch_ready, // @[:@16.4] output [31:0] io_dch_data // @[:@16.4] ); assign io_ach_valid = 1'h0; assign io_ach_addr = 32'h12345678; assign io_dch_valid = 1'h0; assign io_dch_data = 32'h0; endmodule
またChiselは一番最後に評価した接続が有効になるため、ほとんどの端子はモジュールAとの接続なんだけど、この端子だけOR
取りたいみたい状況が発生する場合でも<>
で接続した後に、部分的につなぎ直せばOK。
class MyTop extends Module { val io = IO(new MyIO) val mod = Module(new MyModule) io <> mod.io io.ach.addr := 0x77777777.U // io.ach.addrだけ別の値で上書き }
- 生成されるRTL
module cmd47HelperMyTop( // @[:@13.2] input clock, // @[:@14.4] input reset, // @[:@15.4] output io_ach_valid, // @[:@16.4] input io_ach_ready, // @[:@16.4] input io_ach_cmd, // @[:@16.4] output [31:0] io_ach_addr, // @[:@16.4] output io_dch_valid, // @[:@16.4] input io_dch_ready, // @[:@16.4] output [31:0] io_dch_data // @[:@16.4] ); assign io_ach_valid = 1'h0; assign io_ach_addr = 32'h77777777; // 上書きした値に変化している assign io_dch_valid = 1'h0; assign io_dch_data = 32'h0; endmodule
あとMaster/Slaveでポートをひっくり返すような場合はFlipped
使えば一発。
class MyFlippedModule extends Module { val io = IO(Flipped(new MyIO)) io := DontCare }
- 生成されるRTL
RTL上のI/Oの方向がひっくり返ってるのがわかるかと思う。
このようにFlipped
でひっくり返して作ったポートも<>
を使うことで接続が可能だ。
module cmd51HelperMyFlippedModule( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input io_ach_valid, // @[:@6.4] output io_ach_ready, // @[:@6.4] output io_ach_cmd, // @[:@6.4] input [31:0] io_ach_addr, // @[:@6.4] input io_dch_valid, // @[:@6.4] output io_dch_ready, // @[:@6.4] input [31:0] io_dch_data // @[:@6.4] ); assign io_ach_ready = 1'h0; assign io_ach_cmd = 1'h0; assign io_dch_ready = 1'h0; endmodule
ということで、今日はサンプルを見ながらChiselのBundle
について今の自分の認識で使えそうな機能をまとめた見た。今回例にしたレジスタの実装ももう少し工夫すればもっと効率的に書けそうな気もしてるんだけど、如何せん自分のScala&Chiselの理解度不足により、そこまでには至っていない。
なのでそのあたりについてはもう少しいろいろ掘り下げてから、もう一度まとめてみようと思う。
次の記事もBundle
を使ったネタにする予定で、取り上げるのはChiselのモジュールのIOにオプションポートを付ける方法についてのまとめを書く予定。興味があればそちらもどうぞ。