做事High Level
在前面的五篇文章中,对于Axi4总线互联的所有设计细节都已经做了介绍。那么剩下的就是集成工作了。再来回顾整个总线互联架构:
对于完整的IP来讲,我们如果想要做成一个高度参数化的IP,就需要解决几个问题:
slave端口个数可配置及地址可分配。
master端口个数可配置。
master访问的slave端口可指定
master端口与slave端口的拓扑连接关系指定
pipeline插入(可参照文章《与其在一起纠缠,不如“一别两宽” 》)
只有高度的参数化才能为逻辑设计增色,而SpinalHDL在这方面的优势可谓得天独厚。Axi4CrossbarFactory的设计思路不可谓不精彩。Axi4CrossbarFactory真个设计可谓是纯软件逻辑,通过软件的灵活性我们可以轻松定制总线互联IP。
addSlave
对于slave端口,我们要做的就是向Axi4CrossbarFactory注册每个slave端口并附带上其地址段分区。Axi4CrossbarFactory提供了两个方法用于注册slave端口:
def addSlave(axi: Axi4Bus,mapping: SizeMapping) : this.type
def addSlaves(orders : (Axi4Bus,SizeMapping)*) : this.type
这里核心是addSlave函数:
在注册slave端口时倘若我们传入的是axi4总线,那么会将axi4总线转换成Axi4ReadOnly和Axi4WriteOnly总线,同时将映射信息存放至axi4SlaveToReadWriteOnly映射表中,而slave端口的地址映射信息将会存放至slaveConfigs映射表。
addConnection
addConnection用途在于建立mster端口与slave端口之间的映射关系。Axi4CrossbarFactory提供了三种方法:
def addConnection(axi: Axi4Bus,slaves: Seq[Axi4Bus]) : this.type
def addConnection(order: (Axi4Bus,Seq[Axi4Bus])) : this.type
def addConnections(orders : (Axi4Bus,Seq[Axi4Bus])*) : this.type
这里着重点在于第一个函数:
def addConnection(axi: Axi4Bus,slaves: Seq[Axi4Bus]) : this.type = {
val translatedSlaves = slaves.map(_ match{
case that : Axi4 => axi4SlaveToReadWriteOnly(that)
case that : Axi4Bus => that :: Nil
}).flatten
axi match {
case axi : Axi4 => {
addConnection(axi.toReadOnly().setCompositeName(axi, "readOnly", true),translatedSlaves.filter(!_.isInstanceOf[Axi4WriteOnly]))
addConnection(axi.toWriteOnly().setCompositeName(axi, "writeOnly", true),translatedSlaves.filter(!_.isInstanceOf[Axi4ReadOnly]))
}
case axi : Axi4WriteOnly => {
translatedSlaves.filter(!_.isInstanceOf[Axi4ReadOnly]).foreach(slavesConfigs(_).connections += Axi4CrossbarSlaveConnection(axi))
masters += axi
}
case axi : Axi4ReadOnly => {
translatedSlaves.filter(!_.isInstanceOf[Axi4WriteOnly]).foreach(slavesConfigs(_).connections += Axi4CrossbarSlaveConnection(axi))
masters += axi
}
case axi : Axi4Shared => {
translatedSlaves.foreach(slavesConfigs(_).connections += Axi4CrossbarSlaveConnection(axi))
masters += axi
}
}
this
}
对于每个待连接的slave端口,这里转换成列表存放于translatedSlaves中。之所以将slave端口转换成列表的形式,在于当我们传入的slave端口是axi4时,在addSlave函数中将其转换为了Axi4ReadOnly和Axi4WriteOnly总线,其映射关系以列表形式存放在了 axi4SlaveToReadWriteOnly中。随后根据端口的类型将master与slave端口之间的连接关系存放在slave端口的slaveConfigs映射的connections中。而master端口则注册存放在一个Arrayuffer中。
addPipeLine
Axi4CrossbarFactory中提供了下面几个方法用于添加pipeline:
def addPipelining(axi : Axi4Shared)(bridger : (Axi4Shared,Axi4Shared) => Unit): this.type
def addPipelining(axi : Axi4ReadOnly)(bridger : (Axi4ReadOnly,Axi4ReadOnly) => Unit): this.type
def addPipelining(axi : Axi4WriteOnly)(bridger : (Axi4WriteOnly,Axi4WriteOnly) => Unit): this.type
def addPipelining(axi : Axi4)(ro : (Axi4ReadOnly,Axi4ReadOnly) => Unit)(wo : (Axi4WriteOnly,Axi4WriteOnly) => Unit): this.type
这里最后一个函数值得注意:
def addPipelining(axi : Axi4)(ro : (Axi4ReadOnly,Axi4ReadOnly) => Unit)(wo : (Axi4WriteOnly,Axi4WriteOnly) => Unit): this.type ={
val b = axi4SlaveToReadWriteOnly(axi)
val rAxi = b(0).asInstanceOf[Axi4ReadOnly]
val wAxi = b(1).asInstanceOf[Axi4WriteOnly]
addPipelining(rAxi)(ro)
addPipelining(wAxi)(wo)
this
}
当传入的参数是Axi4总线时,其会查询axi4SlaveToReadWriteOnly找到映射的ReadOnly总线与WriteOnly总线,那这里就限制了其只能用在slave端口而不能用在master端口(axi4SlaveToReadWriteOnly注册是发生在addSlave端口中)。
这几个addPipelining方法只能用来添加decoder入口和arbiter出口的总线,而对于decoder出口和arbiter入口之间的拓扑互联则无能为力。
build
有了前面的注册,那么最后一步就是建立整个IP的生成和拓扑互联了。这里以ReadOnly的处理为例:
对于decoder部分:
对于每个master端口,首先遍历所有的slave端口查询该master可访问的端口并保存在列表中,随后根据slaves列表信息声称该master端口对应的Axi4ReadOnlyDecoder IP,并将output端口与slaves端口建立一一映射关系存放在masterToDecodedSlave映射中。值得注意的是按照IP的默认参数在建立映射是会在aw,ar通路上插入一级pipeline,而w,b,r通路则没有,也就意味着在decoder和arbiter之间这些通路是直连的。随后若用户在master端口上若有调用addpipelining则会插入相应用户指定的逻辑。
对于arbiter部分:
arbiter在处理上只有访问该slave端口的个数大于2时才会例化Axi4ReadOnlyArbiter,所有Arbiter输入端口通过前面的masterToDecodedSlave映射表来获取拓扑连接关系进行连接。随后若用户在slave端口上若有调用addpipelining则会插入相应用户指定的逻辑。
至此,完成了整个IP的例化。
example
这里给出一个example。假定IP有两个master端口和两个slave端口,每个slave端口占1G空间:
写在最后
通过该系列,完整解析了通过SpinalHDL来实现Axi4总线互联的设计思路及代码技巧。整个代码设计实现不过百行,而其中所穿插的设计思路是值得所学习和思考的,目前也就只能基于SpinalHDL这类设计语言才能够达到如此精炼与高效。
原作者:玉骐