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の構成
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) }
memは非同期を行うためのFIFOのエントリ数だと思われる。ridxは読み出し側(つまりQueueの出し側)、widxは書き込み側(つまりQueueの挿入側)の受け渡し用のメモリのインデックスを示しているものと思われる。
index
、safe
はどのように使われているのかはよく分からない。ちなみに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