SpinalHDL(九):不变应万变-MemWrap适配

SpinalHDL(九):不变应万变-MemWrap适配

对于IC开发工程师我们可能都有这样的痛苦经历,经常需要手动为不同工艺库替换Mem。有些团队的MemWrapper可能是有专门的脚本来生成,内部会用宏来区分不同工艺,相对来说还比较省事一点,即便这样MemWrpper的集成也是需要你手动连线。但是如果你是IP提供商,不同的团队或者公司的MemWrapper的信号命名也各不相同。同样替换Mem是一件极其无聊并且容易出错的事情。

1

另外我发现SpinalHDL自带的mem类型并不适合直接拿来做例化,所以需要为此创建一个新的解决方案来满足IC设计中这种常见的需求。spinal的mem模型我会放到Blackbox内部做为行为模型,这样在仿真的时候你可以clearBox来使用mem参考模型来仿真,不需要真正的去包含一个memwrap.v文件,这样在前期设计中非常方便。

因此对于设计者来讲,我本来只需要关心这地方例化的Mem类型(双口,真双口,单口),数据位宽,mem深度即可。除此之外工艺相关的事情对设计者应该是透明的。

可以总结为两点诉求

  • MemWrap替换不应修改HDL代码, 对开发者透明
  • 添加新的Vendor memWrap不应修改Spinal源码,保持前向兼容

2

因此,对于开发者来讲,只需要知道统一的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
44
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 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
trait Vendor {
//默认使用umc的wrapper
def build(mw: Ram1rw): MemBlackBox = new umc.mbb1rw(mw).Build()
def build(mw: Ram1r1w): MemBlackBox = new umc.mbb1r1w(mw).Build()
def build(mw: Ram2rw): MemBlackBox = new umc.mbb2rw(mw).Build()
def build(mw: Rom): MemBlackBox = new umc.mbbrom(mw).Build()
}

case object Intel extends Vendor{
override def build(mw: Ram1rw): MemBlackBox = new intel.mbb1rw(mw).Build()
override def build(mw: Ram1r1w): MemBlackBox = new intel.mbb1r1w(mw).Build()
override def build(mw: Ram2rw): MemBlackBox = new intel.mbb2rw(mw).Build()
override def build(mw: Rom): MemBlackBox = new intel.mbbrom(mw).Build()
}
//对于不同的公司不同的工艺可以定义不同的Wrapper,前提extends Vendor
case object SPRD extends Vendor{...}
case object FishSemi extends Vendor{...}
case object AMD extends Vendor{...}
case object HuaWei extends Vendor{...}
case object HuaWei10nm extends Vendor{...}
case object ZTE extends Vendor{...}
case object Invida extends 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
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
package memlib.vendors.umc

class mbb1rw(wrap: Ram1rw) extends MemBlackBox{
this.setDefinitionName(s"Mwrapper_rfsp${mc.depth}x${mc.dw}m2b1w1")
val io = new Bundle { iobd =>
val CLK = in Bool
val A = in UInt(mc.aw bits)
val D = in Bits(mc.dw bits)
val CEN = in Bool()
val WEN = in Bool()
val BWEN = mc.genBWE
val Q = out Bits(mc.dw bits)
}

val cd = ClockDomain(io.CLK)

def Build(): MemBlackBox = {
wrap.clockDomain.setSyncWith(cd)
this.io.CLK := wrap.clockDomain.readClockWire
this.io.A := wrap.io.addr
this.io.CEN := ~wrap.io.cs
this.io.WEN := ~wrap.io.wr
if(mc.needBWE){
this.io.BWEN := ~wrap.io.bwe
}
this.io.D := wrap.io.wdata
wrap.io.rdata := this.io.Q

if(mc.withBist) {wrap.io.bist >> io.bist}
if(mc.withScan) {wrap.io.scan >> io.scan}
this
}
noIoPrefix()
....
}

实际例化 传入Vendor信息, Vendor也可以放到全局配置,这样当我们的IP更换不同的vendor是只需要修改配置,不需要修改HDL代码

1
2
3
4
5
6
7
class CacheCell(c: GnssConfig, name: String = "") extends Component{
val io = new Bundle{
val rp = slave(rdPort(c.caw, IQ(c.dw)))
val wp = slave(wrPort(c.caw, IQ(c.dw)))
}
val ram = Ram1rw(MemConfig(dw = 32, depth =512, vendor = HuaWei10nm)
ram << (io.rp merge io.wp)

除了Mem之外,还有一些公共的cell,比如clkgate, clkmux, asyncCell, clkdiv 等等,RTL代码中会显示的例化, 不同的公司或者团队使用的wrapper名称也各不相同,当项目切换时,使用不同的工艺或者lib时,通常在cell-wrapper中通过宏来选择不同的lib。对于IP提供商来说,cell-wrapper的耦合例化,不同公司集成时,可能需要专门的的按照原来的wrapper Name来创建一套wrapper库。总之都是不够优雅。

1
2
3
gate_cell u_deR_cg_cell(.CLK(clk), .TSE(test_mode), .E(deR_gate_en), .ECK(deR_clk));
gate_cell u_deC_cg_cell(.CLK(clk), .TSE(test_mode), .E(deC_gate_en), .ECK(deC_clk));
gate_cell u_deI_cg_cell(.CLK(clk), .TSE(test_mode), .E(deI_gate_en), .ECK(deI_clk));

优雅的解决方案是同上面的mem_wrapper 思路一致,HDL代码接受不同的Vendor作为参数来例化不同的wrapper。不管是项目切换还是工艺切换,只需要修改顶层配置的Vendor对象即可。不用修改任何HDL代码。

1
2
3
4
5
6
7
8
9
class clkGate(vendor: Vendor) extends Component{ //创建接口模块
val io = new Bundle{
val clk = in Bool()
val tse = in Bool()
val cgen = in Bool()
val cgclk = out Bool()
}
vendor.Build(this)
}

SpinalHDL来写HDL时,对于clockgate这种cell的例化并不会像Verilog那样去做,那样会显得非常啰嗦,一个简单的方式就是val newcd = this.clockDomian.gateBy(io.clken) 因此我们需要位clockDomian隐式扩展添加方法 gateBy ,具体的例化在函数内部完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
implicit class ClockGateExtend(cd: ClockDomain){
def gateBy(en: Bool, tse: Bool): ClockDomain = {
val cg = new gate_cell(globalData.getVendor)
cg.io.clk:= cd.readClockWire
cg.io.tse:= tse
cg.io.cgen := en
val cde = ClockDomain(clock = cg.io.cgclk,
reset = cd.readResetWire
)
cde.setSynchronousWith(cd) //gate后的时钟和原时钟应该是同步关系
cde
}
}

分频cell, 异步cell都可以用以上方式来实现,对于RTL工程师不需要关心具体例化,只需要在全局定义Vendor对象,或者在IP顶层的配置中定义Vendor对象,例如对于模块Gnss的配置参数,仅仅修改vendor的对象就可以替换所有的底层lib或者mem-lib。

1
2
3
4
5
6
7
8
9
10
11
12
13
case class GnssConfig(diQ: QFormat = SQ(2, 1),
ddc_diQ: QFormat = SQ(3,2),
ddc_doQ: QFormat = SQ(5,4),
corr_dow: Int = 16,
coh_dow: Int = 10,
noncoh_dow: Int = 16,
baseAddress: Long = 0x00000,
vendor: Vendor = FishSemi, //定义Vendor
withScan: Boolean = false,
withBist: Boolean = false
){
....
}

vendor对象应该考虑将其放置到SpinalConfig中去,这样的好处是一些commonCell的例化时vendor参数可以通过globalData获取,避免手工例化。

评论