Skip to content

Commit

Permalink
[util] Add a withShadowLayer Queue (#4589)
Browse files Browse the repository at this point in the history
Add methods to `Queue` and `Queue$` that can be used for creating "shadow"
queues.  These are queues that are tracking other, auxiliiary data not in
the original queue and may possibly be instantiated elsewhere in the
design.

This is added in support of a design verification use case of building
"shadow pipelines" that track additional information about the execution
of some complicated pipeline (e.g., an out-of-order datapath) to expose
this information to tests or as part of debugging the pipeline.

Signed-off-by: Schuyler Eldridge <[email protected]>
  • Loading branch information
seldridge authored Jan 7, 2025
1 parent 81896e6 commit 69463d8
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 0 deletions.
49 changes: 49 additions & 0 deletions integration-tests/src/test/scala/chiselTest/QueueSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,46 @@ class QueueFactoryTester(elements: Seq[Int], queueDepth: Int, bitWidth: Int, tap
}
}

/** Test that a Shadow Queue keeps track of shadow identifiers that are fed to
* it. This feeds data into a queue and an identifier (which is `data >> 1`)
* into the shadow queue. It then checks that each data read out has the
* expected identifier.
*/
class ShadowQueueFactoryTester(queueDepth: Int, tap: Int, useSyncReadMem: Boolean) extends BasicTester {
val enq, deq = Wire(Decoupled(UInt(32.W)))

private val (dataCounter, _) = Counter(0 to 31 by 2, enable = enq.fire)

enq.valid :<= true.B
deq.ready :<= LFSR(16)(tap)

enq.bits :<= dataCounter

private val idIn = Wire(probe.Probe(UInt(4.W), layers.Verification))
private val idOut = Wire(probe.Probe(Valid(UInt(4.W)), layers.Verification))
layer.block(layers.Verification) {
probe.define(idIn, probe.ProbeValue(dataCounter >> 1))

when(deq.fire) {
assert(deq.bits >> 1 === probe.read(idOut).bits)
}
}

private val (_, done) = Counter(0 to 8, enable = deq.fire)
when(done) {
stop()
}

private val (queue, shadow) =
Queue.withShadow(
enq = enq,
entries = queueDepth,
useSyncReadMem = useSyncReadMem
)
deq :<>= queue
probe.define(idOut, shadow(probe.read(idIn), layers.Verification))
}

class QueueSpec extends ChiselPropSpec {

property("Queue should have things pass through") {
Expand Down Expand Up @@ -303,4 +343,13 @@ class QueueSpec extends ChiselPropSpec {
chirrtl should include("inst foo_q of Queue")
chirrtl should include("inst bar_q of Queue")
}

property("A shadow queue should track an identifier") {
forAll(vecSizes, Gen.choose(0, 15), Gen.oneOf(true, false)) { (depth, tap, isSync) =>
info(s"depth: $depth, tap: $tap, isSync: $isSync")
assertTesterPasses {
new ShadowQueueFactoryTester(depth, tap, isSync)
}
}
}
}
112 changes: 112 additions & 0 deletions src/main/scala/chisel3/util/Queue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package chisel3.util

import chisel3._
import chisel3.experimental.requireIsChiselType
import chisel3.layer.{block, Layer}
import chisel3.probe.{define, Probe, ProbeValue}
import chisel3.util.experimental.BoringUtils

/** An I/O Bundle for Queues
* @param gen The type of data to queue
Expand Down Expand Up @@ -139,6 +142,27 @@ class Queue[T <: Data](
* generator's `typeName`
*/
override def desiredName = s"Queue${entries}_${gen.typeName}"

/** Create a "shadow" `Queue` in a specific layer that will be queued and
* dequeued in lockstep with an original `Queue`. Connections are made using
* `BoringUtils.tapAndRead` which allows this method to be called anywhere in
* the hierarchy.
*
* An intended use case of this is as a building block of a "shadow" design
* verification datapath which augments an existing design datapath with
* additional information. E.g., a shadow datapath that tracks transations
* in an interconnect.
*
* @param data a hardware data that should be enqueued together with the
* original `Queue`'s data
* @param layer the `Layer` in which this queue should be created
* @return a layer-colored `Valid` interface of probe type
*/
def shadow[A <: Data](data: A, layer: Layer): Valid[A] =
withClockAndReset(BoringUtils.tapAndRead(clock), BoringUtils.tapAndRead(reset)) {
val shadow = new Queue.ShadowFactory(enq = io.enq, deq = io.deq, entries, pipe, flow, useSyncReadMem, io.flush)
shadow(data, layer)
}
}

/** Factory for a generic hardware queue. */
Expand Down Expand Up @@ -185,6 +209,94 @@ object Queue {
}
}

/** A factory for creating shadow queues. This is created using the
* `withShadow` method.
*/
class ShadowFactory private[Queue] (
enq: ReadyValidIO[Data],
deq: ReadyValidIO[Data],
entries: Int,
pipe: Boolean,
flow: Boolean,
useSyncReadMem: Boolean,
flush: Option[Bool]) {

/** The clock used when building the original Queue. */
private val clock = Module.clock

/** The reset used when elaborating the original Queue. */
private val reset = Module.reset

/** Create a "shadow" `Queue` in a specific layer that will be queued and
* dequeued in lockstep with an original `Queue`. Connections are made
* using `BoringUtils.tapAndRead` which allows this method to be called
* anywhere in the hierarchy.
*
* An intended use case of this is as a building block of a "shadow" design
* verification datapath which augments an existing design datapath with
* additional information. E.g., a shadow datapath that tracks transations
* in an interconnect.
*
* @param data a hardware data that should be enqueued together with the
* original `Queue`'s data
* @param layer the `Layer` in which this queue should be created
* @return a layer-colored `Valid` interface of probe type
*/
def apply[A <: Data](data: A, layer: Layer): Valid[A] =
withClockAndReset(BoringUtils.tapAndRead(clock), BoringUtils.tapAndRead(reset)) {
val shadowDeq = Wire(Probe(Valid(chiselTypeOf(data)), layer))

block(layer) {
val shadowEnq = Wire(Decoupled(chiselTypeOf(data)))
val probeEnq = BoringUtils.tapAndRead(enq)
shadowEnq.valid :<= probeEnq.valid
shadowEnq.bits :<= data

val shadowQueue = Queue(shadowEnq, entries, pipe, flow, useSyncReadMem, flush.map(BoringUtils.tapAndRead))

val _shadowDeq = Wire(Valid(chiselTypeOf(data)))
_shadowDeq.valid :<= shadowQueue.valid
_shadowDeq.bits :<= shadowQueue.bits
shadowQueue.ready :<= BoringUtils.tapAndRead(deq).ready
define(shadowDeq, ProbeValue(_shadowDeq))
}

shadowDeq
}
}

/** Create a [[Queue]] and supply a [[DecoupledIO]] containing the product.
* This additionally returns a [[ShadowFactory]] which can be used to build
* shadow datapaths that work in lockstep with this [[Queue]].
*
* @param enq input (enqueue) interface to the queue, also determines type of
* queue elements.
* @param entries depth (number of elements) of the queue
* @param pipe True if a single entry queue can run at full throughput (like
* a pipeline). The `ready` signals are combinationally coupled.
* @param flow True if the inputs can be consumed on the same cycle (the
* inputs "flow" through the queue immediately). The `valid`
* signals are coupled.
* @param useSyncReadMem True uses SyncReadMem instead of Mem as an internal
* memory element.
* @param flush Optional [[Bool]] signal, if defined, the [[Queue.hasFlush]]
* will be true, and connect correspond signal to [[Queue]]
* instance.
* @return output (dequeue) interface from the queue and a [[ShadowFactory]]
* for creating shadow [[Queue]]s
*/
def withShadow[T <: Data](
enq: ReadyValidIO[T],
entries: Int = 2,
pipe: Boolean = false,
flow: Boolean = false,
useSyncReadMem: Boolean = false,
flush: Option[Bool] = None
): (DecoupledIO[T], ShadowFactory) = {
val deq = apply(enq, entries, pipe, flow, useSyncReadMem, flush)
(deq, new ShadowFactory(enq, deq, entries, pipe, flow, useSyncReadMem, flush))
}

/** Create a queue and supply a [[IrrevocableIO]] containing the product.
* Casting from [[DecoupledIO]] is safe here because we know the [[Queue]] has
* Irrevocable semantics.
Expand Down

0 comments on commit 69463d8

Please sign in to comment.