今日はChiselで実装したモジュールをテストしていて出くわした、わかりにくかったエラーについて解析した際のメモを。
VecをUIntにしたらテストがFAILするようになった。
今回のエラーは以下のような感じのテストを実施した際に発生した。やろうとしていたのはリセット解除後の初期値確認。
Driver.execute(args, () => new SimModTestUnit(limit, p)) { c => new SimModUnitUnitTester(c) { reset() expect(out(0), false) // ここでエラーが発生 expect(out(1), false) step(1) } } should be (true)
上記エラーに対するメッセージが以下↓
[info] chisel3.internal.ChiselException: Error: Not in a UserModule. Likely cause: Missed Module() wrap, bare chisel API call, or attempting to construct hardware inside a BlackBox.
ざっくり訳すと、
ユーザーモジュールの中ではない。起こりそうな原因は
Module()
によるラップを忘れていて、裸のChisel APIがコールされたとか、BlackBox
モジュールの中でハードウェアを構築しようとした場合だ。
いやいや、思いっきりModule
の中だし、、、と思いつつ、モジュールの方を修正する前には普通にPASSしてたテストなんだよなぁ、、、ということで自分の行った修正を振り返ってみて該当するのは以下の修正だった。
- バルク接続
<>
を使って接続しようとした時にVec
の長さが違うためエラーになったので、その部分をUInt
に変更した。
というものだった。 コードで問題になった部分のみを再現したコードは以下のようなものだった
- 修正前(&エラーはなかった)
// 修正前:この状態ではエラーはなかった class BeforeErrorMod extends Module { val io = IO(new Bundle { val out = Output(Vec(2, Bool())) }) io.out(0) := true.B io.out(1) := false.B } object TestElaborateBeforeErrorMod extends App { // Driver.execute(args, () => new BeforeErrorMod()) { c => new PeekPokeTester(c) { expect(c.io.out(0), true) expect(c.io.out(1), true) } } }
- 修正後(&エラー発生)
// 修正後:これにしたらエラーが出た class RegenerateErrorMod extends Module { val io = IO(new Bundle { val out = Output(UInt(2.W)) }) val out = Wire(Vec(2, Bool())) out(0) := true.B out(1) := false.B io.out := out.asUInt() // Vec → UIntに変換して出力 } object TestElaborateRegenerateErrorModFail extends App { // Driver.execute(args, () => new RegenerateErrorMod()) { c => new PeekPokeTester(c) { expect(c.io.out(0), true) expect(c.io.out(1), true) } } }
試してわかったこと
内部構造を理解するのにも役に立ちそうなので、幾つか試したりコードを追ってみたりしてみた。
まず試したのは以下のようなテスト
iotesters.Driver.execute(args, () => new RegenerateErrorMod()) { c => new PeekPokeTester(c) { val a = c.io.out expect(1.U, true) expect(c.io.out, true) } }
上記のコードは普通に動く。
無意識で使ってたけど、val a = Wire(UInt(1.W))
も実質プレースホルダー作ってることになるので、val a = c.io.out
をしたとしても、中身はc.io.out
になる。
要はc.io.out
のエイリアスになる感じ。
上記の場合だとOutput
でラップされたUInt
になっていて、ハードウェアの信号を保持するようになっている。
”無意識で使ってた”というのは以下のようなコードを書いて使っていたのだが、これはこの挙動を期待していることになる。
val a = Wire(Bool()) val b = Wire(Bool()) val signals = Seq(a, b) for (signal <- signals) { signal := true.B }
エラーが起きる起きないの境目はUInt
等のElement
型の派生クラスのメソッドをコールするかどうかで決まってくるようでビットセレクトの場合はBits
のapply
が呼ばれることになり、その中で以下のようにBuilder
クラスのコードが呼び出され、その中で呼出された場所のチェックが行われている
final def do_apply(x: BigInt)(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): Bool = { if (x < 0) { Builder.error(s"Negative bit indices are illegal (got $x)") } if (isLit()) { (((litValue() >> x.toInt) & 1) == 1).asBool } else { requireIsHardware(this, "bits to be indexed") pushOp(DefPrim(sourceInfo, Bool(), BitsExtractOp, this.ref, ILit(x), ILit(x))) } }
上記のpushOp
↓
def pushOp[T <: Data](cmd: DefPrim[T]): T = { // Bind each element of the returned Data to being a Op cmd.id.bind(OpBinding(forcedUserModule)) pushCommand(cmd).id }
上記のforcedUserModule
↓
def forcedUserModule: UserModule = currentModule match { case Some(module: UserModule) => module case _ => throwException( "Error: Not in a UserModule. Likely cause: Missed Module() wrap, bare chisel API call, or attempting to construct hardware inside a BlackBox." // A bare api call is, e.g. calling Wire() from the scala console). ) }
この中でmatch
式によるcurrentModule
の確認が行われており、今回のエラーはここのcase _
に入ったときの例外。
このときのcurrentModule
は以下になる。
var currentModule: Option[BaseModule] = None def currentModule: Option[BaseModule] = dynamicContext.currentModule
此処から先はまだ追っていないが、多分エラボレートの最中にこのvar currentModule
が更新されていく仕組みになっているはず。
今回の場合はPeekPokeTester
の中で呼び出したためNone
になってて、例外が発生する。
val a = c.io.out >> 0x1.U
こういうので1bitだけ取ろうとしても、結局対応する演算がコールされるためエラーになる。(上記の場合はdo_>>
↓)
override def do_>> (that: UInt)(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): UInt = binop(sourceInfo, UInt(this.width), DynamicShiftRightOp, that)
なので出来るのは、ポートの信号をそのまま参照する動きだけ。例えば以下はOKとなる。
println(s"${peek(c.io.out)}")
エラーを修正するには?
前項の挙動を踏まえると結論としては
- ポートの値を
peek
で取得して、加工することでエラーを回避できる
となる。
このようにすることで、ScalaのInt
として値を操作できるので、上記のようなChiselのデータ型依存のメソッドをコールせずに済む。
それを踏まえて修正したのが以下のテスト。テスト対象は先程エラーになったRegenerateErrorMod
を使っている。
object TestElaborateRegenerateErrorModOK extends App { // iotesters.Driver.execute(args, () => new RegenerateErrorMod()) { c => new PeekPokeTester(c) { def ownBit(idx: Int): BigInt = (peek(c.io.out) >> idx) & 0x1 expect(ownBit(0).U, true) expect(ownBit(1).U, false) } } }
シミュレーション結果は以下のようにエラーにならずに正常に終了するようになった。
[info] [0.002] Elaborating design... [info] [1.590] Done elaborating. Total FIRRTL Compile Time: 976.1 ms file loaded in 0.123842947 seconds, 7 symbols, 3 statements [info] [0.001] SEED 1568291532501 test cmd2HelperRegenerateErrorMod Success: 0 tests passed in 5 cycles in 0.075231 seconds 66.46 Hz [info] [0.007] RAN 0 CYCLES PASSED
ということでChiselでよく分からなかったエラーを追ってみた話でした。