Specification
The Stream interface is a simple handshake protocol to carry payload.
It could be used for example to push and pop elements into a FIFO, send requests to a UART controller, etc.
Signal | Type | Driver | Description | Don’t care when |
---|---|---|---|---|
valid | Bool | Master | When high => payload present on the interface | - |
ready | Bool | Slave | When low => transaction are not consumed by the slave | valid is low |
payload | T | Master | Content of the transaction | valid is low |
There is an example diagram :
There is some examples of usage in SpinalHDL :
class StreamFifo[T <: Data](dataType: T, depth: Int) extends Component {
val io = new Bundle {
val push = slave Stream (dataType)
val pop = master Stream (dataType)
}
...
}
class StreamArbiter[T <: Data](dataType: T,portCount: Int) extends Component {
val io = new Bundle {
val inputs = Vec(slave Stream (dataType),portCount)
val output = master Stream (dataType)
}
...
}
- An priority arbiter without lock logic can switch from one input to the other (which will change the payload).
- An UART controller could directly use the write port to drive UART pins and only consume the transaction at the end of the transmission.
Be careful with that.
Functions
Syntax | Description | Return | Latency |
---|---|---|---|
Stream(type : Data) | Create a Stream of a given type | Stream[T] | |
master/slave Stream(type : Data) | Create a Stream of a given type Initialized with corresponding in/out setup |
Stream[T] | |
x.fire | Return True when a transaction is consumed on the bus (valid && ready) | Bool | - |
x.isStall | Return True when a transaction is stall on the bus (valid && ! ready) | Bool | - |
x.queue(size:Int) | Return a Stream connected to x through a FIFO | Stream[T] | 2 |
x.m2sPipe() x.stage() |
Return a Stream drived by x through a register stage that cut valid/payload paths Cost = (payload width + 1) flop flop |
Stream[T] | 1 |
x.s2mPipe() | Return a Stream drived by x ready paths is cut by a register stage Cost = payload width * (mux2 + 1 flip flop) |
Stream[T] | 0 |
x.halfPipe() | Return a Stream drived by x valid/ready/payload paths are cut by some register Cost = (payload width + 2) flip flop, bandwidth divided by two |
Stream[T] | 1 |
x « y y » x |
Connect y to x | 0 | |
x <-< y y >-> x |
Connect y to x through a m2sPipe | 1 | |
x </< y y >/> x |
Connect y to x through a s2mPipe | 0 | |
x <-/< y y >/-> x |
Connect y to x through s2mPipe().m2sPipe() Which imply no combinatorial path between x and y |
1 | |
x.haltWhen(cond : Bool) | Return a Stream connected to x Halted when cond is true |
Stream[T] | 0 |
x.throwWhen(cond : Bool) | Return a Stream connected to x When cond is true, transaction are dropped |
Stream[T] | 0 |
The following code will create this logic :
case class RGB(channelWidth : Int) extends Bundle{
val red = UInt(channelWidth bit)
val green = UInt(channelWidth bit)
val blue = UInt(channelWidth bit)
def isBlack : Bool = red === 0 && green === 0 && blue === 0
}
val source = Stream(RGB(8))
val sink = Stream(RGB(8))
sink <-< source.throwWhen(source.payload.isBlack)
Utils
There is many utils that you can use in your design in conjunction with the Stream bus, This chapter will document them.
StreamFifo
On each stream you can call the .queue(size) to get a buffered stream. But you can also instantiate the FIFO component itself :
val streamA,streamB = Stream(Bits(8 bits))
//...
val myFifo = StreamFifo(
dataType = Bits(8 bits),
depth = 128
)
myFifo.io.push << streamA
myFifo.io.pop >> streamB
parameter name | Type | Description |
---|---|---|
dataType | T | Payload data type |
depth | Int | Size of the memory used to store elements |
io name | Type | Description |
---|---|---|
push | Stream[T] | Used to push elements |
pop | Stream[T] | Used to pop elements |
flush | Bool | Used to remove all elements inside the FIFO |
occupancy | UInt of log2Up(depth + 1) bits | Indicate the internal memory occupancy |
StreamFifoCC
You can instanciate the dual clock domain version of the fifo by the following way :
val clockA = ClockDomain(???)
val clockB = ClockDomain(???)
val streamA,streamB = Stream(Bits(8 bits))
//...
val myFifo = StreamFifoCC(
dataType = Bits(8 bits),
depth = 128,
pushClock = clockA,
popClock = clockB
)
myFifo.io.push << streamA
myFifo.io.pop >> streamB
parameter name | Type | Description |
---|---|---|
dataType | T | Payload data type |
depth | Int | Size of the memory used to store elements |
pushClock | ClockDomain | Clock domain used by the push side |
popClock | ClockDomain | Clock domain used by the pop side |
io name | Type | Description |
---|---|---|
push | Stream[T] | Used to push elements |
pop | Stream[T] | Used to pop elements |
pushOccupancy | UInt of log2Up(depth + 1) bits | Indicate the internal memory occupancy (from the push side perspective) |
popOccupancy | UInt of log2Up(depth + 1) bits | Indicate the internal memory occupancy (from the pop side perspective) |
StreamCCByToggle
Component that provide a Stream cross clock domain bridge based on toggling signals.
This way of doing cross clock domain bridge is characterized by a small area usage but also a low bandwidth.
val clockA = ClockDomain(???)
val clockB = ClockDomain(???)
val streamA,streamB = Stream(Bits(8 bits))
//...
val bridge = StreamCCByToggle(
dataType = Bits(8 bits),
inputClock = clockA,
outputClock = clockB
)
bridge.io.input << streamA
bridge.io.output >> streamB
parameter name | Type | Description |
---|---|---|
dataType | T | Payload data type |
inputClock | ClockDomain | Clock domain used by the push side |
outputClock | ClockDomain | Clock domain used by the pop side |
io name | Type | Description |
---|---|---|
input | Stream[T] | Used to push elements |
output | Stream[T] | Used to pop elements |
But you can also use a this shorter syntax which directly return you the cross clocked stream:
val clockA = ClockDomain(???)
val clockB = ClockDomain(???)
val streamA = Stream(Bits(8 bits))
val streamB = StreamCCByToggle(
input = streamA,
inputClock = clockA,
outputClock = clockB
)
StreamArbiter
When you have multiple Streams and you want to arbitrate them to drive a single one, you can use the StreamArbiterFactory.
val streamA, streamB, streamC = Stream(Bits(8 bits))
val arbitredABC = StreamArbiterFactory.roundRobin.onArgs(streamA, streamB, streamC)
val streamD, streamE, streamF = Stream(Bits(8 bits))
val arbitredDEF = StreamArbiterFactory.lowerFirst.noLock.onArgs(streamD, streamE, streamF)
Arbitration functions | Description |
---|---|
lowerFirst | Lower port have priority over higher port |
roundRobin | Fair round robin arbitration |
sequentialOrder | Could be used to retrieve transaction in a sequancial order First transaction should come from port zero, then from port one, … |
Lock functions | Description |
---|---|
noLock | The port selection could change every cycle, even if the transaction on the selected port is not consumed. |
transactionLock | The port selection is locked until the transaction on the selected port is consumed. |
fragmentLock | Could be used to arbitrate Stream[Flow[T]]. In this mode, the port selection is locked until the selected port finish is burst (last=True). |
Generation functions | Return |
---|---|
on(inputs : Seq[Stream[T]]) | Stream[T] |
onArgs(inputs : Stream[T]*) | Stream[T] |
StreamFork
This utile take its input stream and duplicate it outputCount times.
val inputStream = Stream(Bits(8 bits))
val dispatchedStreams = StreamDispatcherSequencial(
input = inputStream,
outputCount = 3
)
StreamDispatcherSequencial
This utile take its input stream and route it to outputCount
stream in a sequential order.
val inputStream = Stream(Bits(8 bits))
val dispatchedStreams = StreamDispatcherSequencial(
input = inputStream,
outputCount = 3
)