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

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

Chiselのutil.Queueの使い方の再確認

スポンサーリンク

今日は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 Queueapplyメソッドだ。
ちなみに前回のQueueの記事を書いた時には中身がわからなくてスルーしていたが、object Queue.applyを使うとQueueはモジュールとしてではなく、モジュール内の論理回路として実体化する。(詳しくは理解できてないが、上記applyの中のTransitNameの処理) enqQueueの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点について確認した。

  1. 入力を変化させて出力がどう変化するか
  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で使えば良い。

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

pipe = true / flow = false

上記で述べたとおり、pipe=trueだとdeq.readyの変化が同じサイクルでenq.readyに伝わる。

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

pipe = false / flow = true

上記で述べたとおり、flow=trueだとvalidの変化が同じサイクルでdeq.validに伝わる。

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

pipe = true / flow = true

こちらもこれまえの組み合わせでわかるとは思うが、どちらのパラメータともtrueにすると、enq.valid/bitsの変化とdeq.readyの変化が同じサイクルで伝わるようになる。
こちらは調停回路を簡単に書こうとするときに使うとかが良さそう。

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

ということでQueueの使い方の再確認でした。
今回試したソースコードは以下に置いてありますので、興味があれば試してみてください。