FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Chiselで非同期クロック設計をしよう

Chisel3.0からは異種クロックのサポート、つまり複数のクロックを使ったデザインがサポートされるようになった。

Chisel3でクロックを2つ用意するためには、CrossingIOを使用する。

class AsyncQueue[T <: Data](gen: T, params: AsyncQueueParams = AsyncQueueParams()) extends Crossing[T] {
  val io = IO(new CrossingIO(gen))

CrossingIOのBundleには、以下が含まれている。2つのクロック、2つのリセット、そしてデータを示すenq, deqだ。

  • asyncqueue/src/main/scala/Crossinrg.scala
class CrossingIO[T <: Data](gen: T) extends Bundle {
  // Enqueue clock domain
  val enq_clock = Clock(INPUT)
  val enq_reset = Bool(INPUT) // synchronously deasserted wrt. enq_clock
  val enq = Decoupled(gen).flip
  // Dequeue clock domain
  val deq_clock = Clock(INPUT)
  val deq_reset = Bool(INPUT) // synchronously deasserted wrt. deq_clock
  val deq = Decoupled(gen)
}

例えばAsyncQueueでは、以下のようにSourceにenq_clock, enq_reset, Sinkの方にdeq_clock, deq_resetを使用する。

  source.clock := io.enq_clock
  source.reset := io.enq_reset
  sink.clock := io.deq_clock
  sink.reset := io.deq_reset

AsyncQueueの構成

f:id:msyksphinz:20190121233846p:plain
ChiselでのAsyncQueueの構成

AsyncQueueは、大きく分けてsourceとsinkの2種類のモジュールで構成されている。

sourceは入力側、ASyncQueueSourceモジュールで構成されている。sinkは出し側、AsyncQueueSinkモジュールで構成されている。

sourceとsinkはasyncという信号で接続されている。このBundleは以下の信号で構成されている。

class AsyncBundle[T <: Data](private val gen: T, val params: AsyncQueueParams = AsyncQueueParams()) extends Bundle {
  // Data-path synchronization
  val mem   = Output(Vec(params.wires, gen))
  val ridx  = Input (UInt((params.bits+1).W))
  val widx  = Output(UInt((params.bits+1).W))
  val index = params.narrow.option(Input(UInt(params.bits.W)))

  // Signals used to self-stabilize a safe AsyncQueue
  val safe = params.safe.option(new AsyncBundleSafety)
}
f:id:msyksphinz:20190120235419p:plain
AsyncBundleの構成

memは非同期を行うためのFIFOのエントリ数だと思われる。ridxは読み出し側(つまりQueueの出し側)、widxは書き込み側(つまりQueueの挿入側)の受け渡し用のメモリのインデックスを示しているものと思われる。 indexsafeはどのように使われているのかはよく分からない。ちなみにVerilogを生成してみるとasync_indexは生成されていなかった。オプションだろうと思われる。

  output [31:0] io_async_mem_0, // @[:@921.4]
  output [31:0] io_async_mem_1, // @[:@921.4]
  output [31:0] io_async_mem_2, // @[:@921.4]
  output [31:0] io_async_mem_3, // @[:@921.4]
  output [31:0] io_async_mem_4, // @[:@921.4]
  output [31:0] io_async_mem_5, // @[:@921.4]
  output [31:0] io_async_mem_6, // @[:@921.4]
  output [31:0] io_async_mem_7, // @[:@921.4]
  input  [3:0]  io_async_ridx, // @[:@921.4]
  output [3:0]  io_async_widx, // @[:@921.4]

AsyncQueueSourceの内部は、まずはSourceとSinkを接続するためのメモリがインスタンスされている。

  val mem = Reg(Vec(params.depth, gen)) // This does NOT need to be reset at all.

このメモリに書き込むためのインデックスとして、Write側はwidx, Read側はridxが用意されている。 widxはGrayCodeカウンタ、ridxもSink側から接続されるGraycodeカウンタだろうが、Synchronizerを経由して、Sink側のクロックからSource側のクロックに置き換えているものと思われる。

メモリへの書き込み側のインデックスは以下のように生成する。

  val index = if (bits == 0) 0.U else io.async.widx(bits-1, 0) ^ (io.async.widx(bits, bits) << (bits-1))
  val widx_reg = AsyncResetReg(widx, "widx_gray")
  io.async.widx := widx_reg

FIFOにデータが埋まっているか(つまり入力側に対するReady信号)は以下のようにして生成する。 グレイコードが一致するだけではFIFOのFullが判定できないので、以下のような実装となっている。

  val ready = sink_ready && widx =/= (ridx ^ (params.depth | params.depth >> 1).U)

参考文献として以下を見つけた。 ここで、ライト側でFIFOがフル(full)である条件:wadr[WA:0]=={~radr[WA],radr[WA-1:0]}という記述とやりたいことは一緒だと思う。

メモリからのデータ読み出しは、io.asyncバンドルを通じてデータをすべてSink側に見せ、データを取得する。

  io.async.index match {
    case Some(index) => io.async.mem(0) := mem(index)
    case None => io.async.mem := mem
  }

読み出し側では、FIFO内にデータが入っているかどうかを以下の論理で生成する。 ridxはSink側のクロックを使ってGrayCounterを生成し、widxはSource側のクロックで動いているので、AsyncResetSynchronizerShiftRegを使ってクロックを入れ替えている。

  val ridx = GrayCounter(bits+1, io.deq.fire(), !source_ready, "ridx_bin")
  val widx = AsyncResetSynchronizerShiftReg(io.async.widx, params.sync, Some("widx_gray"))
  val valid = source_ready && ridx =/= widx