编 者 按
无论是FPGA还是ASIC,系统设计中总会存在配置寄存器总线的使用,我们会将各种功能、调试寄存器挂载在寄存器总线上使用。在SpinalHDL中,BusIf那套总线模型库写的还是相当不错的,能够同时生成对应的代码和文档(在公司里也做过一些修改,能够直接生成整个系统的寄存器文档而不仅仅是单个模块的寄存器文档)。今天就手把手来基于SpinalHDL中的BusIf来看如何根据自己设计中的寄存器配置总线定义来生成一套寄存器配置模版。
HPI总线
今天以下面一套简单的寄存器总线为例进行设计:
case class Hpi(addrWidth:Int,dataWidth:Int,useStrb:Boolean) extends Bundle with IMasterSlave {
val wr,rd=Bool()
val addr=UInt(addrWidth bits)
val wdata=Bits(dataWidth bits)
val strb= useStrb generate (Bits(dataWidth/8 bits))
val rvalid=Bool()
val rdata=Bits(dataWidth bits)
override def asMaster(): Unit = {
out(wr,rd,addr,wdata)
in(rvalid,rdata)
if(useStrb) out(strb)
}
}
HPI总线很简单,wr用于标识写指令、rd用于标识读指令。addr用于指示读/写地址。wdata用于输入待写入的数据。strb如果使能则用于标识对应写指令的位选信号。而rvalid则用于指示读返回数据有效,rdata则表示读返回数据(这里就不画时序图了)。
设计一套自己的HpiInterface
针对上面的HPI总线,这里我们基于BusIf设计一套自己的配置寄存器总线模板。
BusIf都做了什么
对于寄存器配置总线,其总线信号定义,协议交互定义总是各有千秋的,BusIf无法对寄存器配置总线的这些定义做提前预知。然而对于寄存器配置总线的用途,则相对明确,无非就是实现不同数据类型(像UVM中定义的寄存器模型)的读写操作。那么,当我们基于BusIf定义一套自己的配置寄存器模板时,所需要做的就无非是:
- 实现配置寄存器交互协议的逻辑实现
- 告诉BusIf如何什么情况下触发了某个寄存器的读/写操作
HpiInterface
这里先贴上一个完整的代码,随后进行逐行解析
这里我们定义的寄存器总线相对来讲较为简单,故只有30~31行是用来进行配置寄存器总线协议时序的处理的。
line3
override def getModuleName: String = moduleName.name
利用隐式参数获取模块名,BusIf中并无显示使用。
line5~6
override def writeAddress(): UInt = bus.addroverride def readAddress(): UInt = bus.addr
这里用来告知BusIf配置寄存器总线的写地址,读地址分别是什么,在HPI总线中均为bus.addr
line8~9
override def readHalt(): Unit = {}override def writeHalt(): Unit = {}
这里用来填写发生读阻塞或者写阻塞时的响应动作,HPI总线中没有阻塞的概念,直接不做任何处理即可(由于配置寄存器总线接口层协议我们会自己实现,BusIf中当前版本内部无使用的地方,故直接填空即可)。
line11
override def busDataWidth: Int = bus.dataWidth
这里用来告知BusIf配置寄存器总线的数据位宽
line13~18
override val withStrb: Boolean = bus.useStrb
val wstrb: Bits = withStrb generate(Bits(strbWidth bit))
val wmask: Bits = withStrb generate(Bits(busDataWidth bit))
val wmaskn: Bits = withStrb generate(Bits(busDataWidth bit))
initStrbMasks()
if(bus.useStrb){wstrb:=bus.strb}
这里用于设置配置寄存器总线是否有使用掩码功能。line13用于告知BusIf是否使用掩码,而line14~17则是一套针对掩码的BusIf设置(直接copy即可)。在line18行如果使能了掩码功能,则通过用bus.strb来驱动 wstrb来告知BusIf配置寄存器总线对应的写掩码。
line20~26
override val askRead: Bool = bus.rd
override val askWrite: Bool = bus.wr
override val doWrite: Bool = bus.wr
override val doRead: Bool = bus.rd
override val readData: Bits = Bits(bus.dataWidth bits)
override val writeData: Bits =bus.wdata
override val readError: Bool = Bool()
- askRead:告知BusIf什么情况下配置寄存器产生了读请求(如果总线类型是Stream那种握手型的,则只需填valid即可,可参照regif下的AxiLite4BusInterface)。在当前版本中,askRead在BusIf中并未有使用
- askWrite:告知BusIf什么情况下配置寄存器产生了写请求,同上
- doWrite:告知BusIf当前时钟是否出发了寄存器写操作
- doRead:告知BusIf当前时钟是否发生了寄存器读操作
- readData:为BusIf声明一个对应总线数据位宽的信号,BusIf会将读结果返回到当前信号上。
- writeData:告知BusIf如果发生写数据,写入的数据是什么
- readError: 声明一个Bool类型,BusIf会将是否有读错误发生通过该信号进行表示。
line28
setReservedAddressReadValue(BigInt("deaddead",16))
此处用于设置当总线读了未使用的地址时,应当返回何值。当然也可以不必在这里进行统一设置,可以在真正的例化位置为每个模块设置一个不同的值。
line30~31
bus.rdata:=RegNext(readData)bus.rvalid:=RegNext(bus.rd,False)
该处则用于处理Hpi总线的协议时序,我们仅需处理接口层面上的时序即可。
使用HpiInterface
定义好之后,使用就和regif文档里的例子一样了,下面给出一个例子:
这里定义了data0,data1,data2三个寄存器。data0,data1可读可写,data2只读。
值得注意的是这里针对data0,data1采用了不同形式的API,对于data0,使用field来注册会生成reg0_data0,其是带有复位处理的,这里不希望其有复位逻辑,故这里添加了removeInitAssignments()
DIY时间
来看下在BusIf中针对读逻辑是怎么处理的:
从FPGA的角度来看的话,这个askRead判断是没有必要,徒增延迟,看起来不那么优雅,而且在Hpi总线使用时往往是要求地址按位宽对齐的,所以完全可以将地址判断抹去低比特,由于这里readGenerator定义成了private,外部无法重载,这里给出一个在HpiInterface中进行DIY的例子:
val discardAddrWidth = log2Up(busDataWidth / 8)
def hitDoWriteOverride() = {
orderdRegInsts.foreach(regInst => {
regInst.hitDoWrite.removeAssignments()
regInst.hitDoWrite := writeAddress()(bus.addrWidth - 1 downto discardAddrWidth) === regInst.addr / (busDataWidth / 8) && doWrite
})
}
def reworkReadGenerate() = {
readError.removeAssignments()
readData.removeAssignments()
switch(readAddress()(bus.addrWidth - 1 downto discardAddrWidth)) {
orderdRegInsts.foreach(regInst => {
if (!regInst.allIsNA) {
is(regInst.addr / (busDataWidth / 8)) {
readData := regInst.readBits
readError := Bool(regInst.haveWO)
}
}
})
default {
readData := getReservedAddressReadValue
readError := True
}
}
}
component.addPrePopTask(() => {
hitDoWriteOverride()
reworkReadGenerate()
})
这里对读和写均做了一些优化。对于写操作进行地址判定时将会抹去地址相应地比特的判断。而对于读操作,也会抹去相应地址位,同时也删除了askRead的判断。
感兴趣的小伙伴可自行扩展定制,比如将HPI的读返回拆成两排分级译码以获取更好的时序等等。
☆ END ☆
作者:玉骐
原文链接:Spinal FPGA
微信公众号:
推荐阅读
更多SpinalHDL技术干货请关注[Spinal FPGA]欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。