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

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

ChiselのBundleの使い方をまとめてみる

スポンサーリンク

前回のChiselの記事では出力ポートに0xdeadbeafを定数で入れたらエラーが出てハマったので解決方法について調べた。

www.tech-diningyo.info

今回は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にも書いてあるので、興味があれば見てみてほしい。

github.com

改めて”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にオプションポートを付ける方法についてのまとめを書く予定。興味があればそちらもどうぞ。