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

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

Chiselの文法 - 入門編 〜その4:Chiselの演算子〜

スポンサーリンク

前回の続きでChiselの文法入門編その4
4回目はChiselの各演算子について。

Chisel入門編〜その4:Chiselの演算子

これについては”Chiselではこう書く”という文法のお話になるので、Verilog HDLの記述と見比べつつ各種演算について記載していきます。
挙動を紹介する必要のある部分については、コンパイル可能なモジュール形式でのソースを記載し、結果についても適宜補足を行っていきます。 まず紹介を始める前に、言葉の定義をしておきます。

  • 定数値:Chiselの定数表現。例えばfalse.B0.Uのようなものを指します
  • ハードウェア要素:Chiselのハードウェアの要素。前回紹介したとおりでMoudle/IO/Wire/Reg(及びその派生種)を指しますが、ここではModuleは除きます。

代入(というか接続と言ったほうがいいかも)

  • Verilog HDL
    • Verilog HDLではwireregで扱いが異なる
      • wireの場合:assign <信号名> = <値 or 変数>
      • regの場合:通常ノンブロッキング代入<=を使う。<信号名> <= <値 or 変数>
wire a;
reg b;

assgin a = 1'b1;

alwasy @(posedge clk or negedge rst) begin
  if (!rst) begin
    b <= 1'b0;
  end
  else begin
    b <= 1'b1;
  end
end
  • Chisel
    • ハードウェア要素への代入は:=演算子を使って行う
      • <接続先ハードウェア要素> := <定数値 or 接続元ハードウェア要素>
    • RTLに変換した際にassignになるかノンブロッキング代入<=になるかは接続先ハードウェア要素によって変わります ex.)
  val a = Wire(Bool())
  a := false.B          // 定数値を接続/WireなのでRTL変換時にはassignに変換されます
  val b = Reg(Bool())
  b := a                // ハードウェア要素の接続/Regなのでalwaysブロック内でノンブロッキング代入になります

四則演算(剰余も含む)

これについては基本的にはVerilog HDLと一緒だが、加算/減算にはオーバーフロー/アンダーフロー発生時のビットの扱いに応じて2つの演算子が定義されている。

  • Verilog HDL

    • 加算 : a + 1'b1;
    • 減算 : a - 1'b1;
    • 乗算 : a * 2'b2;
    • 除算 : a / 2'b2;
    • 剰余 : a % 2'b2;
  • Chisel

    • 加算
      • 通常 : a + <ハードウェア要素 or 定数値>
      • オーバーフロービット拡張 : a +& <ハードウェア要素 or 定数値>
        • 例えば8bit同士の加算をした際の結果をvalで新しい変数に格納すると、新しい変数のビット幅が9bitになります。
    • 減算 : a - <ハードウェア要素 or 定数値>
      • 通常 : a - <ハードウェア要素 or 定数値>
      • アンダーフロービット拡張 : a -& <ハードウェア要素 or 定数値>
    • 乗算 : a * <ハードウェア要素 or 定数値>
    • 除算 : a / <ハードウェア要素 or 定数値>
    • 剰余 : a % <ハードウェア要素 or 定数値>

上記のChiselの演算をビット幅も込みで試してみたのが以下のソースコードになります。

class ArithOps(baseVal: Int) extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(8.W))
    val b = Input(UInt(8.W))
  })

  val a = io.a
  val b = io.b

  val addInit = a + b
  val subInit = a - b
  val addInit2 = a +& b
  val subInit2 = a -& b
  val mulInit = a * b
  val divInit = a / b
  val modInit = a % b

  val addInit3 = Wire(UInt(8.W))
  val subInit3 = Wire(UInt(8.W))

  addInit3 := a +& b
  subInit3 := a -& b

  println(s"addInit Width = ${addInit.getWidth}")
  println(s"addInit2 Width = ${addInit2.getWidth}")
  println(s"addInit3 Width = ${addInit3.getWidth}")
  println(s"subInit Width = ${addInit.getWidth}")
  println(s"subInit2 Width = ${addInit2.getWidth}")
  println(s"subInit3 Width = ${addInit3.getWidth}")
  println(s"mulInit Width = ${mulInit.getWidth}")
  println(s"divInit2 Width = ${divInit.getWidth}")
  println(s"modInit Width = ${modInit.getWidth}")

  printf("addInit : %d\n", addInit)
  printf("subInit : %d\n", subInit)
  printf("mulInit : %d\n", mulInit)
  printf("divInit : %d\n", divInit)
  printf("modInit : %d\n", modInit)

  val add = a +& b
  val sub = a -& b
  val mul = a *  b
  val div = a /  b
  val mod = a &  b

  printf("add : %d\n", add)
  printf("sub : %d\n", sub)
  printf("mul : %d\n", mul)
  printf("div : %d\n", div)
  printf("mod : %d\n", mod)
}
  • 実行結果
[info] [0.000] Elaborating design...
addInit Width = 8
addInit2 Width = 9 // "+&"を使っているのでbit幅が9bit
addInit3 Width = 8 // "+&"を使っているが変数定義自体はUInt(8.W)なのでbit幅は8bit
subInit Width = 8
subInit2 Width = 9 // -&も一緒
subInit3 Width = 8 // -&も一緒
mulInit Width = 16
divInit2 Width = 8
modInit Width = 8
[info] [0.049] Done elaborating.
Total FIRRTL Compile Time: 15.9 ms
Total FIRRTL Compile Time: 11.4 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1559360234623
// a == 1 / b == 2の結果
addInit : 3
subInit : 255 // 1 - 2 -> -1 = 0xff
mulInit : 2
divInit : 0
modInit : 1
add : 3
sub : 511
mul : 2
div : 0
mod : 0

比較

以下の4つについてはVerilog HDLとChiselで共通

  • Verilog HDL/Chisel
    • より大きい : a > b
    • 以上 : a >= b
    • より小さい : a < b
    • 以下 : a <= b

”等しい”と"等しくない"はVerilog HDLとは異なります。

  • Verilog HDL
    • 等しい : a == b
    • 等しくない : a != b
  • Chisel
    • 等しい : a === b
    • 等しくない : a =/= b // 実は!=もあるにはあるがdeprecated扱いなので使わないほうがよいです

以下のコードは各種演算結果の戻り値の型を確認するためのものです。

class CompareOps extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(8.W))
    val b = Input(UInt(8.W))
  })

  val a = io.a
  val b = io.b

  val equal = a === b
  val notEqual = a =/= b
  val lessThan = a < b
  val greaterThan = a > b
  val lessThanEqual = a <= b
  val greaterThanEqual = a >= b

  println(equal)
  println(notEqual)
  println(lessThan)
  println(greaterThan)
  println(lessThanEqual)
  println(greaterThanEqual)

  printf("equal            : %b\n", equal)
  printf("notEqual         : %b\n", notEqual)
  printf("lessThan         : %b\n", lessThan)
  printf("greaterThan      : %b\n", greaterThan)
  printf("lessThanEqual    : %b\n", lessThanEqual)
  printf("greaterThanEqual : %b\n", greaterThanEqual)
}
  • 実行結果
// 以下のように戻り値は全てBoolになります
[info] [0.000] Elaborating design...
chisel3.core.Bool@11
chisel3.core.Bool@12
chisel3.core.Bool@13
chisel3.core.Bool@14
chisel3.core.Bool@15
chisel3.core.Bool@16
[info] [0.004] Done elaborating.
Total FIRRTL Compile Time: 5.0 ms
Total FIRRTL Compile Time: 4.0 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1559372898505
// io.a = 1 / io.b = 2
equal            : 0
notEqual         : 1
lessThan         : 1
greaterThan      : 0
lessThanEqual    : 1
greaterThanEqual : 0

論理演算

論理演算ももほぼVerilog HDLとChiselで共通になります。

  • 論理演算
    • 論理積(AND):a && b (ChiselではBoolのみで使用可能(UInt(1.W)では使用できない)
    • 論理和(OR):a || b (ChiselではBoolのみで使用可能(UInt(1.W)では使用できない)
    • 否定(NOT) : !a
    • 排他的論理和 : a ^ b
  • ビット単位の論理演算

リダクション演算についてはVerilog HDLとChiselで異なります。

以下のコードはChiselの論理演算を試したものです。

class LogicalOps extends Module {
  val io = IO(new Bundle {
    val a = Input(Bool())
    val b = Input(Bool())
    val c = Input(UInt(8.W))
    val d = Input(UInt(8.W))
  })

  val a = io.a
  val b = io.b
  val c = io.c
  val d = io.d

  val and = a && b
  val or = a || b
  val not = !a
  val xor = a ^ b
  //val bitwiseAnd = c && d // UIntなのでError
  //val bitwiseOr = c || d  // UIntなのでError
  val bitwiseAnd = c & d
  val bitwiseOr = c | d
  val bitwiseNot = ~c
  val bitwiseXor = c ^ d
  val reductionAnd = c.andR
  val reductionOr = c.orR
  val reductionXor = c.xorR

  printf("and          : %d'b%b\n", and.getWidth.U, and)
  printf("or           : %d'b%b\n", or.getWidth.U, or)
  printf("not          : %d'b%b\n", not.getWidth.U, not)
  printf("xor          : %d'b%b\n", xor.getWidth.U, xor)
  printf("bitwiseAnd   : %d'b%b\n", bitwiseAnd.getWidth.U, bitwiseAnd)
  printf("bitwiseOr    : %d'b%b\n", bitwiseOr.getWidth.U, bitwiseOr)
  printf("bitwiseNot   : %d'b%b\n", bitwiseNot.getWidth.U, bitwiseNot)
  printf("bitwiseXor   : %d'b%b\n", bitwiseXor.getWidth.U, bitwiseXor)
  printf("reductionAnd : %d'b%b\n", reductionAnd.getWidth.U, reductionAnd)
  printf("reductionOr  : %d'b%b\n", reductionOr.getWidth.U, reductionOr)
  printf("reductionXor : %d'b%b\n", reductionXor.getWidth.U, reductionXor)
}
  • 実行結果
// a = true.B / b = false.B / c = 0xaa.U / d = 0x55.U
and          : 1'b0
or           : 1'b1
not          : 1'b0
xor          : 1'b1
bitwiseAnd   : 8'b0
bitwiseOr    : 8'b11111111
bitwiseNot   : 8'b1010101
bitwiseXor   : 8'b11111111
reductionAnd : 1'b0
reductionOr  : 1'b1
reductionXor : 1'b0

連接

Verilog HDLでは以下のように定義されている各信号を束ねる処理を指します。

assign a = {<信号 or 定数値>, ... <信号 or 定数値>}

Chiselでは以下のいずれかの方法でビットの連接を実行します
Catを使う方法についてはいずれかの方法をでその時々に合わせて選択します。

val a = <ハードウェア要素 or 定数値> ## <ハードウェア要素 or 定数値> // これ今回Chiselのソース見てて気づいた。。。
val a = Cat(<ハードウェア要素 or 定数値>, .... , <ハードウェア or 定数値>)
val a = Cat(Seq(<ハードウェア要素 or 定数値>, ... , <ハードウェア要素 or 定数値>))

なおCatchisel3.utilに存在するオブジェクトなのでインポートが必要になります。
実装は以下のようにobjectになっており、引数に応じて適切なapplyメソッドが呼び出されます。
以下の定義をみてもらうとわかるかと思いますが、どちらの形を使うにしても型パラメータがT <: Bitsのみなので、型の混在は出来ません。

object Cat {
  /** Concatenates the argument data elements, in argument order, together. The first argument
    * forms the most significant bits, while the last argument forms the least significant bits.
    */
  def apply[T <: Bits](a: T, r: T*): UInt = apply(a :: r.toList)

  /** Concatenates the data elements of the input sequence, in reverse sequence order, together.
    * The first element of the sequence forms the most significant bits, while the last element
    * in the sequence forms the least significant bits.
    *
    * Equivalent to r(0) ## r(1) ## ... ## r(n-1).
    */
  def apply[T <: Bits](r: Seq[T]): UInt = SeqUtils.asUInt(r.reverse)
}

実際に挙動を試してみたのが以下のコードになります。

class CatOps extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(8.W))
    val b = Input(UInt(8.W))
    val c = Input(UInt(8.W))
    val d = Input(UInt(8.W))
  })

  val a = io.a
  val b = io.b
  val c = io.c
  val d = io.d

  val cat0 = a ## b ## c ## d
  val cat1 = Cat(a, b, c, c)
  val cat2 = Cat(Seq(a, b, c, d))
  //val cat2Error = Cat(a, Seq(b, c, d)) // 型の混在はNG
  val vec3 = WireInit(VecInit(Seq(a, b, c, d)))
  val cat3 = Cat(vec3)
  val vec4 = WireInit(VecInit(Seq(b, c, d)))
  //val cat4Error = Cat(a, vec4) // こちらもUIntとVec[UInt]で型が混在するのでNG

  printf("cat0 : %d'h%x\n", cat0.getWidth.U, cat0)
  printf("cat1 : %d'h%x\n", cat1.getWidth.U, cat1)
  printf("cat2 : %d'h%x\n", cat2.getWidth.U, cat2)
  printf("cat3 : %d'h%x\n", cat3.getWidth.U, cat3)
}
  • 実行結果
[info] [0.000] SEED 1559365774378
// 全て同じ結果が得られる
cat1 : 32'h1020303
cat2 : 32'h1020304
cat3 : 32'h1020304

ビット選択

ビット選択の処理はChiselでは括弧が異なるだけ。これはScalaのcollection型の要素を指定する際の書式と一緒です。
ただ、サンプルコードに記載したとおりビット選択処理はChiselの処理的にはread-onlyの扱いになっているため、"sink"(データの宛先)に使うことが出来ません。
簡単に言うとa(0) := b(0)という処理が出来ません。

assign a = b[0];
assign a = b[9:0];
assign a[1] = b[0];
  • Chisel
a := b(MSB<ハードウェア要素 or 定数値>[, LSB<ハードウェア要素 or 定数値>])
a := b.head(<n:Intでビット指定>) // MSBからnまでを取得(b(b.getWidth-1, n)と同じ)
a := b.tail(<n:Intでビット指定>) // LSBからnまでを取得(b(n-1, 0)と同じ)
// a(1) := b(0) // **これはChiselでは出来ない**
// -> chisel3.internal.ChiselException: Cannot reassign to read-only chisel3.core.UInt@11

ということで上記の要素をまとめたコードは以下になります。

class BitSelOps extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(8.W))
  })

  val a = io.a

  val singleBit = a(7)
  val multiBits = a(7, 4)
  val headBits = a.head(4)
  val tailBits = a.tail(4)

  printf("singleBit : %d'h%x\n", singleBit.getWidth.U, singleBit)
  printf("multiBits : %d'h%x\n", multiBits.getWidth.U, multiBits)
  printf("headBits  : %d'h%x\n", headBits.getWidth.U, headBits)
  printf("tailBits  : %d'h%x\n", tailBits.getWidth.U, tailBits)
}
  • 実行結果
// io.a = 0xab
singleBit : 1'h1
multiBits : 4'ha
headBits  : 4'ha
tailBits  : 4'hb

各種演算についてはここまで。次回その5は制御構文について↓

www.tech-diningyo.info