今日はChiselのutilに入っているArbiterについてを再度調べてみたのでその内容をまとめようと思う。
ChiselのArbiter
先日のQueue
と同様にArbiter
についてはChisel-Bootcampの勉強の一端として調べていて、その内容は以下の記事にまとめている。
今回はその時には触れていなかったutilに入っているArbiterの挙動の調査と以前に書いたコードを少し修正したものを紹介しようと思う
utilに入っているArbiterについて
以前に紹介したArbiterは以下の2つだった。
ご存知の方もいるとは思うが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
/out
がDecoupledIO
になっているので、前回扱った時のように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
に出力される。
RRArbiterの波形
こちらはラウンド・ロビンで処理されるので、出力がポート1→ポート2→ポート0 ... となっているのが確認できた。
LockingArbiterの波形
イマイチ挙動がわからないのが、このLockingArbiter
だ。冒頭に書いたとおりロック付きのArbiter
になるのだが、今回のテスト入力に対してはArbiter
との違いが出てこなかった。
ChiselのソースではArbiter
と比較すると違いはある(LockingRRArbiter
と同様にlockCount
が存在しており、ロックをかけている様子)
あんまり興味が湧かなくてRTLの比較してなかった。。
確認したら追記するかも。
LockingRRArbiterの波形
こちらはロック付き、、で違いは??というと、モジュールのインスタンス時に指定できる第3引数(count
)の値の回数分サービスを行った後、次のポートに処理権が移動する、、ということのようだ。
ということでChiselのアービターについてのまとめ再びでした。