原文链接:
当Spinal HDL遇到TCL
Spinal HDL是一门基于Scala的硬件描述语言,它可以借助Scala语言的高级特性帮助我们更快的完成开发任务。不过需要注意的是Spinal HDL是HDL而不是HLS,我们在开发的时候仍然需要做到心中有电路这样才能写出更好的代码,因为Spinal HDL本质上还是在描述电路。
在使用Spinal HDL的时候不可避免的会使用别人的IP核,这个时候可以采用BlackBox进行融合该IP核。但是比如在使用xilinx的IP的时候,我们只去在SpinalHDL中定义了该BlackBox,那么生成Verilog,VHDL或SV之后,到vivado里面进行综合的时候,我们仍然需要去IP Catalog里面找到这个IP核,并且去指定相应的参数来集成这个IP核到我们的工程里面,这个过程还是比较繁琐的,一旦中间有个参数忘记设置了,那么又要花时间去调试。又比如我们上版的时候需要ILA去调试,虽然SpinalHDL生成的Verilog等可读性还是很好的,但是我们一个个去找要监测的信号,然后例化ILA。有这个时间例化的时间,摸鱼他不爽吗?
那么下面就以Xilinx的加法器这个IP为例,说明怎么生成TCL脚本自动例化IP。首先看一下Add这个IP核有哪些参数
主要的参数是A,B的位宽,是unsigned还是signed,以及是加法还是减法,latency,使用lut还是dsp资源等。
那么我们可以在SpinalHDL里面先定义一个配置类,来描述这些配置信息:
object AddSubConfig {
val signed = "signed"
val unsigned = "Unsigned"
val dsp = "DSP"
val lut = "LUT"
val subtract = "Subtract"
val add = "Add"
}
之后定义一个BlackBox用于描述这个IP核的端口信息:
class AddSub(A_WIDTH: Int, B_WIDTH: Int, S_WIDTH: Int, A_TYPE: String, B_TYPE: String, clockDomain: ClockDomain, componentName: String)
extends BlackBox {
val io = new Bundle {
val A = if (A_TYPE == MulConfig.signed) {
in SInt (A_WIDTH bits)
} else {
in UInt (A_WIDTH bits)
}
val B = if (B_TYPE == MulConfig.signed) {
in SInt (B_WIDTH bits)
} else {
in UInt (B_WIDTH bits)
}
val S = if (A_TYPE == MulConfig.signed || B_TYPE == MulConfig.signed) {
out SInt (S_WIDTH bits)
} else {
out UInt (S_WIDTH bits)
}
val CLK = in Bool()
}
noIoPrefix()
this.setDefinitionName(componentName)
mapClockDomain(clockDomain, io.CLK)
}
之后定义一个genTcl的函数用于生成相应的TCL脚本
private def genTcl(A_WIDTH: Int, B_WIDTH: Int, P_WIDTH: Int, A_TYPE: String, B_TYPE: String, PIPELINE_STAGE: Int, RESOURCES_TYPE: String,ADD_MODE:String,componentName:String): Unit ={
import java.io._
val createAddCmd = s"set addSubExit [lsearch -exact [get_ips $componentName] $componentName]\n" +
s"if { $$addSubExit <0} {\n" +
s"create_ip -name c_addsub -vendor xilinx.com -library ip -version 12.0 -module_name $componentName\n" +
s"}\n"
val tclHeader = new PrintWriter(new File(s"generate$componentName.tcl"))
tclHeader.write(createAddCmd)
tclHeader.write(s"set_property -dict [list ")
tclHeader.write(s"CONFIG.A_Width {$A_WIDTH} ")
tclHeader.write(s"CONFIG.A_Type {$A_TYPE} ")
tclHeader.write(s"CONFIG.B_Width {$B_WIDTH} ")
tclHeader.write(s"CONFIG.B_Type {$B_TYPE} ")
tclHeader.write(s"CONFIG.CE {false} ")
tclHeader.write(s"CONFIG.Add_Mode {$ADD_MODE} ")
// tclHeader.write(s"CONFIG.B_Value {false} ")
val res = if(RESOURCES_TYPE == AddSubConfig.dsp){
"DSP48"
} else {
"Fabric"
}
tclHeader.write(s"CONFIG.Implementation {$res} ")
// val w = P_WIDTH - 1
tclHeader.write(s"CONFIG.Out_Width {$P_WIDTH} ")
tclHeader.write(s"CONFIG.Latency {$PIPELINE_STAGE} ")
tclHeader.write(s"] [get_ips $componentName] \n")
tclHeader.close()
}
最后就是把上面两者集成到一块
def apply(A_WIDTH: Int, B_WIDTH: Int, S_WIDTH: Int, A_TYPE: String, B_TYPE: String, PIPELINE_STAGE: Int, RESOURCES_TYPE: String,clockDomain: ClockDomain,ADD_MODE:String,componentName:String,genTclScript:Boolean=true) = {
if(genTclScript){
genTcl(A_WIDTH, B_WIDTH, S_WIDTH, A_TYPE, B_TYPE, PIPELINE_STAGE, RESOURCES_TYPE,ADD_MODE,componentName)
}
val addSub = new AddSub(A_WIDTH, B_WIDTH, S_WIDTH, A_TYPE, B_TYPE,clockDomain,componentName)
addSub
}
下就让我们来测试一下吧:
class testAddSub extends Component{
val io = new Bundle{
val A = in SInt(8 bits)
val B = in UInt(8 bits)
val P = out Vec(SInt(8 bits),5)
}
noIoPrefix()
val mul = Array.tabulate(5){i=>
def gen = {
val m = AddSub(8,8,8,AddSubConfig.signed,AddSubConfig.unsigned,1,AddSubConfig.lut,this.clockDomain,AddSubConfig.add,"add8_8_8",i==0)
m.io.A <> io.A
m.io.B <> io.B
m.io.S <> io.P(i)
}
gen
}
}
object testAddSub extends App {
SpinalVerilog(new testAddSub)
}
可以看到生成一个verilog文件和对应的tcl文件,我们只需要在vivado里面运行这个tcl文件,就会生成相应IP核了,这样只需要封装一次IP核就可以在以后节省我们大量的时间。
这个就是TCL生成的IP核,和我们的预期是一样的。
如需完整代码请在FPGA开源工坊公众号回复SpinalHDL与TCL。