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

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

Chiselのモジュールのテスト時に出くわした分かりにくかったエラー

スポンサーリンク

今日は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型の派生クラスのメソッドをコールするかどうかで決まってくるようでビットセレクトの場合はBitsapplyが呼ばれることになり、その中で以下のように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で取得して、加工することでエラーを回避できる

となる。

このようにすることで、ScalaIntとして値を操作できるので、上記のような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でよく分からなかったエラーを追ってみた話でした。