Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[util] Add a withShadowLayer Queue #4589

Merged
merged 13 commits into from
Jan 7, 2025
4 changes: 2 additions & 2 deletions core/src/main/scala-3/chisel3/probe/Probe.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ object Probe extends ProbeBase {
def apply[T <: Data](source: => T)(using sourceInfo: SourceInfo): T =
super.apply(source, false, None)

def apply[T <: Data](source: => T, color: Option[layer.Layer])(using sourceInfo: SourceInfo): T =
super.apply(source, false, color)
def apply[T <: Data](source: => T, color: layer.Layer)(using sourceInfo: SourceInfo): T =
super.apply(source, false, Some(color))
}

object RWProbe extends ProbeBase with SourceInfoDoc {
Expand Down
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](
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this have two data types? Shadow is probably not the same type as T...?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, no, due to how ShadowFactory works. A ShadowFactory takes a type parameter that is the same as the queue it was created from. However, it has a method that has a different type parameter that is the type of the shadow.

I.e., if you start with a Queue[UInt], you get a ShadowFactory[UInt]. However, that has a method apply[A <: Data] where A is the type of the new shadow queue. This A isn't fixed, however. I.e., if you create a ShadowFactory, you then want to create an A that can be a UInt, SInt, or any user-defined Bundle.

If withShadow took a second type parameter, then it would be locked in to creating shadow queues of only that type (or it would be something overly pessimistic like Data requiring asTypeOf casts). (I originally ran into this problem and switched this to the ShadowFactory approach to avoid it!)

Sound good?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, I agree with what you've said that we don't need to pass the desired shadow type through, though it still seems like the ShadowFactory type might as well just be Data... I am not sure how the fact that the original queue was of type T is used at all by the code in ShadowFactory? not blocking

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh! i see you have changed this so that ShadowFactory no longer takes the T parameter 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, your intuition was right here in that it doesn't need the type parameter at all. That was a nice suggestion.

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