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

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

chisel3.utilに入ってるアービターの再調査(Arbiter/RRArbiter/LockingArbiter/LockingRRArbiter)

スポンサーリンク

今日はChiselのutilに入っているArbiterについてを再度調べてみたのでその内容をまとめようと思う。

ChiselのArbiter

先日のQueueと同様にArbiterについてはChisel-Bootcampの勉強の一端として調べていて、その内容は以下の記事にまとめている。

今回はその時には触れていなかったutilに入っているArbiterの挙動の調査と以前に書いたコードを少し修正したものを紹介しようと思う

utilに入っているArbiterについて

以前に紹介したArbiterは以下の2つだった。

  • Arbiter : 入力ポートのインデックスが小さいポートが優先されるアービター
  • RRArbiter : ポートの調停にラウンド・ロビンを使用するアービター

ご存知の方もいるとは思うがChiselのutilの下には後2つArbiterが用意されている。それが以下の2つだ。

  • LockingArbiter
  • LockingRRArbiter

名前から受ける印象は最初に上げた2つのロック付きのバージョン言ったところだ。
では早速適当なコードを準備してどんな処理が行われるのかを葉系で確認してみる。

テスト対象のアービター・モジュール

以前にChisel-Bootcampの勉強の一環で記事を書いた時に以下のようにパラメータでArbiter/RRArbiterのどちらを使用するかを切り替えられるモジュールを実装した。
その時のコードが以下のものだ。
とりあえずif式で切り替えました!!というだけのもの。。

class MyArbiter(InputNum: Int, DataWidth: Int, Type: Int = 0) extends Module {
  val io = IO(new Bundle {
    val in = Flipped(Vec(InputNum, Decoupled(UInt(DataWidth.W))))
    val out = Decoupled(UInt(DataWidth.W))
  })

  var arb = Module
  if (Type == 1) {
    val arb = Module(new Arbiter(UInt(DataWidth.W), InputNum))
    arb.io.in <> io.in
    io.out <> arb.io.out
  }
  else {
    val arb = Module(new RRArbiter(UInt(DataWidth.W), InputNum))
    arb.io.in <> io.in
    io.out <> arb.io.out
  }
}

今回はこれを少し修正&追加実装を行い、全4種のアービターをパラメータで切り替え可能にして確認を行う。
その修正したコードを以下に示す。
少しはマシになったかな。。

sealed trait TypeOfArbiter
case object Normal extends TypeOfArbiter
case object RRA extends TypeOfArbiter
case object Locking extends TypeOfArbiter
case object LockingRRA extends TypeOfArbiter

class ArbiterTrial(numOfInPort: Int, arbType: TypeOfArbiter, count: Int=2 ) extends Module {
  val io = IO(new ArbiterIO(UInt(8.W), numOfInPort))

  val arb = Module(arbType match {
    case Normal => new Arbiter(UInt(8.W), numOfInPort)
    case RRA => new RRArbiter(UInt(8.W), numOfInPort)
    case Locking => new LockingArbiter(UInt(8.W), numOfInPort, count)
    case LockingRRA => new LockingRRArbiter(UInt(8.W), numOfInPort, count)
  })

  arb.io <> io
}

変更点はざっと以下

chisel3.utilに入っているアービターは全種類IOが統一されており、それがArbiterIOというクラスになっている。
実装は以下のようなものでn個のDecoupledIOの入力と1個のDecoupledIOの出力、そしてどのポートが選択されたかを示すchosenとなっている。
このようにin/outDecoupledIOになっているので、前回扱った時のようにIO部分をDecoupledIOで宣言して接続することができていた。
今回はArbiterIOを直接上に出したことにより最後の<>で接続を完了することができている。

  • chisel3.util.Arbiter.scala
class ArbiterIO[T <: Data](private val gen: T, val n: Int) extends Bundle {
  // See github.com/freechipsproject/chisel3/issues/765 for why gen is a private val and proposed replacement APIs.

  val in  = Flipped(Vec(n, Decoupled(gen)))
  val out = Decoupled(gen)
  val chosen = Output(UInt(log2Ceil(n).W))
}

動作を確認する

上記のArbiterTrialモジュール内部でインスタンスされるアービターによって、出力の挙動がどう変わるかを確認するため、以下のようなテストコードを作成しarbTypeの設定を変更してインスタンスしたモジュールに同一の入力を行って、波形がどう変化するかを実際に確認してみた。

object Test extends App {

  val arbMap = Seq(Normal, RRA, Locking, LockingRRA).map(t => (t.toString, t))
  val numOfInPort = 4

  for (((name, arbType), idx) <- arbMap.zipWithIndex) {
    chisel3.iotesters.Driver.execute(Array(
      f"-tn=Arbiter$name",
      f"-td=test_run_dir/$idx%03d_Arbiter$name",
      s"-tgvo=on",
      //"-tbn=verilator"
      "-tbn=treadle"
    ),
      () => new ArbiterTrial(numOfInPort, arbType, 3)) {
      c => new PeekPokeTester(c) {

        val r = new Random(1)

        poke(c.io.out.ready, true)
        for (in <- c.io.in) {
          poke(in.valid, false)
        }
        step(1)

        for (_ <- 0 until 10) {
          val inData = Seq.fill(numOfInPort)(r.nextInt(0xff))
          for ((in, data) <- c.io.in.zip(inData)) {
            poke(in.valid, true)
            poke(in.bits, data)
          }

          for (_ <- 0 until numOfInPort) {
            for ((data, idx) <- inData.zipWithIndex) {
              println(f"in($idx).bits = 0x$data%02x")
            }
            println(f"out.bits   = 0x${peek(c.io.out.bits)}%02x")
            step(1)
          }
        }

        poke(c.io.out.ready, true)
        for (in <- c.io.in) {
          poke(in.valid, false)
        }

        step(5)
      }
    }
  }
}

ご覧いただくとわかるかと思うが、上記テストコードでは全入力ポートのvalid同時にHighにして競合状態の挙動のみを観測している。

Arbiterの波形

Arbiterは冒頭に記載したとおり、入力ポートのうちポート番号の小さいポートが優先されるため、今回のテスト入力では常にポート0の入力がoutに出力される。

f:id:diningyo-kpuku-jougeki:20190709230510p:plain

RRArbiterの波形

こちらはラウンド・ロビンで処理されるので、出力がポート1→ポート2→ポート0 ... となっているのが確認できた。

f:id:diningyo-kpuku-jougeki:20190709230520p:plain

LockingArbiterの波形

イマイチ挙動がわからないのが、このLockingArbiterだ。冒頭に書いたとおりロック付きのArbiterになるのだが、今回のテスト入力に対してはArbiterとの違いが出てこなかった。
ChiselのソースではArbiterと比較すると違いはある(LockingRRArbiterと同様にlockCountが存在しており、ロックをかけている様子)
あんまり興味が湧かなくてRTLの比較してなかった。。
確認したら追記するかも。

f:id:diningyo-kpuku-jougeki:20190709230532p:plain

LockingRRArbiterの波形

こちらはロック付き、、で違いは??というと、モジュールのインスタンス時に指定できる第3引数(count)の値の回数分サービスを行った後、次のポートに処理権が移動する、、ということのようだ。

f:id:diningyo-kpuku-jougeki:20190709230543p:plain

ということでChiselのアービターについてのまとめ再びでした。