本文作为SpinalHDL学习笔记第四篇,记录使用SpinalHDL的流水线相关库函数方法。
SpinalHDL学习笔记总纲链接如下:
流水线作为现代芯片必备组件,SpinalHDL针对流水线的库函数极大地提升了开发效率,特别是后期有改动的情况,无论是增减流水线技术还是反压的步骤,与使用Verilog相比不可同日而语,那真是鸟枪换美琴。
SpinalHDL流水线API
spinal.lib.misc.pipeline 提供了一套流水线 API。相对于手动流水线它的主要优点是:
• 不必预先准备好整个流水系统中所需的所有信号元素。可以根据设计需要,以更特别的方式创建和使用可分级的信号,而无需重构所有中间阶段来处理信号
• 流水线的信号可以利用 SpinalHDL 的强大参数化能力,并且如果设计构建中不需要特定的参数化特征,则可以进行优化/移除,而不需要以显著的方式修改流水系统设计或项目代码库。
• 手动时序调整要容易得多,因为您不必手动处理寄存器/仲裁器
• 它会自行管理仲裁器
API 由 4 个主要部分组成:
• Node:表示管道中的层
• Link:允许节点相互连接
• Builder:生成整个管道所需的硬件
• Payload:用于获取流水线的节点上的硬件信号
重要的是, Payload 不是硬件数据/信号实例,而是用于检索流水线在节点中数据/信号的关键,并且流水
线构建器随后将在节点之间的每次给定 Payload 出现时自动互连/流水线。
下面是一个简单的例子,它只使用了基本的 API:
import spinal.core._
import spinal.core.sim._
import spinal.lib._
import spinal.lib.misc.pipeline._
class TopLevel extends Component {
val io = new Bundle{
val up = slave Stream (UInt(16 bits))
val down = master Stream (UInt(16 bits))
}
// Let's define 3 Nodes for our pipeline
val n0, n1, n2 = Node()
// Let's connect those nodes by using simples registers
val s01 = StageLink(n0, n1)
val s12 = StageLink(n1, n2)
// Let's define a few Payload things that can go through the pipeline
val VALUE = Payload(UInt(16 bits))
val RESULT = Payload(UInt(16 bits))
// Let's bind io.up to n0
io.up.ready := n0.ready
n0.valid := io.up.valid
n0(VALUE) := io.up.payload
// Let's do some processing on n1
n1(RESULT) := n1(VALUE) + 0x1200
// Let's bind n2 to io.down
n2.ready := io.down.ready
io.down.valid := n2.valid
io.down.payload := n2(RESULT)
// Let's ask the builder to generate all the required hardware
Builder(s01, s12)
}
这将产生以下硬件:
下面是一个仿真波形:
下面是相同的示例,但使用了更多的 API:
import spinal.core._
import spinal.core.sim._
import spinal.lib._
import spinal.lib.misc.pipeline._
class TopLevel extends Component {
val VALUE = Payload(UInt(16 bits))
val io = new Bundle{
val up = slave Stream(VALUE) //VALUE can also be used as a HardType
val down = master Stream(VALUE)
}
// NodesBuilder will be used to register all the nodes created, connect them via␣
,→stages and generate the hardware
val builder = new NodesBuilder()
// Let's define a Node which connect from io.up
val n0 = new builder.Node{
arbitrateFrom(io.up)
VALUE := io.up.payload
}
// Let's define a Node which do some processing
val n1 = new builder.Node{
val RESULT = insert(VALUE + 0x1200)
}
// Let's define a Node which connect to io.down
val n2 = new builder.Node {
arbitrateTo(io.down)
io.down.payload := n1.RESULT
}
// Let's connect those nodes by using registers stages and generate the related␣
,→hardware
builder.genStagedPipeline()
}
Payload
Payload 对象用于引用可以通过流水线的数据。从技术上讲, Payload 是一个 HardType,它有一个名字,并被用作在流水线某个级中检索信号的“键”。
val PC = Payload(UInt(32 bits))
val PC_PLUS_4 = Payload(UInt(32 bits))
val n0, n1 = Node()
val s01 = StageLink(n0, n1)
n0(PC) := 0x42
n1(PC_PLUS_4) := n1(PC) + 4
Node
Node 主要托管有效/就绪仲裁信号,以及所有通过它的硬件信号所需的 Payload。
您可以通过以下方式访问其仲裁器:
API | 访问 | 描述 |
node.valid | RW | 指定节点上是否存在事务的信号。它是由上游逻辑驱动的。一旦置为 1,则 |
node.ready | RW | 指定节点的事务是否可以向下游进行的信号。它是由下游驱动以创建反压。 |
node.cancel | RW | 指定节点的事务是否正在从流水线中取消的信号。它由下游驱动。当没有事 |
node.isValid | RO | node.valid 的只读访问器 |
node.isReady | RO | node.ready 的只读访问器 |
node.isCancel | RO | node.cancel 的只读访问器 |
node.isFiring | RO | 当节点事务成功继续进行时为 True(valid && ready && !cancel)。用于提交状 |
node.isMoving | RO | 当节点事务将不再存在于节点上时(从下一周期开始)为 True,要么是因为 |
node.isCanceling | RO | 当节点事务正在被取消时为 True。这意味着在将来的周期中它不会出现在流 |
Node 的控制信号(valid/ready/cancel)和状态信号(isValid、 isReady、 isCancel、 isFiring 等)是按需创建
的。因此,例如,可以通过永远不引用 ready 信号来创建没有反压的流水线。这就是在想要读取某物的
状态时使用状态信号,仅在想要驱动某物时使用控制信号的重要性所在。
以下是节点上可能出现的仲裁情况列表。 valid/ready/cancel 定义了我们所处的状态,而 isFiring/isMoving
是这些状态的结果:
valid | ready | cancel | 描述 | isFiring | isMoving |
0 | X | X | 无事务 | 0 | 0 |
1 | 1 | 0 | 正在进行 | 1 | 1 |
1 | 0 | 0 | 阻塞 | 0 | 0 |
1 | X | 1 | 取消 | 0 | 1 |
可以通过以下方式访问由 Payload 引用的信号:
API | 描述 |
node(Payload) | 返回对应的硬件信号 |
node(Payload, Any) | 与上述相同,但包括一个用作“次要键”的第二个参数。这有助于构建 |
node.insert(Data) | 返回一个新的 Payload 实例,该实例连接到给定的 Data 硬件信号 |
val n0, n1 = Node()
val PC = Payload(UInt(32 bits))
n0(PC) := 0x42
n0(PC, "true") := 0x42
n0(PC, 0x666) := 0xEE
val SOMETHING = n0.insert(myHardwareSignal) //This create a new Payload
when(n1(SOMETHING) === 0xFFAA){ ... }
可以手动驱动/读取流水线的第一个/最后一级的仲裁/数据,但有一些实用工具可以连接其边界。
API | 描述 |
node.arbitrateFrom(Stream[T]]) | 由反压流驱动节点仲裁。 |
node.arbitrateFrom(Flow[T]]) | 由数据流驱动节点仲裁。 |
node.arbitrateTo(Stream[T]]) | 由节点驱动反压流仲裁。 |
node.arbitrateTo(Flow[T]]) | 由节点驱动数据流仲裁。 |
node.driveFrom(Stream[T]])((Node, T) => Unit) | 由反压流驱动节点。提供的 lambda 函数可以用于 |
node.driveFrom(Flow[T]])((Node, T) => Unit) | 与上述类似,但适用于 Flow |
node.driveTo(Stream[T]])((T, Node) => Unit) | 由节点驱动反压流。提供的 lambda 函数可以用于 |
node.driveTo(Flow[T]])((T, Node) => Unit) | 与上述类似,但适用于 Flow |
val n0, n1, n2 = Node()
val IN = Payload(UInt(16 bits))
val OUT = Payload(UInt(16 bits))
n1(OUT) := n1(IN) + 0x42
// Define the input / output stream that will be later connected to the pipeline
val up = slave Stream(UInt(16 bits))
val down = master Stream(UInt(16 bits)) //Note master Stream(OUT) is good aswell
n0.driveFrom(up)((self, payload) => self(IN) := payload)
n2.driveTo(down)((payload, self) => payload := self(OUT))
为了减少冗长,在 Payload 与其数据表示之间有一组隐式转换,可在 Node 下使用:
val VALUE = Payload(UInt(16 bits))
val n1 = new Node{
val PLUS_ONE = insert(VALUE + 1) // VALUE is implicitly converted into its␣
,→n1(VALUE) representation
}
还可以通过导入它们来使用这些隐式转换:
val VALUE = Payload(UInt(16 bits))
val n1 = Node()
val n1Stuff = new Area {
import n1._
val PLUS_ONE = insert(VALUE) + 1 // Equivalent to n1.insert(n1(VALUE)) + 1
}
还有一个 API,它允许你创建新的 Area,这个 Area 提供了给定节点实例的全部 API(包括隐式转换),而无需导入:
val n1 = Node()
val VALUE = Payload(UInt(16 bits))
val n1Stuff = new n1.Area{
val PLUS_ONE = insert(VALUE) + 1 // Equivalent to n1.insert(n1(VALUE)) + 1
}
Links
目前已经实现了一些不同的 Links(但您也可以创建自己的自定义 Links)。 Links 的思想是以各种方式将
两个节点连接在一起,它们通常有一个 up 节点和一个 down 节点。
DirectLink
非常简单,它只使用导线连接两个节点。以下是一个示例:
val c01 = DirectLink(n0, n1)
StageLink
这使用 data/valid 信号上的寄存器和 ready 信号上的一些仲裁连接了两个节点。
val c01 = StageLink(n0, n1)
S2mLink
这使用 ready 信号上的寄存器连接两个节点,这对于改进反压组合时序非常有用。
val c01 = S2mLink(n0, n1)
CtrlLink
这是一种特殊的 Link,用于连接两个节点,具有可选的流量控制/旁路逻辑。它的应用程序接口应该足够
灵活,可以用它来实现 CPU 流水级。
以下是其流量控制 API(Bool 参数启用了相关功能):
API | 描述 |
haltWhen(Bool) | 允许阻止当前传输事务(清除 up.ready down.valid) |
throwWhen(Bool) | 允许从流水线中取消当前事务(清除 down.valid,使事务驱动逻辑忘记 |
forgetOneWhen(Bool) | 允许请求上游节点忘记其当前事务(但不会清除 down.valid) |
ignoreReadyWhen(Bool) | 允许忽略下游节点 ready(设置 up.ready 为 1) |
duplicateWhen(Bool) | 允许复制当前传输事务(清零 up.ready) |
terminateWhen(Bool) | 允许下游节点隐藏当前传输事务(清零 down.valid) |
还要注意的是,如果要在条件作用域(例如在 when 语句中)进行通信流控制,可以调用以下函数:
• haltIt(), duplicateIt(), terminateIt(), forgetOneNow(), ignoreReadyNow(), throwIt()
val c01 = CtrlLink(n0, n1)
c01.haltWhen(something) // Explicit halt request
when(somethingElse){
c01.haltIt() // Conditional scope sensitive halt request, same as c01.
,→haltWhen(somethingElse)
}
可以使用 node.up / node.down 查看哪些节点连接到了链接。
CtrlLink 还提供了访问 Payload 的 API:
API | 描述 |
link(Payload) | 与 Link.down(Payload) 相同 |
link(Payload, Any) | 与 Link.down(Payload, Any) 相同 |
link.insert(Data) | 与 Link.down.insert(Data) 相同 |
link.bypass(Payload) | 允许在 link.up -> link.down 之间有条件地覆盖 Payload 值。例如,这可用 |
val c01 = CtrlLink(n0, n1)
val PC = Payload(UInt(32 bits))
c01(PC) := 0x42
c01(PC, 0x666) := 0xEE
val DATA = Payload(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
请注意,如果创建的 CtrlLink 不带节点参数,它将在内部创建自己的节点。
val decode = CtrlLink()
val execute = CtrlLink()
val d2e = StageLink(decode.down, execute.up)
Builder
要生成流水线硬件,您需要提供流水线中使用的所有链接列表。
// Let's define 3 Nodes for our pipeline
val n0, n1, n2 = Node()
// Let's connect those nodes by using simples registers
val s01 = StageLink(n0, n1)
val s12 = StageLink(n1, n2)
// Let's ask the builder to generate all the required hardware
Builder(s01, s12)
此外,还有一套“一体化“的构建工具,可以利用它来帮助你自己。
例如,有一个 NodesBuilder 类,可用于创建按顺序分级的流水线:
val builder = new NodesBuilder()
// Let's define a few nodes
val n0, n1, n2 = new builder.Node
// Let's connect those nodes by using registers and generate the related hardware
builder.genStagedPipeline()