SpinalHDL(七):逢山开路-寄存器接口

SpinalHDL(七):逢山开路-寄存器接口

目前大量存在手写寄存器接口的情况,对于几十个甚至几百个寄存器的IP,即便你有体力,也不能保证不犯错。
70

存在的问题

  • 纯体力活,工作量繁重
  • 容易引入错误,增加验证工作量
  • 文档和代码不同步,增加软件调试工作量

在这类问题上耗费测试debug时间非常不值当,只要你手写寄存器必然会遇到这了问题,软件调了好半天,最后发现文档跟代码不一致,
人是懒惰的,谁能完全保证代码更文档能同步更新,也肯定会犯错的,所以只能通过验证和测试来覆盖,如果问题能够越早发现,成本越低

这里分享二张图:

一张是某软件产品在各个阶段bug的成本
71

另外一张是IC开发各个阶段bug的成本
72

所以尽可能的在前期解决问题,第一次把事情做对,就是节约成本。

如何优雅的生成寄存器接口

  • 唯一源头, 代码文档严格保存一致
  • 错误检查能力, 输出的代码一定是正确无误的。
  • 一次书写,多种输出,节省工作量

Excel表格自动生成寄存器接口代码

73
为了达到以上效果,很多公司可能都有类似的工具,通过Excel表格或者在网页定制寄存器,然后一键生成verilog代码和Word文档.
74
在生成之前会做严格的检查,比如命名重复,地址重复,字段冲突,复位值位宽不匹配等等,保证生成的代码一定是正确的。

SpinalHDL寄存器接口方案

75
SpinalHDL采用的方法跟Excel表格略有不同,通过书写SpinalHDL代码生成文档和verilog代码,不过原则是一样的,都是源头唯一,一到多的思路

76

寄存器地址自动分配

通过newReg来自动分配,也可以newRegAt(address=0x100),切换锚点,后续的地址将从0x100递增。

1
2
3
4
5
6
7
8
9
10
11
12
class RegBankExample extends Component{
val io = new Bundle{
apb = Apb3(Apb3Config(16,32))
}
val busSlave = BusInterface(io.apb,(0x0000, 100 Byte)
val M_REG0 = busSlave.newReg(doc="REG0")
val M_REG1 = busSlave.newReg(doc="REG1")
val M_REG2 = busSlave.newReg(doc="REG2")

val M_REGn = busSlave.newRegAt(address=0x40, doc="REGn")
val M_REGn1 = busSlave.newReg(doc="REGn1")
}

同时还会检测最大分配地址是否超越总线Mapping分配的空间,保证生成的代码是安全正确的。

77

寄存器字段自动分配

同理通过field来自动分配字段,也可以fieldAt(pos=16)来切换锚点,后续字段从16bit位置递增

1
2
3
4
5
6
7
8
val M_REG0  = busSlave.newReg(doc="REG1")
val fd0 = M_REG0.field(2 bits, RW, doc= "fields 0")
M_REG0.reserved(5 bits)
val fd1 = M_REG0.field(3 bits, RW, doc= "fields 0")
val fd2 = M_REG0.field(3 bits, RW, doc= "fields 0")
//auto reserved 2 bits
val fd3 = M_REG0.fieldAt(pos=16, 4 bits, doc= "fields 3")
//auto reserved 12 bits

同样自动的分配会自动插入reserved,也可以手动插入。同时会保证字段分配不会超过总线数据位宽的最大值。
78

一个中断寄存器实例

中断寄存器场景的有两种方案

    1. 中断使能和中断状态
    1. 中断使能中断MASK和中断状态

第二种类型更为通用,可以让软件打开所有中断,也可以通过Mask来屏蔽部分信号的中断,这里以第二种类型来举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class InterruptRegIf extends Component {
val io = new Bundle{
val psc_done, pth_done, ssc_done, grp_done, scd_done, srch_finish = in Bool()
val interrupt = out Bool()
val apb = slave(Apb3(Apb3Config(16, 32)))
}

val busif = Apb3BusInterface(io.apb, (0x000, 100 Byte))

val M_SRCH_INT_EN = busif.newReg(doc="srch int enable register")
val psc_int_en = M_SRCH_INT_EN.field(1 bits, RW, doc="psc interrupt enable register")
val pth_int_en = M_SRCH_INT_EN.field(1 bits, RW, doc="pth interrupt enable register")
...

val M_SRCH_INT_MASK = busif.newReg(doc="srch int mask register")
val psc_int_mask = M_SRCH_INT_MASK.field(1 bits, RW, doc="psc interrupt mask register")
val pth_int_mask = M_SRCH_INT_MASK.field(1 bits, RW, doc="pth interrupt mask register")
...

val M_SRCH_INT_STATUS = busif.newReg(doc="srch int status register")

val psc_int_status = M_SRCH_INT_STATUS.field(1 bits, RC, doc="psc interrupt status register")
val pth_int_status = M_SRCH_INT_STATUS.field(1 bits, RC, doc="pth interrupt status register")
...

when(io.psc_done && psc_int_en.asBool){psc_int_status(0).set()}
when(io.pth_done && pth_int_en.asBool){pth_int_status(0).set()}
...

io.interrupt := (psc_int_status & pth_int_status & ssc_int_status &
grp_int_status & scd_int_status & srch_finish_status).lsb
}

生成verilog代码的同时生成如下文档
79

你可以发现中断的3个寄存器非常类似,这种方式书写依然有大量的重复。能不能更简单的方式生成中断类型,答案是肯定的,在scala上这一切都不是问题

1
2
3
4
5
6
7
8
9
10
11
12
13
class InterruptRegIf2 extends Component {
val io = new Bundle {
val psc_done, pth_done, ssc_done, grp_done, scd_done, srch_finish = in Bool()
val interrupt = out Bool()
val apb = slave(Apb3(Apb3Config(16, 32)))
}
val busif = Apb3BusInterface(io.apb, (0x000, 100 Byte))

val int = busif.FactoryInterruptWithMask("M_INT",io.psc_done,
io.pth_done,io.ssc_done,io.grp_done,io.scd_done,io.srch_finish)

io.interrupt := int
}

实际上就用了一个函数factorInterrruptWithMask(namePre:String, triggers:
Bool*)就完成了前面那段一模一样的工作。并且也会生成一样的寄存器表格
你只需要把所有触发中断的脉冲信号作为参数丢进去,factorInterruptWithMask会抓取信号名,重新生成3组寄存器,并且正确命名。背后的机理会用到1点宏的知识,后面章节会介绍。

注:该方法会在SpinalHDL1.3.7以后版本中体现

评论