Skip to content

Commit

Permalink
Better doc, thanks for the feedback you gived me <3
Browse files Browse the repository at this point in the history
  • Loading branch information
Dolu1990 committed Nov 9, 2023
1 parent 48c82f4 commit 78a9c2f
Showing 1 changed file with 85 additions and 53 deletions.
138 changes: 85 additions & 53 deletions source/SpinalHDL/Libraries/Pipeline/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ Introduction

spinal.lib.misc.pipeline provide a pipelining API. The main advantages over manual pipelining are :

- You don't have to pre define data structure with all the element which goes through each individual stage
- You can compose the pipeline (as a consequence of the above point)
- You don't have to pre define upfront all the signal elements needed for the entire staged system, you can create and consume stagable signals in a more adhoc fashion as your design requires without needing to refactor all the intervening stages to know about the signal
- Signals of the pipeline can utilize the powerful parametrization capabilities of SpinalHDL and be subject to optimization/removal if a specific design build does not require a particular parametrized feature, without any need to modify the staging system design or project code base in a significant way.
- Manual retiming is much easier, as you don't have to handle the registers / arbitration manualy
- Manage the arbitration by itself

The API is composed of 4 main things :

- Node : which represent a layer in the pipeline
- Node : which represents a layer in the pipeline
- Connector : which allows to connect nodes to each others
- Builder : which will generate the hardware required for a whole pipeline
- NamedType : which are used to define a hardware signal which can go through the pipeline
- SignalKey : which are used to retrieve hardware signals on nodes along the pipeline

It is important to understande that NamedType isn't a hardware data instance, but a key to retrieve a data along the pipeline on nodes.
It is important to understand that SignalKey isn't a hardware data/signal instance, but a key to retrieve a data/signal on nodes along the pipeline, and that the pipeline builder will then automatically interconnect/pipeline every occurrence of a given SignalKey between nodes.

Here is an example to illustrate :

Expand Down Expand Up @@ -49,9 +49,9 @@ Here is a simple example which only use the basics of the API :
val s01 = StageConnector(n0, n1)
val s12 = StageConnector(n1, n2)
// Let's define a few NamedType things that can go through the pipeline
val VALUE = NamedType(UInt(16 bits))
val RESULT = NamedType(UInt(16 bits))
// Let's define a few SignalKey things that can go through the pipeline
val VALUE = SignalKey(UInt(16 bits))
val RESULT = SignalKey(UInt(16 bits))
// Let's bind io.up to n0
io.up.ready := n0.ready
Expand Down Expand Up @@ -90,7 +90,7 @@ Here is the same example but using more of the API :
import spinal.lib.misc.pipeline._
class TopLevel extends Component {
val VALUE = NamedType(UInt(16 bits))
val VALUE = SignalKey(UInt(16 bits))
val io = new Bundle{
val up = slave Stream(VALUE) //VALUE can also be used as a HardType
Expand Down Expand Up @@ -121,79 +121,109 @@ Here is the same example but using more of the API :
builder.genStagedPipeline()
}
NamedType
SignalKey
============

NamedType class can be instanciated to represent some data which can go through the pipeline. Technicaly speaking, NamedType is a HardType which has a name and is used as a "key" to retrieve stuff.
SignalKey class can be instanciated to represent some data which can go through the pipeline. Technicaly speaking, SignalKey is a HardType which has a name and is used as a "key" to retrieve stuff.

.. code-block:: scala
val PC = NamedType(UInt(32 bits))
val PC_PLUS_4 = NamedType(UInt(32 bits))
val PC = SignalKey(UInt(32 bits))
val PC_PLUS_4 = SignalKey(UInt(32 bits))
val n0, n1 = Node()
val s01 = StageConnector(n0, n1)
n0(PC) := 0x42
n1(PC_PLUS_4) := n1(PC) + 4
Note that I got used to name the NamedType instances using uppercase. This is to make it very explicit that the thing isn't a hardware signal, but are more like a "key/type" to access things.
Note that I got used to name the SignalKey instances using uppercase. This is to make it very explicit that the thing isn't a hardware signal, but are more like a "key/type" to access things.

Node
============

Node mostly host the valid/ready arbitration signal, and the hardware signal required for all the NamedType values going through it.
Node mostly host the valid/ready arbitration signal, and the hardware signal required for all the SignalKey values going through it.

You can access its arbitration via :


.. list-table::
:header-rows: 1
:widths: 1 5
:widths: 2 1 10

* - API
- Access
- Description
* - node.valid
- Is the signal which specify if a transaction is present on the node
- RW
- Is the signal which specify if a transaction is present on the node. It is driven by the upstream. Once asserted, it can only be dropped the cycle after which ready is high or node.isRemoved.
* - node.ready
- Is the signal which specify if the node transaction can move away.
- RW
- Is the signal which specify if the node's transaction should move away. It is driven by the downstream to create backpresure. The signal has no meaning when there is no transaction (node.valid being deasserted)
* - node.isValid
- RO
- node.valid's read only accessor
* - node.isReady
- RO
- node.ready's read only accessor
* - node.isFiring
- True when the node transaction is successfuly moving futher (isValid && isReady && !isRemoved). Usefull to commit state changes
- RO
- True when the node transaction is successfuly moving futher (isValid && isReady && !isRemoved). Useful to commit state changes.
* - node.isMoving
- True when the node transaction is moving (isValid && (isReady || isRemoved)). Usefull to "reset" states
- RO
- True when the node transaction is moving away from the node (will not be in the node anymore starting from the next cycle),
either because downstream is ready to take the transaction,
either because the transaction is removed/flushed from the while pipeline. (isValid && (isReady || isRemoved)). Useful to "reset" states.
* - node.isRemoved
- True when the node is being flushed
- RO
- True when the node is being marked to be removed/flushed. Meaning that it will not appear anywhere in the pipeline in future cycles.

You can access its NamedType's signals via :
Note that the node.valid/node.ready signals follows the same conventions than the Stream's ones.

Here is a list of arbitration cases you can have on a node. valid/ready/isRemoved define the state we are in, while isFiring/isMoving result of those :

+-------+-------+-----------+------------------------------+----------+----------+
| valid | ready | isRemoved | Description | isFiring | isMoving |
+=======+=======+===========+==============================+==========+==========+
| 0 | X | 0 | No transaction | 0 | 0 |
+-------+-------+-----------+------------------------------+----------+----------+
| 1 | 1 | 0 | Going through | 1 | 1 |
+-------+-------+-----------+------------------------------+----------+----------+
| 1 | 0 | 0 | Blocked | 0 | 0 |
+-------+-------+-----------+------------------------------+----------+----------+
| 1 | X | 1 | Removed | 0 | 1 |
+-------+-------+-----------+------------------------------+----------+----------+
| 1 | 0 | 1 | Blocked and Removed | 0 | 1 |
+-------+-------+-----------+------------------------------+----------+----------+

Note that if you want to model things like for instance a CPU stage which can block and flush stuff, take a look a the CtrlConnector, as it provide the API to do such things.

You can access its SignalKey's signals via :

.. list-table::
:header-rows: 1
:widths: 2 5

* - API
- Description
* - node(NamedType)
* - node(SignalKey)
- Return the corresponding hardware signal
* - node(NamedType, Any)
* - node(SignalKey, Any)
- Same as above, but include a second argument which is used as a "secondary key". This ease the construction of multi lane hardware. For instance, when you have a multi issue CPU pipeline, you can use the lane Int id as secondary key
* - node.insert(Data)
- Return a new NamedType instance which is connected to the given Data hardware signal
- Return a new SignalKey instance which is connected to the given Data hardware signal



.. code-block:: scala
val n0, n1 = Node()
val PC = NamedType(UInt(32 bits))
val PC = SignalKey(UInt(32 bits))
n0(PC) := 0x42
n0(PC, "true") := 0x42
n0(PC, 0x666) := 0xEE
val SOMETHING = n0.insert(myHardwareSignal) //This create a new NamedType
val SOMETHING = n0.insert(myHardwareSignal) //This create a new SignalKey
when(n1(SOMETHING) === 0xFFAA){ ... }
Expand All @@ -208,12 +238,12 @@ Also, there is an API to define nodes which are always valid / ready
* - node.setAlwaysValid()
- Specify that the valid signal of the given node is always True. To use on the first node of a pipeline
* - node.setAlwaysReady()
- Specify that the ready signal of the given node is always True. To use on the last node of a pipeline, usefull if you don't have to implement backpresure.
- Specify that the ready signal of the given node is always True. To use on the last node of a pipeline, useful if you don't have to implement backpresure.

.. code-block:: scala
val n0, n1, n2 = Node()
val OUT = NamedType(UInt(16 bits))
val OUT = SignalKey(UInt(16 bits))
val outputFlow = master Flow(UInt(16 bits))
outputFlow.valid := n2.valid
Expand All @@ -238,11 +268,11 @@ While you can manualy drive/read the arbitration/data of the first/last stage of
* - node.arbitrateTo(Flow[T]])
- Drive a Flow arbitration from the node.
* - node.driveFrom(Stream[T]])((Node, T) => Unit)
- Drive a node from a stream. The provided landa function can be use to connect the data
- Drive a node from a stream. The provided lambda function can be use to connect the data
* - node.driveFrom(Flow[T]])((Node, T) => Unit)
- Same as above but for Flow
* - node.driveTo(Stream[T]])((T, Node) => Unit)
- Drive a stream from the node. The provided landa function can be use to connect the data
- Drive a stream from the node. The provided lambda function can be use to connect the data
* - node.driveTo(Flow[T]])((T, Node) => Unit)
- Same as above but for Flow

Expand All @@ -251,8 +281,8 @@ While you can manualy drive/read the arbitration/data of the first/last stage of
val n0, n1, n2 = Node()
val IN = NamedType(UInt(16 bits))
val OUT = NamedType(UInt(16 bits))
val IN = SignalKey(UInt(16 bits))
val OUT = SignalKey(UInt(16 bits))
n1(OUT) := n1(IN) + 0x42
Expand All @@ -264,20 +294,20 @@ While you can manualy drive/read the arbitration/data of the first/last stage of
n2.driveTo(down)((payload, self) => payload := self(OUT))
In order to reduce verbosity, there is a set of implicit convertions between NamedType toward their data representation which can be used when you are in the context of a Node :
In order to reduce verbosity, there is a set of implicit conversions between SignalKey toward their data representation which can be used when you are in the context of a Node :

.. code-block:: scala
val VALUE = NamedType(UInt(16 bits))
val VALUE = SignalKey(UInt(16 bits))
val n1 = new Node{
val PLUS_ONE = insert(VALUE + 1) // VALUE is implicitly converted into its n1(VALUE) representation
}
You can also use those implicit convertions by importing them :
You can also use those implicit conversions by importing them :

.. code-block:: scala
val VALUE = NamedType(UInt(16 bits))
val VALUE = SignalKey(UInt(16 bits))
val n1 = Node()
val n1Stuff = new Area {
Expand All @@ -291,19 +321,21 @@ There is also an API which alows you to create new Area which provide the whole
.. code-block:: scala
val n1 = Node()
val VALUE = NamedType(UInt(16 bits))
val VALUE = SignalKey(UInt(16 bits))
val n1Stuff = new n1.Area{
val PLUS_ONE = insert(VALUE) + 1 // Equivalent to n1.insert(n1(VALUE)) + 1
}
Such feature is very usefull when you have parametrable pipelines locations for your hardware (see retiming example).
Such feature is very useful when you have parametrizable pipeline locations for your hardware (see retiming example).


Connectors
============

There is few different connectors already implemented (but you could also create your own custom one) :
There is few different connectors already implemented (but you could also create your own custom one).
The idea of connectors is to connect two nodes together in various ways.
They generally have a `up` Node and a `down` Node.

DirectConnector
------------------
Expand All @@ -330,7 +362,7 @@ This connect two nodes using registers on the data / valid signals and some arbi
S2mConnector
------------------

This connect two nodes using registers on the ready signal, which can be usefull to improve backpresure combinatorial timings.
This connect two nodes using registers on the ready signal, which can be useful to improve backpresure combinatorial timings.

.. code-block:: scala
Expand Down Expand Up @@ -375,41 +407,41 @@ Also note that if you want to do flow control in a conditional scope (ex in a wh
You can retrieve which node are connected using node.up / node.down.

The CtrlConnector also provide an API to access NamedType :
The CtrlConnector also provide an API to access SignalKey :

.. list-table::
:header-rows: 1
:widths: 2 5

* - API
- Description
* - connector(NamedType)
- Same as connector.down(NamedType)
* - connector(NamedType, Any)
- Same as connector.down(NamedType, Any)
* - connector(SignalKey)
- Same as connector.down(SignalKey)
* - connector(SignalKey, Any)
- Same as connector.down(SignalKey, Any)
* - connector.insert(Data)
- Same as connector.down.insert(Data)
* - connector.bypass(NamedType)
- Allows to conditionaly override a NamedType value between connector.up -> connector.down. This can be used to fix data hazard in CPU pipelines for instance.
* - connector.bypass(SignalKey)
- Allows to conditionaly override a SignalKey value between connector.up -> connector.down. This can be used to fix data hazard in CPU pipelines for instance.


.. code-block:: scala
val c01 = CtrlConnector(n0, n1)
val PC = NamedType(UInt(32 bits))
val PC = SignalKey(UInt(32 bits))
c01(PC) := 0x42
c01(PC, 0x666) := 0xEE
val DATA = NamedType(UInt(32 bits))
val DATA = SignalKey(UInt(32 bits))
// Let's say Data is inserted in the pipeline before c01
when(hazard){
c01.bypass(DATA) := fixedValue
}
// c01(DATA) and below will get the hazard patch
Note that if you create a CtrlConnector without node arguements, it will create its own nodes internaly.
Note that if you create a CtrlConnector without node arguments, it will create its own nodes internally.

.. code-block:: scala
Expand Down Expand Up @@ -461,7 +493,7 @@ To generate the hardware of your pipeline, you need to give a list of all the co
There is also a set of "all in one" builders that you can instanciate to help yourself.

For instance there is the NodesBuilder class which can be used to create sequencialy staged pipelines :
For instance there is the NodesBuilder class which can be used to create sequentially staged pipelines :

.. code-block:: scala
Expand All @@ -487,7 +519,7 @@ The example below show a pattern which compose a pipeline with multiple lanes to
// This area allows to take a input value and do +1 +1 +1 over 3 stages.
// I know that's useless, but let's pretend that instead it does a multiplication between two numbers over 3 stages (for FMax reasons)
class PLus3(INPUT: NamedType[UInt], stage1: Node, stage2: Node, stage3: Node) extends Area {
class PLus3(INPUT: SignalKey[UInt], stage1: Node, stage2: Node, stage3: Node) extends Area {
val ONE = stage1.insert(stage1(INPUT) + 1)
val TWO = stage2.insert(stage2(ONE) + 1)
val THREE = stage3.insert(stage3(TWO) + 1)
Expand Down

0 comments on commit 78a9c2f

Please sign in to comment.