对于IC开发工程师我们可能都有这样的痛苦经历,经常需要手动为不同工艺库替换Mem。有些团队的MemWrapper可能是有专门的脚本来生成,内部会用宏来区分不同工艺,相对来说还比较省事一点,即便这样MemWrpper的集成也是需要你手动连线。但是如果你是IP提供商,不同的团队或者公司的MemWrapper的信号命名也各不相同。同样替换Mem是一件极其无聊并且容易出错的事情。
另外我发现SpinalHDL自带的mem类型并不适合直接拿来做例化,所以需要为此创建一个新的解决方案来满足IC设计中这种常见的需求。spinal的mem模型我会放到Blackbox内部做为行为模型,这样在仿真的时候你可以clearBox来使用mem参考模型来仿真,不需要真正的去包含一个memwrap.v文件,这样在前期设计中非常方便。
因此对于设计者来讲,我本来只需要关心这地方例化的Mem类型(双口,真双口,单口),数据位宽,mem深度即可。除此之外工艺相关的事情对设计者应该是透明的。
可以总结为两点诉求
- MemWrap替换不应修改HDL代码, 对开发者透明
- 添加新的Vendor memWrap不应修改Spinal源码,保持前向兼容
因此,对于开发者来讲,只需要知道统一的RAM例化接口,为此为Spinal设计常见的3种MEM类型.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
33
34
35
36
37
38
39
40
41
42
43
44case class Ram1rw(mc: MemConfig, memName: String = "") extends Component with MemWrap{
val io = new Bundle{
val cs, wr = in Bool()
val bwe = mc.genBWE()
val addr = in UInt(mc.aw bits)
val wdata = in Bits(mc.dw bits)
val rdata = out Bits(mc.dw bits)
}
noIoPrefix()
val ram = mc.vendor.build(this)
}
case class Ram1rw(mc: MemConfig, memName: String = "") extends Component with MemWrap{
val io = new Bundle{
val cs, wr = in Bool()
val bwe = mc.genBWE()
val addr = in UInt(mc.aw bits)
val wdata = in Bits(mc.dw bits)
val rdata = out Bits(mc.dw bits)
}
noIoPrefix()
val ram = mc.vendor.build(this)
}
case class Ram2rw(mc: MemConfig, memName: String = "") extends Component with MemWrap{
val io = new Bundle{
val Aclk = in Bool()
val Awr, Acs = in Bool()
val Abwe = mc.genBWE
val Aaddr = in UInt()
val Awdata = in Bits()
val Ardata = out Bits()
val Bclk = in Bool()
val Bwr, Bcs = in Bool()
val Bbwe = mc.genBWE
val Baddr = in UInt()
val Bwdata = in Bits()
val Brdata = out Bits()
}
noIoPrefix()
val ram = mc.vendor.build(this)
}
开发者仅需要关心Ram1rw、Ram1r1w、Ram2rw的例化和集成即可。而它们和Vendor生成的MemWrapper的库的例化连接则应当自动完成。仔细看上面的代码不难发现其中mc.vendor.build(this)
实际上就是通过类似于回调函数的方式来执行vendor的build函数,build函数则完成当前wrap和MemWrapper的连接。
1 | trait Vendor { |
其中build接受不同的参数,这是一个典型设计模式之生成器模式(The builder design pattern),对于Scala来说是非常简单自然,无需额外操作。不同的参数调用不同的vendor.memblackbox实现,比如我们来看下UMC 单口mem wrap的具体实现,我们需要将公共的wrap作为参数传递给厂商的wrapper,厂商的wrapper将自己的信号跟公共的wrap通过创建Build()函数来明确指出连接关系。这点非常重要,只有这样才能保持向后的扩展性,因为spinal不会也不可能把所有厂家的wrapper给你创建好, 也办不到,自己厂家的wrapper也有可能不断的在变化,唯一不变的是公共wrap的信号命名,所以只需要创建Build()函数高速连接关系,设计者在例化RAM对象的时候只需要高速Vendor的名称即可。
1 | package memlib.vendors.umc |
实际例化 传入Vendor信息, Vendor也可以放到全局配置,这样当我们的IP更换不同的vendor是只需要修改配置,不需要修改HDL代码
1 | class CacheCell(c: GnssConfig, name: String = "") extends Component{ |
除了Mem之外,还有一些公共的cell,比如clkgate, clkmux, asyncCell, clkdiv 等等,RTL代码中会显示的例化, 不同的公司或者团队使用的wrapper名称也各不相同,当项目切换时,使用不同的工艺或者lib时,通常在cell-wrapper中通过宏来选择不同的lib。对于IP提供商来说,cell-wrapper的耦合例化,不同公司集成时,可能需要专门的的按照原来的wrapper Name来创建一套wrapper库。总之都是不够优雅。
1 | gate_cell u_deR_cg_cell(.CLK(clk), .TSE(test_mode), .E(deR_gate_en), .ECK(deR_clk)); |
优雅的解决方案是同上面的mem_wrapper 思路一致,HDL代码接受不同的Vendor作为参数来例化不同的wrapper。不管是项目切换还是工艺切换,只需要修改顶层配置的Vendor对象即可。不用修改任何HDL代码。
1 | class clkGate(vendor: Vendor) extends Component{ //创建接口模块 |
SpinalHDL来写HDL时,对于clockgate这种cell的例化并不会像Verilog那样去做,那样会显得非常啰嗦,一个简单的方式就是val newcd = this.clockDomian.gateBy(io.clken) 因此我们需要位clockDomian隐式扩展添加方法 gateBy
,具体的例化在函数内部完成。
1 | implicit class ClockGateExtend(cd: ClockDomain){ |
分频cell, 异步cell都可以用以上方式来实现,对于RTL工程师不需要关心具体例化,只需要在全局定义Vendor对象,或者在IP顶层的配置中定义Vendor对象,例如对于模块Gnss的配置参数,仅仅修改vendor的对象就可以替换所有的底层lib或者mem-lib。
1 | case class GnssConfig(diQ: QFormat = SQ(2, 1), |
vendor对象应该考虑将其放置到SpinalConfig中去,这样的好处是一些commonCell的例化时vendor参数可以通过globalData获取,避免手工例化。