FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages

Chiselを使ったハードウェアデザインでトレイトを活用するためには

f:id:msyksphinz:20190113171015p:plain
Chiselでのトレイトの活用

Chiselはハードウェア記述言語だが、ベースの言語はScalaなので様々な高級言語の機能を活用することができる。 ScalaやRustではトレイトという機能を使うことができる。 トレイトというのはあまり私も詳しくないのだが、

ja.wikipedia.org

  • 振る舞いを実装するメソッド群を提供する。
  • 提供された振る舞いをパラメーター化するメソッド群を必要とする。
  • 状態変数を必要としない。
    • 状態変数を直接には読み書きしない

ということで、ハードウェアのモジュール(Chiselではクラス)に対して分離したメソッドを定義することができるという理解だ。 これにより、ベースのクラスから、必要な機能に従ってトレイトを追加することで、コードの再利用を行うことができるようになる。

トレイトを使ってChiselのハードウェアを実装するために、どのようにすればよいのかいろいろ試行してみた。

ベースとなるクラス

ベースクラスとして、以下のようなモジュールを作ってみる。 入出力として32bitのin_A, in_Bを取り、出力としてhashというポートにハッシュ値を32bitのサイズで出力する。 ここで、out_Cは実質的に使用されていない。別の場所でfunc()を定義しなければならない。

abstract class TraitTest extends Module {
  def Width: Int = 32
  def HashWidth: Int = 32
  def func(valA: SInt, valB: SInt): SInt;
  val io = IO(new Bundle {
    val in_A = Input(SInt(Width.W))
    val in_B = Input(SInt(Width.W))
    val hash = Output(UInt(HashWidth.W))
    val out_C = Output(SInt(Width.W))
  })

  io.out_C := func(io.in_A, io.in_B)
  io.hash  := (io.in_A.asUInt ^ io.in_B.asUInt)(HashWidth-1, 0)

ここで、out_Cに計算させる関数を定義するために、2種類のトレイトを作ってみる。funcを定義し、加算と減算を定義した。 さらに注目なのは、このトレイト内でビット幅を示す変数Widthを変更していること。これで、モジュールごとに出力ポートのサイズを調整できる。

コード修正しました。トレイト内でWidthをオーバライドするためには、TraitTestから継承させる必要があります。

trait AddModuleTrait extends TraitTest {
  override def Width: Int = 64
  def func(valA: SInt, valB: SInt): SInt = {
    return valA + valB
  }
}

trait SubModuleTrait extends TraitTest {
  override def Width: Int = 96
  def func(valA: SInt, valB: SInt): SInt = {
    return valA - valB
  }
}

つまり、AddModuleTraitは加算をするためのモジュール、SubModuleTraitは減算をするためのモジュールである。

このトレイトを使用したモジュールを2種類作ってみる。TraitTestAddTraitTestモジュールをベースにしており、AddModuleTraitをトレイトとして接続している。 一方で、TraitTestを同様にベースモジュールとしているTraitTestSubは、SubModuleTraitをトレイトとして接続している。

class TraitTestAdd extends TraitTest with AddModuleTrait {
  override def Width: Int = 64
  io.out_C := func(io.in_A, io.in_B)
}

class TraitTestSub extends TraitTest with SubModuleTrait {
  io.out_C := func(io.in_A, io.in_B)
}

上記の2種類のモジュールをVerilogとして生成させてみよう。

object TraitTest extends App {
  chisel3.Driver.execute(args, () => new TraitTestAdd())
  chisel3.Driver.execute(args, () => new TraitTestSub())
}

それぞれ、生成されたVerilogは以下のようになり、ベースモジュールに対して機能を切り替えることができるようになった。

それでも、

  assign _T_17 = $signed(io_in_A) + $signed(io_in_B); // @[trait_test.scala 23:17:@13.4]
  assign _T_18 = $signed(io_in_A) + $signed(io_in_B); // @[trait_test.scala 23:17:@14.4]

とか、

  assign _T_17 = $signed(io_in_A) - $signed(io_in_B); // @[trait_test.scala 29:17:@13.4]
  assign _T_18 = $signed(io_in_A) - $signed(io_in_B); // @[trait_test.scala 29:17:@14.4]

と、2回もインスタンスされているのはなんでだろう(論理合成で消えるだろうから別に良いのだけれども)。

  • TraitTestAdd.v
module TraitTestAdd( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [63:0] io_in_A, // @[:@6.4]
  input  [63:0] io_in_B, // @[:@6.4]
  output [31:0] io_hash, // @[:@6.4]
  output [63:0] io_out_C // @[:@6.4]
);
  wire [63:0] _T_13; // @[trait_test.scala 17:23:@8.4]
  wire [63:0] _T_14; // @[trait_test.scala 17:40:@9.4]
  wire [63:0] _T_15; // @[trait_test.scala 17:30:@10.4]
  wire [64:0] _T_17; // @[trait_test.scala 23:17:@13.4]
  wire [63:0] _T_18; // @[trait_test.scala 23:17:@14.4]
  assign _T_13 = $unsigned(io_in_A); // @[trait_test.scala 17:23:@8.4]
  assign _T_14 = $unsigned(io_in_B); // @[trait_test.scala 17:40:@9.4]
  assign _T_15 = _T_13 ^ _T_14; // @[trait_test.scala 17:30:@10.4]
  assign _T_17 = $signed(io_in_A) + $signed(io_in_B); // @[trait_test.scala 23:17:@13.4]
  assign _T_18 = $signed(io_in_A) + $signed(io_in_B); // @[trait_test.scala 23:17:@14.4]
  assign io_hash = _T_15[31:0]; // @[trait_test.scala 17:11:@12.4]
  assign io_out_C = $signed(_T_18); // @[trait_test.scala 35:12:@16.4]
endmodule
  • TraitTestSub.v
module TraitTestSub( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [95:0] io_in_A, // @[:@6.4]
  input  [95:0] io_in_B, // @[:@6.4]
  output [31:0] io_hash, // @[:@6.4]
  output [95:0] io_out_C // @[:@6.4]
);
  wire [95:0] _T_13; // @[trait_test.scala 17:23:@8.4]
  wire [95:0] _T_14; // @[trait_test.scala 17:40:@9.4]
  wire [95:0] _T_15; // @[trait_test.scala 17:30:@10.4]
  wire [96:0] _T_17; // @[trait_test.scala 29:17:@13.4]
  wire [95:0] _T_18; // @[trait_test.scala 29:17:@14.4]
  assign _T_13 = $unsigned(io_in_A); // @[trait_test.scala 17:23:@8.4]
  assign _T_14 = $unsigned(io_in_B); // @[trait_test.scala 17:40:@9.4]
  assign _T_15 = _T_13 ^ _T_14; // @[trait_test.scala 17:30:@10.4]
  assign _T_17 = $signed(io_in_A) - $signed(io_in_B); // @[trait_test.scala 29:17:@13.4]
  assign _T_18 = $signed(io_in_A) - $signed(io_in_B); // @[trait_test.scala 29:17:@14.4]
  assign io_hash = _T_15[31:0]; // @[trait_test.scala 17:11:@12.4]
  assign io_out_C = $signed(_T_18); // @[trait_test.scala 40:12:@16.4]
endmodule

I/Oの追加はどのようにするのか?

モジュールの機能を切り替える方法については何となく理解できるようになったが、 ついでならばトレイトを使ってI/Oを追加したり削除したりしたい。 これはどのようにすれば実現できるのだろう?要調査だ。