FPGA開発日記

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

Chiselで部分代入を実現するためのいくつかのテクニック

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

Chiselでは、以下のようなビット列の一部に対する部分代入が許されない。 ハードウェア記述言語としてみると非常に不便だが、もともとChiselがソフトウェア記述言語Scalaがベースであるということを考えると何となく想像がつく。

val res = Wire(UInt(32.W))
res(idx) := true.B

ではこれ以外に部分代入を実現する方法は無いのか?

  • bitSetを使う

bitSetはある信号の特定の1ビットを0/1に設定して結果を返す。特定の1ビットのみを更新する場合に使用できる。 例えばこんな感じ。

class BitSet (WIDTH: Int) extends Module {
  val io = IO(new Bundle {
    val in    = Input(UInt(WIDTH.W))
    val valid = Input(Bool())
    val idx = Input(UInt(log2Ceil(WIDTH).W))
    val out = Output(UInt(WIDTH.W))
  })

  io.out := io.in
  when(io.valid) {
    io.out := io.in.bitSet(io.idx, true.B)
  }
}

io.validが有効になると、io.inのうちio.idxで指定された1ビットのみが1にアップデートされてio.outに返される。それ以外はio.inがそのままio.outに返されるという記述になる。

出力されたVerilogは以下の通り。

module BitSet( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [31:0] io_in, // @[:@6.4]
  input         io_valid, // @[:@6.4]
  input  [4:0]  io_idx, // @[:@6.4]
  output [31:0] io_out // @[:@6.4]
);
  wire [31:0] _T_15; // @[bitset.scala 18:27:@10.6]
  wire [31:0] _T_16; // @[bitset.scala 18:27:@11.6]
  assign _T_15 = 32'h1 << io_idx; // @[bitset.scala 18:27:@10.6]
  assign _T_16 = io_in | _T_15; // @[bitset.scala 18:27:@11.6]
  assign io_out = io_valid ? _T_16 : io_in; // @[bitset.scala 16:10:@8.4 bitset.scala 18:12:@16.6]
endmodule
  • 再代入可能なvar変数を使う

これは若干やり方が汚いし、生成させる回路も汚い。なのであまりお勧めしないが、どうしてもこの書き方の方がScala的にきれいになる場合、この方法を使用する。

var変数を使うことで、valと異なり再代入を行うことができるようになる。 例えば、以下の記述は32ビット各ビットに対して特定の演算func()を適用し、その結果を返す。

class VarLoopTest (WIDTH: Int) extends Module {
  val io = IO(new Bundle {
    val in1 = Input(UInt(WIDTH.W))
    val in2 = Input(UInt(WIDTH.W))
    val out = Output(UInt(WIDTH.W))
  })

  def func(a: Bool, b: Bool) : Bool = {
    return a ^ b
  }
  var res = WireInit(0.U(WIDTH.W))
  for (i <- 0 until WIDTH) {
    res = res | (func(io.in1(i), io.in2(i)) << i)
  }

  io.out := res
}

変数resは再代入可能なので、32回ループを回しながら当該ビット位置に結果を当てはめる。 ちなみに生成されるコードはとても汚い。以下のようなVerilogが生成された。なんでこれしきのコードにこんなに長いVerilogファイルが生成されるのか悲しい気持ちになるが。。。

module VarLoopTest( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [31:0] io_in1, // @[:@6.4]
  input  [31:0] io_in2, // @[:@6.4]
  output [31:0] io_out // @[:@6.4]
);
  wire  _T_14; // @[bitset.scala 35:29:@10.4]
  wire  _T_15; // @[bitset.scala 35:40:@11.4]
  wire  _T_16; // @[bitset.scala 31:14:@12.4]
  wire [31:0] _T_18; // @[bitset.scala 35:15:@14.4]
  wire  _T_19; // @[bitset.scala 35:29:@15.4]
  wire  _T_20; // @[bitset.scala 35:40:@16.4]
  wire  _T_21; // @[bitset.scala 31:14:@17.4]
  wire [1:0] _GEN_0; // @[bitset.scala 35:45:@18.4]
  wire [1:0] _T_22; // @[bitset.scala 35:45:@18.4]
  wire [31:0] _GEN_1; // @[bitset.scala 35:15:@19.4]
...
  wire  _T_171; // @[bitset.scala 31:14:@167.4]
  wire [31:0] _GEN_60; // @[bitset.scala 35:45:@168.4]
  wire [31:0] _T_172; // @[bitset.scala 35:45:@168.4]
  assign _T_14 = io_in1[0]; // @[bitset.scala 35:29:@10.4]
  assign _T_15 = io_in2[0]; // @[bitset.scala 35:40:@11.4]
  assign _T_16 = _T_14 ^ _T_15; // @[bitset.scala 31:14:@12.4]
  assign _T_18 = {{31'd0}, _T_16}; // @[bitset.scala 35:15:@14.4]
...
  assign _GEN_60 = {{31'd0}, _T_171}; // @[bitset.scala 35:45:@168.4]
  assign _T_172 = _GEN_60 << 31; // @[bitset.scala 35:45:@168.4]
  assign io_out = _T_168 | _T_172; // @[bitset.scala 38:10:@170.4]
endmodule

まあ、この程度のサンプルコードであれば、以下のように書いた方がきれいだし、生成されるVerilog的にも(比較的)心の平安が保たれる(これでも生成されるコードはかなりどぎついが...)

class VarLoopTest (WIDTH: Int) extends Module {
  val io = IO(new Bundle {
    val in1 = Input(UInt(WIDTH.W))
    val in2 = Input(UInt(WIDTH.W))
    val out = Output(UInt(WIDTH.W))
  })

  def func(a: Bool, b: Bool) : Bool = {
    return a ^ b
  }
  var res = Wire(Vec(WIDTH, Bool()))
  for(i <- 0 until WIDTH) {
    res(i) := func(io.in1(i), io.in2(i))
  }

  io.out := res.asUInt
}