本文作为SpinalHDL学习笔记第二十三篇,介绍SpinalHDL总线相关API。
SpinalHDL学习笔记总纲链接如下:
目录:
1.AHB-Lite3
2.Apb3
3.Axi4
4.AvalonMM
5.Tilelink
6.tilelink.fabric.Node
1.AHB-Lite3
配置和实例化
首先,想要创建 AHB-Lite3 总线时,都需要一个配置对象。该配置对象是一个 AhbLite3Config,并具有以下参数:
参数名称 |
类型 |
默认值 |
描述 |
addressWidth |
Int |
HADDR 的位宽(字节粒度) |
HADDR位宽 |
dataWidth |
Int |
HWDATA 和 HRDATA 的位宽 |
HWDATA和HRDATA位宽 |
简而言之, AHB-Lite3 总线在 SpinalHDL 库中是如下定义的:
case class AhbLite3(config: AhbLite3Config) extends Bundle with IMasterSlave{
// Address and control
val HADDR = UInt(config.addressWidth bits)
val HSEL = Bool()
val HREADY = Bool()
val HWRITE = Bool()
val HSIZE = Bits(3 bits)
val HBURST = Bits(3 bits)
val HPROT = Bits(4 bits)
val HTRANS = Bits(2 bits)
val HMASTLOCK = Bool()
// Data
val HWDATA = Bits(config.dataWidth bits)
val HRDATA = Bits(config.dataWidth bits)
// Transfer response
val HREADYOUT = Bool()
val HRESP = Bool()
override def asMaster(): Unit = {
out(HADDR,HWRITE,HSIZE,HBURST,HPROT,HTRANS,HMASTLOCK,HWDATA,HREADY,HSEL)
in(HREADYOUT,HRESP,HRDATA)
}
}
这是一个简单的使用示例:
val ahbConfig = AhbLite3Config(
addressWidth = 12,
dataWidth = 32
)
val ahbX = AhbLite3(ahbConfig)
val ahbY = AhbLite3(ahbConfig)
when(ahbY.HSEL){
//...
}
变体
有一个 AhbLite3Master 变体,唯一的区别是缺少 HREADYOUT 信号。当互连线和从端使用 AhbLite3 时,此变体只能由主端使用。
2.Apb3
AMBA3-APB 总线通常用于连接低带宽外设。
配置和实例化
首先,每当想要创建 APB3 总线时,都需要一个配置对象。该配置对象是一个 Apb3Config 并具有以下参数:
参数名称 |
类型 |
描述 |
---|---|---|
addressWidth |
Int |
PADDR 的位宽(字节粒度) |
dataWidth |
Int |
PWDATA 和 PRDATA 的位宽 |
selWidth |
Int |
PSEL的位宽 |
useSlaveError |
Boolean |
指定是否出现PSLVERROR |
简而言之, APB3 总线在 SpinalHDL 库中定义方式如下:
case class Apb3(config: Apb3Config) extends Bundle with IMasterSlave {
val PADDR = UInt(config.addressWidth bits)
val PSEL = Bits(config.selWidth bits)
val PENABLE = Bool()
val PREADY = Bool()
val PWRITE = Bool()
val PWDATA = Bits(config.dataWidth bits)
val PRDATA = Bits(config.dataWidth bits)
val PSLVERROR = if(config.useSlaveError) Bool() else null
//...
}
这是一个简单的使用示例:
val apbConfig = Apb3Config(
addressWidth = 12,
dataWidth = 32
)
val apbX = Apb3(apbConfig)
val apbY = Apb3(apbConfig)
when(apbY.PENABLE){
//...
}
函数和运算符
名称 |
描述 |
---|---|
X » Y |
将 X 连接到 Y。 Y 的地址可以小于 X 的地址 |
X « Y |
执行 » 运算符相反的操作 |
3.Axi4
AXI4 是 ARM 定义的高带宽总线。
配置和实例化
首先,想要创建 AXI4 总线时需要一个配置对象。该配置对象是一个 Axi4Config 并具有以下参数:
Note: useXXX 用于指定总线是否存在 XXX 信号。
参数名称 |
类型 |
默认值 |
addressWidth |
Int |
|
dataWidth |
Int |
|
idWidth |
Int |
|
userWidth |
Int |
|
useId |
Boolean |
true |
useRegion |
Boolean |
true |
useBurst |
Boolean |
true |
useLock |
Boolean |
true |
useCache |
Boolean |
true |
useSize |
Boolean |
true |
useQos |
Boolean |
true |
useLen |
Boolean |
true |
useLast |
Boolean |
true |
useResp |
Boolean |
true |
useProt |
Boolean |
true |
useStrb |
Boolean |
true |
useUser |
Boolean |
false |
简而言之, AXI4 总线在 SpinalHDL 库中定义方式如下:
case class Axi4(config: Axi4Config) extends Bundle with IMasterSlave{
val aw = Stream(Axi4Aw(config))
val w = Stream(Axi4W(config))
val b = Stream(Axi4B(config))
val ar = Stream(Axi4Ar(config))
val r = Stream(Axi4R(config))
override def asMaster(): Unit = {
master(ar,aw,w)
slave(r,b)
}
}
这是一个简单的使用示例:
val axiConfig = Axi4Config(
addressWidth = 32,
dataWidth = 32,
idWidth = 4
)
val axiX = Axi4(axiConfig)
val axiY = Axi4(axiConfig)
when(axiY.aw.valid){
//...
}
变体
Axi4 总线还有其他 3 种变体:
类型 |
描述 |
---|---|
Axi4ReadOnly |
只存在 AR 和 R 通道 |
Axi4WriteOnly |
只存在 AW、 W 和 B 通道 |
Axi4Shared |
此变体是该库的首创。 |
4.AvalonMM
AvalonMM 总线非常适合 FPGA。它非常灵活:
• 能够与 APB 一样简单
• 在许多需要带宽的应用中比 AHB 更好,因为 AvalonMM 有一种将读取响应与命令解耦的模式(减少延迟读延迟的影响)。
• 性能不如 AXI,但使用的逻辑面积少得多(读取和写入命令使用相同的握手通道。主端不需要存储挂起请求的地址,从而避免读取/写入冒险)
配置和实例化
AvalonMM 包有一个构造参数 AvalonMMConfig 。由于 Avalon 总线的灵活性,AvalonMMConfig 有很多配置元素。有关 Avalon 规范的更多信息,请访问英特尔网站。
case class AvalonMMConfig( addressWidth : Int,
dataWidth : Int,
burstCountWidth : Int,
useByteEnable : Boolean,
useDebugAccess : Boolean,
useRead : Boolean,
useWrite : Boolean,
useResponse : Boolean,
useLock : Boolean,
useWaitRequestn : Boolean,
useReadDataValid : Boolean,
useBurstCount : Boolean,
//useEndOfPacket : Boolean,
addressUnits : AddressUnits = symbols,
burstCountUnits : AddressUnits = words,
burstOnBurstBoundariesOnly : Boolean = false,
constantBurstBehavior : Boolean = false,
holdTime : Int = 0,
linewrapBursts : Boolean = false,
maximumPendingReadTransactions : Int = 1,
maximumPendingWriteTransactions : Int = 0, // unlimited
readLatency : Int = 0,
readWaitTime : Int = 0,
setupTime : Int = 0,
writeWaitTime : Int = 0
)
这个配置类还有一些函数:
名称 |
返回类型 |
描述 |
---|---|---|
getReadOnlyConfg |
AvalonMMConfg |
返回一个类似的配置,但禁用所有写入属性 |
getWriteOnlyConfg |
AvalonMMConfg |
返回一个类似的配置,但禁用所有读取属性 |
// Create a write only AvalonMM configuration with burst capabilities and byte␣
,→enable
val myAvalonConfig = AvalonMMConfig.bursted(
addressWidth = addressWidth,
dataWidth = memDataWidth,
burstCountWidth = log2Up(burstSize + 1)
).copy(
useByteEnable = true,
constantBurstBehavior = true,
burstOnBurstBoundariesOnly = true
).getWriteOnlyConfig
// Create an instance of the AvalonMM bus by using this configuration
val bus = AvalonMM(myAvalonConfig)
5.Tilelink
配置和实例化
这是一个简单的示例,它定义了两个不相干的 tilelink 总线实例并将它们连接起来:
import spinal.lib.bus.tilelink
val param = tilelink.BusParameter.simple(
addressWidth = 32,
dataWidth = 64,
sizeBytes = 64,
sourceWidth = 4
)
val busA, busB = tilelink.Bus(param)
busA << busB
这里与上面相同,但是具有一致性通道:
import spinal.lib.bus.tilelink
val param = tilelink.BusParameter(
addressWidth = 32,
dataWidth = 64,
sizeBytes = 64,
sourceWidth = 4,
sinkWidth = 0,
withBCE = false,
withDataA = true,
withDataB = false,
withDataC = false,
withDataD = true,
node = null
)
val busA, busB = tilelink.Bus(param)
busA << busB
以上的内容是关于硬件实例化的,这是简单/容易的部分。当涉及 SoC/内存一致性时,可能需要一个额外的层来协调/传递参数。这就是 tilelink.fabric.Node 的作用。
6.tilelink.fabric.Node
tilelink.fabric.Node 是常规 tilelink 硬件实例之上的附加层,用于处理 SoC 级别的协调和参数递。它主要基于 Fiber API,它允许创建精化时间纤程(用户空间线程),从而允许确定未来的参数传递/协调和硬件生成。
可以通过 3 种方式创建节点 (Node):
• tilelink.fabric.Node.down():创建一个可以向下连接(向从端)的节点,因此它将用于 CPU/DMA/桥的代理
• tilelink.fabric.Node():创建中间节点
• tilelink.fabric.Node.up():创建一个可以向上连接(向主端)的节点,因此它将用于外设/存储器/桥的代理
节点大多具有以下属性:
• bus : Handle[tilelink.Bus];总线的硬件实例
• m2s.proposed : Handle[tilelink.M2sSupport];由向上连接提出的功能集
• m2s.supported : Handle[tilelink.M2sSupport]: 向下连接支持的功能集
• m2s.parameter : Handle[tilelink.M2sParameter]: 最终的总线参数
可以注意到它们都是句柄。 Handle 是 SpinalHDL 中在纤程之间共享值的一种方式。如果一个纤程读取一个句柄,而这个句柄还没有值,它将阻止该纤程的执行,直到另一个纤程向该句柄提供一个值。还有一组属性,类似于 m2s,但是反向的(名为 s2m),它们指定了由互连的从端发起的事务的参数(例如内存一致性)。
有两个演讲介绍了 tilelink.fabric.Node。这两个演讲可能并不完全遵循实际语法,它们仍然遵循以下概念:
• 介绍: https://youtu.be/hVi9xOGuuek
• 深入: https://peertube.f-si.org/videos/watch/bcf49c84-d21d-4571-a73e-96d7eb89e907
顶层示例
以下是一个简单的虚拟 SoC 顶层设计示例:
val cpu = new CpuFiber()
val ram = new RamFiber()
ram.up at(0x10000, 0x200) of cpu.down // map the ram at [0x10000-0x101FF], the ram␣
,→will infer its own size from it
val gpio = new GpioFiber()
gpio.up at 0x20000 of cpu.down // map the gpio at [0x20000-0x20FFF], its range of␣
,→4KB being fixed internally
还可以定义互连中的中间节点,如下所示:
val cpu = new CpuFiber()
val ram = new RamFiber()
ram.up at(0x10000, 0x200) of cpu.down
// Create a peripherals namespace to keep things clean
val peripherals = new Area{
// Create a intermediate node in the interconnect
val access = tilelink.fabric.Node()
access at 0x20000 of cpu.down
val gpioA = new GpioFiber()
gpioA.up at 0x0000 of access
val gpioB = new GpioFiber()
gpioB.up at 0x1000 of access
}
GPIOFiber 示例
GpioFiber 是一个简单的 tilelink 外设,可以读取/驱动 32 位三态阵列。
import spinal.lib._
import spinal.lib.bus.tilelink
import spinal.core.fiber.Fiber
class GpioFiber extends Area {
// Define a node facing upward (toward masters only)
val up = tilelink.fabric.Node.up()
// Define a elaboration thread to specify the "up" parameters and generate the␣
,→hardware
val fiber = Fiber build new Area {
// Here we first define what our up node support. m2s mean master to slave␣
,→requests
up.m2s.supported load tilelink.M2sSupport(
addressWidth = 12,
dataWidth = 32,
// Transfers define which kind of memory transactions our up node will␣
,→support.
// Here it only support 4 bytes get/putfull
transfers = tilelink.M2sTransfers(
get = tilelink.SizeRange(4),
putFull = tilelink.SizeRange(4)
)
)
// s2m mean slave to master requests, those are only use for memory coherency␣
,→purpose
// So here we specify we do not need any
up.s2m.none()
// Then we can finally generate some hardware
// Starting by defining a 32 bits TriStateArray (Array meaning that each pin␣
,→has its own writeEnable bit
val pins = master(TriStateArray(32 bits))
// tilelink.SlaveFactory is a utility allowing to easily generate the logic␣
,→required
// to control some hardware from a tilelink bus.
val factory = new tilelink.SlaveFactory(up.bus, allowBurst = false)
// Use the SlaveFactory API to generate some hardware to read / drive the pins
val writeEnableReg = factory.drive(pins.writeEnable, 0x0) init (0)
val writeReg = factory.drive(pins.write, 0x4) init(0)
factory.read(pins.read, 0x8)
}
}
RamFiber 示例
RamFiber 是常规 tilelink Ram 组件的集成层。
import spinal.lib.bus.tilelink
import spinal.core.fiber.Fiber
class RamFiber() extends Area {
val up = tilelink.fabric.Node.up()
val thread = Fiber build new Area {
// Here the supported parameters are function of what the master would like us␣
,→to idealy support.
// The tilelink.Ram support all addressWidth / dataWidth / burst length / get /
,→ put accesses
// but doesn't support atomic / coherency. So we take what is proposed to use␣
,→and restrict it to
// all sorts of get / put request
up.m2s.supported load up.m2s.proposed.intersect(M2sTransfers.allGetPut)
up.s2m.none()
// Here we infer how many bytes our ram need to be, by looking at the memory␣
,→mapping of the connected masters
val bytes = up.ups.map(e => e.mapping.value.highestBound - e.mapping.value.
,→lowerBound + 1).max.toInt
// Then we finaly generate the regular hardware
val logic = new tilelink.Ram(up.bus.p.node, bytes)
logic.io.up << up.bus
}
}
CpuFiber 示例
CpuFiber 是一个虚拟的主端集成的示例。
import spinal.lib.bus.tilelink
import spinal.core.fiber.Fiber
class CpuFiber extends Area {
// Define a node facing downward (toward slaves only)
val down = tilelink.fabric.Node.down()
val fiber = Fiber build new Area {
// Here we force the bus parameters to a specific configurations
down.m2s forceParameters tilelink.M2sParameters(
addressWidth = 32,
dataWidth = 64,
// We define the traffic of each master using this node. (one master => one␣
,→M2sAgent)
// In our case, there is only the CpuFiber.
masters = List(
tilelink.M2sAgent(
name = CpuFiber.this, // Reference to the original agent.
// A agent can use multiple sets of source ID for different purposes
// Here we define the usage of every sets of source ID
// In our case, let's say we use ID [0-3] to emit get/putFull requests
mapping = List(
tilelink.M2sSource(
id = SizeMapping(0, 4),
emits = M2sTransfers(
get = tilelink.SizeRange(1, 64), //Meaning the get access can be␣
,→any power of 2 size in [1, 64]
putFull = tilelink.SizeRange(1, 64)
)
)
)
)
)
)
// Lets say the CPU doesn't support any slave initiated requests (memory␣
,→coherency)
down.s2m.supported load tilelink.S2mSupport.none()
// Then we can generate some hardware (nothing usefull in this example)
down.bus.a.setIdle()
down.bus.d.ready := True
}
}
Tilelink 的一个特殊性是,它假设主端不会向未映射的内存空间发出请求。为了让主机识别允许访问哪些内存,可以使用 spinal.lib.system.tag.MemoryConnection.getMemoryTransfers 工具,如下所示:
val mappings = spinal.lib.system.tag.MemoryConnection.getMemoryTransfers(down)
// Here we just print the values out in stdout, but instead you can generate some␣
,→hardware from it.
for(mapping <- mappings){
println(s"- ${mapping.where} -> ${mapping.transfers}")
}
如果在 CPU 的纤程中运行此命令,在下面的 soc 中:
val cpu = new CpuFiber()
val ram = new RamFiber()
ram.up at(0x10000, 0x200) of cpu.down
// Create a peripherals namespace to keep things clean
val peripherals = new Area{
// Create a intermediate node in the interconnect
val access = tilelink.fabric.Node()
access at 0x20000 of cpu.down
val gpioA = new GpioFiber()
gpioA.up at 0x0000 of access
val gpioB = new GpioFiber()
gpioB.up at 0x1000 of access
}
会得到:
- toplevel/ram_up mapped=SM(0x10000, 0x200) through=List(OT(0x10000)) -> GF
- toplevel/peripherals_gpioA_up mapped=SM(0x20000, 0x1000)␣
,→through=List(OT(0x20000), OT(0x0)) -> GF
- toplevel/peripherals_gpioB_up mapped=SM(0x21000, 0x1000)␣
,→through=List(OT(0x20000), OT(0x1000)) -> GF
• “through=”指定了到达目标所需的地址转换链。
• “SM”表示 SizeMapping(address, size)
• “OT”表示 OffsetTransformer(offset)