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

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

ChiselのArbiterのvalid/readyの調停テストコードが上手く作れなかった話

スポンサーリンク

今日の記事はChiselでテストを書いていた時に出くわしたトラブルとそれに対する修正について。回路自体は前回まで記事にしていたNICのArbiter部分のような調停回路。

トラブルと言ってもChiselのバグとかそういう話ではなく、自分の理解不足からくるもので、一言で書くと以下になる。

  • テスト対象のモジュールに与えた入力が意図したとおりに反映されない

Arbiterに対するテストコードの調査

RRArbiterを使って以下のようなテストコードを動かすとvalidの制御がうまく出来ない。 やりたい事としては

  • 全ポートのvalidをアサートして、validに対するreadyが来たらvalidを落とす

というもので、それに対して以下のようなコードを準備して実行してみたところうまく意図通りに動かなかった。

/**
  * Arbiterのユニットテストクラス
  * @param c RRArbiter
  */
class ArbiterUnitTester(c: RRArbiter[UInt]) extends PeekPokeTester(c) {

  val in = c.io.in
  val out = c.io.out

  /**
    * バスリクエスト発行
    * @param portIdx リクエストを発行するポート番号
    */
  def req(portIdx: Int): Unit = {
    poke(c.io.in(portIdx).valid, true)
    poke(c.io.in(portIdx).bits, portIdx)
  }
}

/**
  * RRArbiterのテストクラス
  */
class ArbiterTester extends ChiselFlatSpec {
  behavior of "RRArbiter"

  val numOfInput = 4

  it should "以下のテストを実行するとin(2)/in(3)のvalidが同時にLOWに落ちる" in {
    iotesters.Driver.execute(
      Array(
        "-tn=RRAbiter",
        "-td=test_run_dir/RRArbiter-error",
        "-tgvo=on"
      ),
      () => new RRArbiter(UInt(8.W), numOfInput)) {
      c => new ArbiterUnitTester(c) {

        poke(out.ready, true)

        // すべてのポートで同時にリクエスト発行
        for (portIdx <- 0 until numOfInput) {
          req(portIdx)
        }
        step(1)

        for (idx <- 0 until numOfInput) {
          expect(out.valid, true)

          // 入力側のvalidの制御
          // → "in(portIdx).readyがHighならvalidをLOWにする"を
          //   書いたつもりだがうまく動かない。
          for (portIdx <- 0 until numOfInput) {
            if (peek(in(portIdx).ready) == 0x1) {
              poke(in(portIdx).valid, false)
            }
          }
          step(1)
        }
        expect(out.valid, false)
      }
    } should be (true)
  }
}

コード後半の2重のforループの内側ループのコメント部分に書いたとおりで、このコードは思ったとおりに動かず、1cycleの間に複数のポートのvalidがまとめて落ちるという結果になる。

  • 上記テスト実行時の波形

f:id:diningyo-kpuku-jougeki:20190728121944p:plain
うまく行かなかったテストの実行時の動作波形

波形上で見るとマーカーのあるサイクルで、io.in(3).validがin(2)と同じサイクルにLOWに遷移している。readyが半周期だけアサートされておりその値をpeekで拾ってしまっているっぽいのか??と思い、一応ログにも出してみた

  • テスト時のvalid/readyのログ
[info] [0.002] SEED 1564282748218
[info] [0.005] [1] chosen port = 2
valid_0 / ready_0 = 1, 0
valid_1 / ready_1 = 1, 0
valid_2 / ready_2 = 1, 1
valid_3 / ready_3 = 1, 0
[info] [0.007] [2] chosen port = 1
valid_0 / ready_0 = 1, 0
valid_1 / ready_1 = 1, 1
valid_2 / ready_2 = 0, 0
valid_3 / ready_3 = 0, 0 <- ここでvalid_3が落ちる
[info] [0.009] [3] chosen port = 0
valid_0 / ready_0 = 1, 1
valid_1 / ready_1 = 0, 1
valid_2 / ready_2 = 0, 1
valid_3 / ready_3 = 0, 1
[info] [0.011] [4] chosen port = 3
valid_0 / ready_0 = 0, 1
valid_1 / ready_1 = 0, 1
valid_2 / ready_2 = 0, 1
valid_3 / ready_3 = 0, 1

結局のところ原因は??

色々試しながら右往左往した結果、原因はものすごく単純なことだった。

以下のようにループ中にpeekで値を取得しながらpokeで端子の評価を行うと、そのタイミングでpokeの処理が反映されるのが理由。
要はverilogブロッキング代入か。この辺は全然意識できてなかったな。。

for (portIdx <- 0 until numOfInput) {
  if (peek(in(portIdx).ready) == 0x1) {
    poke(in(portIdx).valid, false)
  }
}

解決策

ということで、原因さえきちんと分かれば対応は簡単。
値を取得してから、端子の制御を行うようにすればいい。この修正をしたのが以下のコード

  it should "in(N).readyを取得してからvalidを制御するとうまくいく" in {
    iotesters.Driver.execute(
      Array(
        "-tn=RRAbiter",
        "-td=test_run_dir/RRArbiter-ok",
        "-tgvo=on"
      ),
      () => new RRArbiter(UInt(8.W), numOfInput)) {
      c => new ArbiterUnitTester(c) {

        poke(out.ready, true)

        // すべてのポートで同時にリクエスト発行
        for (portIdx <- 0 until numOfInput) {
          req(portIdx)
        }
        step(1)

        for (idx <- 0 until numOfInput) {
          expect(out.valid, true)

          // pokeの発行前にio(N).readyの情報を取得
          val in_readies = in.map(p => peek(p.ready))

          // 取得したreadyの情報を元にvalidを制御
          for ((ready, in_port) <- in_readies zip in) {
            if (ready == 0x1) {
              poke(in_port.valid, false)
            }
          }
          step(1)
        }
        expect(out.valid, false)
      }
    } should be (true)
  }
  • 修正後のテスト実行時の波形

f:id:diningyo-kpuku-jougeki:20190728122021p:plain
修正したテストの実行時の動作波形

  • 修正したテストのvalid/readyのログ
[info] [0.000] Elaborating design...
[info] [0.015] Done elaborating.
Total FIRRTL Compile Time: 47.6 ms
Total FIRRTL Compile Time: 49.6 ms
file loaded in 0.060379056 seconds, 52 symbols, 40 statements
[info] [0.000] SEED 1564283435690
[info] [0.002] [1] chosen port = 2
valid_0 / ready_0 = 1, 0
valid_1 / ready_1 = 1, 0
valid_2 / ready_2 = 1, 1
valid_3 / ready_3 = 1, 0
[info] [0.004] [2] chosen port = 0
valid_0 / ready_0 = 1, 1
valid_1 / ready_1 = 1, 0
valid_2 / ready_2 = 0, 0
valid_3 / ready_3 = 1, 1 <- ちゃんとvalid_3がHighでキープされた!
[info] [0.005] [3] chosen port = 3
valid_0 / ready_0 = 0, 0
valid_1 / ready_1 = 1, 0
valid_2 / ready_2 = 0, 1
valid_3 / ready_3 = 1, 0
[info] [0.006] [4] chosen port = 1
valid_0 / ready_0 = 0, 1
valid_1 / ready_1 = 1, 1
valid_2 / ready_2 = 0, 1
valid_3 / ready_3 = 0, 1

といこうことで、気づけば当たり前じゃん、、、、という感じの話でしたが、途中に書いたとおり、意識しておいたほうがいいかな、と思ったネタでした。