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

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

ChiselのUIntで大きな定数を使おうとしてハマった話

スポンサーリンク

前回のChiselの記事ではChisel-bootcampのModule3.5のコンパニオン・オブジェクトとケース・クラスについてを勉強した。

www.tech-diningyo.info

今回はちょっとChisel-Bootcampから離れてChiselを試していたハマったUIntの大きい定数値の扱いについてを紹介していく。

ChiselのUIntで大きな定数を使おうとしてハマった話

最近になってやっとある程度思い通りにChiselでの実装が出来るようになってきて、ちょっとずつ作りたいモジュールを実装を始めている。 その際にChiselデバッグ用のポートを作ろうとして、とりあえず挙動を調べるためのコードを書いていた時にUIntの大きめの定数値(32bitでMSBが0x1になるような数字)でハマった時のことを書いておこうと思う。

先に書いておくと、もちろん解決策は存在していてChiselは普通に32bitの範囲内はもちろんそれ以上の定数も扱えます。

一応何をどうしたらエラーになって、、、的な部分から書いていきますので、とりあえず解決策のみを見たい方はこの記事の後半部分にジャンプしてください。

何がしたかったのか??

では、状況の説明から。 やろうとしたのは以下のようにChiselのModule内で作ったポートに32bitの大きなデータを入れようとしただけなんだ。 因みにこの話はChiselでオプションポートを扱う楽な方法を模索していた時に、適当に32bitの固定値だし0xdeadbeafだな、と入れてみたら予期せぬエラーに出くわした、という経緯。 なので以下のコードはエラーが起きるケースをシンプルに再現したコード。

class MyModule extends Module {
  val io = IO(new Bundle {
  val out = Output(UInt(32.W))
  })

  io.out := 0xdeadbeaf.asUInt(32.W)
}

説明する必要も無い気もしますが、一応説明。

  • io.outScalaのIntのデータ0xdeadbeafasUIntで32bitのUIntに変換してChiselのDataとして接続する

というだけ。 上記のコードはScalaの文法的には何もおかしな部分は無いので、クラスの定義だけだと普通にビルドが終わる。 以下のメッセージは上記のclassの定義だけをコンパイルした時のもの(== Chiselのエラボレートは動いていない状態)で何事もなくMyModuleクラスが定義されたとjupyter-notebook上にメッセージが出る。

defined class MyModule

でもエラボレートするとエラーが起きるんだ

エラボレートしてVerilogのRTLとか作ろうとするとエラーになるんですよ、このコード。 ということで実際にやってみた際のメッセージが以下。

[info] [0.000] Elaborating design...

java.lang.IllegalArgumentException: requirement failed: UInt literal -559038801 is negative
  scala.Predef$.require(Predef.scala:224)
  chisel3.internal.firrtl.ULit.<init>(IR.scala:79)
  chisel3.core.UIntFactory$class.Lit(Bits.scala:573)
  chisel3.core.UInt$.Lit(Bits.scala:590)
  chisel3.core.package$fromBigIntToLiteral.asUInt(package.scala:48)
  ammonite.$sess.cmd2$Helper$MyModule.<init>(cmd2.sc:6)
  ammonite.$sess.cmd3$Helper$$anonfun$1.apply(cmd3.sc:1)
  ammonite.$sess.cmd3$Helper$$anonfun$1.apply(cmd3.sc:1)
  chisel3.core.Module$.do_apply(Module.scala:49)
  chisel3.Driver$$anonfun$elaborate$1.apply(Driver.scala:93)
  chisel3.Driver$$anonfun$elaborate$1.apply(Driver.scala:93)
  chisel3.internal.Builder$$anonfun$build$1.apply(Builder.scala:297)
  chisel3.internal.Builder$$anonfun$build$1.apply(Builder.scala:295)
  scala.util.DynamicVariable.withValue(DynamicVariable.scala:58)
  chisel3.internal.Builder$.build(Builder.scala:295)
  chisel3.Driver$.elaborate(Driver.scala:93)
  chisel3.Driver$.execute(Driver.scala:140)
  chisel3.Driver$.execute(Driver.scala:202)
  ammonite.$file.dummy.source.load$minusivy$Helper.getVerilog(Main.sc:9)
  ammonite.$sess.cmd3$Helper.<init>(cmd3.sc:1)
  ammonite.$sess.cmd3$.<init>(cmd3.sc:7)
  ammonite.$sess.cmd3$.<clinit>(cmd3.sc:-1)

見てお分かりの通り"UIntの値が負数になってる!!"と怒られます。エラーメッセージの-559038801はお気づきの通り0xdeadbeafを32bitのIntとして解釈した時の数字。 発生する理由はそのままズバリでScalaにはunsignedに相当する型が無いため0xdeadbeafと書くとInt型になるのでMSBが符号ビットとして解釈されて、そのデータがChiselのUIntに入るため”負数を入力してくれるな!!!”とお叱りが入るわけです。

一応これを確認したコードが以下のコード(jupyter-notebook上であれば、以下のようにvalだけ定義してもエラーにはなりません)

  • MSBを除く全ビットが0x1の場合はOK
val a = 0x7fffffff.U // OK
a: UInt = chisel3.core.UInt@0
  • MSBに0x1が立つとNG
val a = 0x80000000.U // OK
java.lang.IllegalArgumentException: requirement failed: UInt literal -2147483648 is negative
  scala.Predef$.require(Predef.scala:224)
  chisel3.internal.firrtl.ULit.<init>(IR.scala:79)
  chisel3.core.UIntFactory$class.Lit(Bits.scala:573)
  chisel3.core.UInt$.Lit(Bits.scala:590)
  chisel3.core.package$fromBigIntToLiteral.U(package.scala:29)
  ammonite.$sess.cmd7$Helper.<init>(cmd7.sc:1)
  ammonite.$sess.cmd7$.<init>(cmd7.sc:7)
  ammonite.$sess.cmd7$.<clinit>(cmd7.sc:-1)

どうずれば解決できるのか

冒頭に書いたとおり解決策はあります。今回みたいな符号なしの32bitのデータはもちろん、64bitやそれ以上の定数もきちんと使えるようになってます。じゃないとSHA1みたいなハッシュの初期値とかをわざわざ符号付きにして扱うとかいう訳わからんことに。今回の件もそのデータを扱う部分をSIntに変えるとエラーは起きません。起きませんが、望んでいるのはもちろんそういうことじゃないです。

ということで解決方法を紹介していきます。なお以下に記載する内容はChiselのgithubのissuesに載っている内容なので、必要に応じて以下もご覧ください。

github.com

解決策1:ScalaLongを使う(64bit未満のデータ限定)

これはScala側での対処でScalaIntでダメならLongにすればいい!!という解決策で以下のようにやると64bitまでのデータならこれで乗り切れます。

val a = 0x80000000L.U // OK:定数値の末尾にLongを示す"L"を入れるだけ。
a: UInt = chisel3.core.UInt@0

ScalaLongは64bitなので、先の32bitの例と同様にMSBに0x1が立つような符号なしデータはエラーが発生します。

  • エラーは起きない
val a = 0x7fffffffffffffffL.U // OK
a: UInt = chisel3.core.UInt@0
  • MSBに0x1が立つとNG@64bit
val a = 0x8000000000000000L.U // NG
java.lang.IllegalArgumentException: requirement failed: UInt literal -9223372036854775808 is negative
  scala.Predef$.require(Predef.scala:224)
  chisel3.internal.firrtl.ULit.<init>(IR.scala:79)
  chisel3.core.UIntFactory$class.Lit(Bits.scala:573)
  chisel3.core.UInt$.Lit(Bits.scala:590)
  chisel3.core.package$fromBigIntToLiteral.U(package.scala:29)
  ammonite.$sess.cmd5$Helper.<init>(cmd5.sc:1)
  ammonite.$sess.cmd5$.<init>(cmd5.sc:7)
  ammonite.$sess.cmd5$.<clinit>(cmd5.sc:-1)

実は割とこれで乗りきれるケースは多いはず。定数値にLを付けるだけだし。

解決策2:ScalaBigIntに変換してからChiselのUIntに渡す

これもScala側での対処。ScalaBigIntは任意精度の整数になるので、引っかかる定数データを変換してから渡すという方法。 コードは以下のようにBigIntで16進数文字列を基数16で変換してからUIntに渡す。

val b = BigInt("deadbeaf", 16).U // ScalaのBigIntで変換してからUIntに渡す
b: UInt = chisel3.core.UInt@0

因みにBigIntはBigInt(0x10)といった基数を指定した数値を使った変換も出来るが、これを使うとうまくいかなかった。

val b = BigInt(0xdeadbeaf).U // ScalaのBigIntで変換してからUIntに渡す:でもエラー
java.lang.IllegalArgumentException: requirement failed: UInt literal -559038801 is negative
  scala.Predef$.require(Predef.scala:224)
  chisel3.internal.firrtl.ULit.<init>(IR.scala:79)
  chisel3.core.UIntFactory$class.Lit(Bits.scala:573)
  chisel3.core.UInt$.Lit(Bits.scala:590)
  chisel3.core.package$fromBigIntToLiteral.U(package.scala:29)
  ammonite.$sess.cmd14$Helper.<init>(cmd14.sc:1)
  ammonite.$sess.cmd14$.<init>(cmd14.sc:7)
  ammonite.$sess.cmd14$.<clinit>(cmd14.sc:-1)

解決策3:Chiselの型変換でBigIntUIntに変換

こちらは全部Chiselで解決する方法(Chisel側でIntBitIntUIntの変換をしてくれる)。 この方法は基数として指定できる文字が2通り用意されている。

val c = "hdeadbeaf".U            // 16進文字列定数を直接UIntに渡す(hが16進の基数表記)
val d = "xdeadbeaf".U            // 16進文字列定数を直接UIntに渡す(xも16進の基数表記)
c: UInt = chisel3.core.UInt@0
d: UInt = chisel3.core.UInt@0

上記の2つはChiselのソースコード的にはcore/packages.scalaに定義されているfromBigIntToLiteralの中の関数になる。

ということでまとめると以下になる。

val a = 0xdeadbeafL.U            // 64bit未満のデータ限定
val b = BigInt("deadbeaf", 16).U // ScalaのBigIntで変換してからUIntに渡す
val c = "hdeadbeaf".U            // 16進文字列定数を直接UIntに渡す(hが16進の基数表記)
val d = "xdeadbeaf".U            // 16進文字列定数を直接UIntに渡す(xも16進の基数表記)

BigIntに変換に変換する手法なら諸々引っかからずに済む。ただBigIntを呼ぶのは面倒なのでc/dで書くのが良さそう。普段から全部統一するかは悩みどころ(0で初期化とか)