FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

ChiselでBundleの演算子をオーバロードする方法

https://cdn-ak.f.st-hatena.com/images/fotolife/m/msyksphinz/20181105/20181105012126.png

ハードウェア記述言語の一種であるChiselは、Scalaをベースにした言語であり、Scalaの機能を使って様々な便利な記法が実現可能だ。

その一つの便利な手法として、Bundleを使った演算子のオーバロードがある。SystemVerilogからの移行や、Chiselでの便利な書き方の一つとしてメモしておきたい。

Bundleとは、SystemVerilogのstructのようなもの。複数の信号をまとめて、一つの信号タイプとして定義する。 以下の例は、Bundle ExampleBundleを定義する。ExampleBundleには、upperlowerのそれぞれ32ビットの信号が含まれている。

class ExampleBundle extends Bundle {
  val upper = UInt(32.W)
  val lower = UInt(32.W)
}

このBundleは、Chiselのモジュール内で様々な形で使用することができる。SystemVerilogと同様、信号の入出力と肩に使用したり、内部ステートを記憶するためのレジスタや配線として定義することもできる。 以下は単純な例だが、ExampleBundle型の信号を一度レジスタで叩き、その結果を出力する(まあスライサみたいなものだ)。

  • モジュール : ExampleModule
  • 入力ポート : in_a(ExampleBundle型)
  • 入力ポート : out_a(ExampleBundle型)
class ExampleBundle extends Bundle {
  val upper = UInt(32.W)
  val lower = UInt(32.W)
}

class ExampleModule extends Module {
  val io = IO(new Bundle {
    val in_a = Input(new BundleExample)
    val out_a = Output(new BundleExample)
  })

  val reg_a = Reg(new BundleExample)
  reg_a := io.in_a
  io.out_a := reg_a
}

Verilogを生成すると、以下のようになる。

module ExampleModule( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [31:0] io_in_a_upper, // @[:@6.4]
  input  [31:0] io_in_a_lower, // @[:@6.4]
  output [31:0] io_out_a_upper, // @[:@6.4]
  output [31:0] io_out_a_lower // @[:@6.4]
);
  reg [31:0] reg_a_upper; // @[bundle_example.scala 18:18:@8.4]
  reg [31:0] reg_a_lower; // @[bundle_example.scala 18:18:@8.4]
  assign io_out_a_upper = reg_a_upper; // @[bundle_example.scala 20:12:@12.4]
  assign io_out_a_lower = reg_a_lower; // @[bundle_example.scala 20:12:@11.4]
...
  always @(posedge clock) begin
    reg_a_upper <= io_in_a_upper;
    reg_a_lower <= io_in_a_lower;
  end

上記は単純な例だが、BundleExample型で定義されていない、BundleExample型同士の演算や、UInt型からのBundleExampleへの代入を行ってみると、当然エラーとなる。

class ExampleModule extends Module {
  val io = IO(new Bundle {
    val in_a = Input(new ExampleBundle)
    val out_a = Output(new ExampleBundle)
  })

  val reg_a = Reg(new ExampleBundle)
  val const_1 = Wire(new ExampleBundle)
  const_1 := 1.U

  reg_a := io.in_a + const_1
  io.out_a := reg_a
}
[error] (run-main-0) chisel3.internal.ChiselException: Connection between sink (bundle_example.ExampleBundle@2a) and source (chisel3.core.UInt@2d) failed @: Sink (bundle_example.ExampleBundle@2a) and Source (chisel3.core.UInt@2d) have different types.
[error] chisel3.internal.ChiselException: Connection between sink (bundle_example.ExampleBundle@2a) and source (chisel3.core.UInt@2d) failed @: Sink (bundle_example.ExampleBundle@2a) and Source (chisel3.core.UInt@2d) have different types.

これは、ExampleBundle型で、

が定義されていないことに依存する。C++経験者なら容易に想像できるだろうが、これは演算子のオーバロードで解決することができる。

つまり、ExamlpeBundleに対して、以下を定義する。

class ExampleBundle extends Bundle {
  val upper = UInt(32.W)
  val lower = UInt(32.W)

  def := (that: UInt) : ExampleBundle = {
    this.upper := that(63,32)
    this.lower := that(31, 0)

    this
  }

  def + (that: ExampleBundle) : ExampleBundle = {
    val res = Wire(new ExampleBundle)
    res.upper := this.upper + that.upper
    res.lower := this.lower + that.lower

    res
  }
}

C++と同様に見れば良く分かる。def :=の方は、現在のExampleBundleの実体thisに対してUIntの値thatを分割して代入する。その結果はthisとして返される。 また、def +の場合は新たな実体ExampleBundleを定義して、そこに対して実体thisともう一つのオペランドthatを代入し、その結果をresとして返す。

これでVerilogを生成すると、正しくVerilogが生成されていることが分かる。このように、新たなBundleを定義した場合、同様に多用する演算子も定義しておくと、うまくBundleを活用することができる。

module ExampleModule( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [31:0] io_in_a_upper, // @[:@6.4]
  input  [31:0] io_in_a_lower, // @[:@6.4]
  output [31:0] io_out_a_upper, // @[:@6.4]
  output [31:0] io_out_a_lower // @[:@6.4]
);
  reg [31:0] reg_a_upper; // @[bundle_example.scala 34:18:@8.4]
  reg [31:0] reg_a_lower; // @[bundle_example.scala 34:18:@8.4]
  wire [32:0] _T_16; // @[bundle_example.scala 20:29:@13.4]
  wire [32:0] _T_18; // @[bundle_example.scala 21:29:@16.4]
  assign _T_16 = {{1'd0}, io_in_a_upper}; // @[bundle_example.scala 20:29:@13.4]
  assign _T_18 = io_in_a_lower + 32'h1; // @[bundle_example.scala 21:29:@16.4]
  assign io_out_a_upper = reg_a_upper; // @[bundle_example.scala 39:12:@22.4]
  assign io_out_a_lower = reg_a_lower; // @[bundle_example.scala 39:12:@21.4]
...
  always @(posedge clock) begin
    reg_a_upper <= _T_16[31:0];
    reg_a_lower <= io_in_a_lower + 32'h1;
  end