今日はChiselのutilに入っているQueueについてを再度調べてみたのでその内容をまとめようと思う。
ChiselのQueue
”再度”と書いたとおり、以前にもChiselのQueue
について調べていて、その内容は以下の記事にまとめている。
その時はChisel Bootcampの学習の一環で中身を確認していてそのModuleのサンプルコードを確認しただけだった。
では今回は何を見るのかについてだが、その時には放置していたQueue
の引数について何をどうするとどうなるのか、、という部分を改めて確認したので、それをまとめておく。
Queueの引数
では早速だがQueue
の引数部分を確認しておこう。
- chisel3/util/Decoupled.scala
object Queue { /** Create a queue and supply a DecoupledIO containing the product. */ @chiselName def apply[T <: Data]( enq: ReadyValidIO[T], entries: Int = 2, pipe: Boolean = false, flow: Boolean = false): DecoupledIO[T] = { if (entries == 0) { val deq = Wire(new DecoupledIO(enq.bits)) deq.valid := enq.valid deq.bits := enq.bits enq.ready := deq.ready deq } else { require(entries > 0) val q = Module(new Queue(chiselTypeOf(enq.bits), entries, pipe, flow)) q.io.enq.valid := enq.valid // not using <> so that override is allowed q.io.enq.bits := enq.bits enq.ready := q.io.enq.ready TransitName(q.io.deq, q) } }
上記はQueue
を使う時に使用することもあるobject Queue
のapply
メソッドだ。
ちなみに前回のQueueの記事を書いた時には中身がわからなくてスルーしていたが、object Queue.apply
を使うとQueue
はモジュールとしてではなく、モジュール内の論理回路として実体化する。(詳しくは理解できてないが、上記apply
の中のTransitName
の処理)
enq
はQueue
のI/Fになるデータ、entries
はQueueの段数になるので良いとして、残りのpipe
/flow
について見ていこう
pipe
先ほど見たapply
ではなく、class Queue
の方にはこれら引数の説明が載っていて以下のようになっている。
* @param pipe True if a single entry queue can run at full throughput (like a pipeline). The ''ready'' signals are
* combinationally coupled.
pipe
についてをざっくり訳すと、
ということのようだ。
これらの引数を変更した場合の挙動については後ほどまとめて記載するので続いてflowを。
flow
続いてはflow
だ。
* @param flow True if the inputs can be consumed on the same cycle (the inputs "flow" through the queue immediately)
こっちもざっくりと訳すと、
- Trueにした場合、入力はそのサイクルで消費される(入力がQueueをスルーする)
となる。
実際にテストしてみた
何となくは分かった方もいるとは思うが、テストして比較してみたのでそのテストコードを波形を載せておこうと思う。
テストコード
テストコードは以下のようなものだ。
なおここではQueue
をモジュールとしてインスタンスしたいのでapply
メソッドは使用せずにnew
を使ってインスタンスしている。
やってる事自体は単純でQueue
をインスタンスした後で、以下の2点について確認した。
- 入力を変化させて出力がどう変化するか
deq
側のready
を変化させた際のenq
側のready
がどう変化するか
import scala.util.Random import chisel3._ import chisel3.util._ import chisel3.iotesters._ class QueueTester extends ChiselFlatSpec { def dutName: String = "Queue" behavior of dutName val numOfEntry = 8 val r = new Random(1) it should "PipeOffFlowOff" in { val cfg = "PipeOffFlowOff" iotesters.Driver.execute(Array( s"-tn=$dutName$cfg", s"-td=test_run_dir/$dutName$cfg", "-tgvo=on", "-tbn=verilator" ), () => new Queue(UInt(8.W), numOfEntry, pipe = false, flow = false)) { c => new PeekPokeTester(c) { // 1.対向がデータを受け取れる時(deq.ready == true) poke(c.io.enq.valid, false) poke(c.io.deq.ready, true) for (_ <- 0 until numOfEntry) { val data = r.nextInt(0xff) poke(c.io.enq.valid, true) poke(c.io.enq.bits, data) step(1) } poke(c.io.enq.valid, false) step(5) // 2.対向がデータを受け取れない時(deq.ready == false) poke(c.io.enq.valid, false) poke(c.io.deq.ready, false) for (_ <- 0 until numOfEntry) { val data = r.nextInt(0xff) poke(c.io.enq.valid, true) poke(c.io.enq.bits, data) step(1) } for (_ <- 0 until numOfEntry) { poke(c.io.enq.valid, false) poke(c.io.deq.ready, true) step(1) } step(5) } } } }
new Queue
の実行時の引数をpipe/flow = (true, false)
で変化させて実行した結果の波形を載せておく。
pipe = false / flow = false
false/falseの組み合わせだと、レジスタスライスとかskid bufferと言われる動きになって、enq.valid
の際のデータがQueue
内部のメモリに格納され、次のサイクルでdeq
側にデータが出てくる。
ready
も同様でdeq
側のready
の変化が1cycle遅れてenq
側につわたるようになる
なので、ReadyValidIO
を使ったインターフェースでタイミングを切りたい場合はentries=1/pipe=false/flow=false
で使えば良い。
pipe = true / flow = false
上記で述べたとおり、pipe=trueだとdeq.ready
の変化が同じサイクルでenq.ready
に伝わる。
pipe = false / flow = true
上記で述べたとおり、flow=trueだとvalid
の変化が同じサイクルでdeq.valid
に伝わる。
pipe = true / flow = true
こちらもこれまえの組み合わせでわかるとは思うが、どちらのパラメータともtrue
にすると、enq.valid/bits
の変化とdeq.ready
の変化が同じサイクルで伝わるようになる。
こちらは調停回路を簡単に書こうとするときに使うとかが良さそう。
ということでQueue
の使い方の再確認でした。
今回試したソースコードは以下に置いてありますので、興味があれば試してみてください。