在当前的RTL设计中,国内仍以Verilog/VHDL为主,且不可否认未来或许很长一段时间仍旧是以Verilog/VHDL为主。SpinalHDL作为一门新的硬件描述语言,其充分考虑了这一点,也为我们在SpinalHDL的设计中提供了集成现有RTL设计(IP)的渠道——BlackBox。
BlackBox
顾名思义,SpinalHDL将待集成的RTL设计当作一个黑盒对待,不关心内部的设计,只关心顶层接口及parameter参数(这也是我们在RTL里例化IP时常用的)。我们来看SpinalHDL-doc给出的example:
class Ram_1w_1r(wordWidth: Int, wordCount: Int) extends BlackBox {
// SpinalHDL will look at Generic classes to get attributes which
// should be used as VHDL generics / Verilog parameters
// You can use String, Int, Double, Boolean, and all SpinalHDL base
// types as generic values
val generic = new Generic {
val wordCount = Ram_1w_1r.this.wordCount
val wordWidth = Ram_1w_1r.this.wordWidth
}
// Define IO of the VHDL entity / Verilog module
val io = new Bundle {
val clk = in Bool
val wr = new Bundle {
val en = in Bool
val addr = in UInt (log2Up(wordCount) bit)
val data = in Bits (wordWidth bit)
}
val rd = new Bundle {
val en = in Bool
val addr = in UInt (log2Up(wordCount) bit)
val data = out Bits (wordWidth bit)
}
}
// Map the current clock domain to the io.clk pin
mapClockDomain(clock=io.clk)
}
整个代码里做了三件事:参数声明、端口声明,时钟域映射。
参数声明
在上例中,Ram_1w_1r IP包含两个参数wordCount、wordWidth,在SpinalHDL里需要将参数“透传”给IP用于在例化时使用,在SpinalHDL里通过Generic来实现,它提供了两种方式(个人更倾向第二种):
val generic = new Generic {
val wordCount = Ram.this.wordCount
val wordWidth = Ram.this.wordWidth
}
// OR
addGeneric("wordCount", wordWidth)
addGeneric("wordWidth", wordWidth)
}
端口声明
端口声明无特别指出,仅仅是将待集成的IP的端口进行重新声明即可,有一点值得注意的是在上例中例化时端口名会带有“io\\_”前缀,欲避免该问题可添加下述语句:
noIoPrefix()
时钟域映射
在集成IP时需显示映射IP的时钟与复位信号,SpinalHDL提供了mapClockDomain、mapCurrentClockDomain两个函数用于时钟映射,mapClockDomain参数列表如下(mapCurrentClockDomain除了不包含clockDomain外无差别):
指定IP代码路径
SpinalHDL集成Verilator仿真器仿真接口,在对带RTL IP的代码进行仿真时,需指定RTL IP代码的路径,SpinalHDL提供了addRTLPath()函数用于集成RTL IP:
class MyBlackBox() extends Blackbox {
val io = new Bundle {
val clk = in Bool
val start = in Bool
val dIn = in Bits(32 bits)
val dOut = out Bits(32 bits)
val ready = out Bool
}
// Map the clk
mapCurrentClockDomain(io.clk)
// Remove io_ prefix
noIoPrefix()
// Add all rtl dependencies
addRTLPath("./rtl/RegisterBank.v") // Add a verilog file
addRTLPath(s"./rtl/myDesign.vhd") // Add a vhdl file
addRTLPath(s"${sys.env("MY_PROJECT")}/myTopLevel.vhd") // Use an environement variable MY_PROJECT (System.getenv("MY_PROJECT"))
}class MyBlackBox() extends Blackbox {val io = new Bundle {val clk = in Boolval start = in Boolval dIn = in Bits(32 bits)val dOut = out Bits(32 bits)val ready = out Bool}// Map the clkmapCurrentClockDomain(io.clk)// Remove io_ prefixnoIoPrefix()// Add all rtl dependenciesaddRTLPath("./rtl/RegisterBank.v") // Add a verilog fileaddRTLPath(s"./rtl/myDesign.vhd") // Add a vhdl fileaddRTLPath(s"${sys.env("MY_PROJECT")}/myTopLevel.vhd") // Use an environement variable MY_PROJECT (System.getenv("MY_PROJECT"))}
IP 例化
对于IP的例化在SpinalHDL里无差别,如下例所示:
// Create the top level and instantiate the Ram
class TopLevel extends Component {
val io = new Bundle {
val wr = new Bundle {
val en = in Bool
val addr = in UInt (log2Up(16) bit)
val data = in Bits (8 bit)
}
val rd = new Bundle {
val en = in Bool
val addr = in UInt (log2Up(16) bit)
val data = out Bits (8 bit)
}
}
// Instantiate the blackbox
val ram = new Ram_1w_1r(8,16)
// Connect all the signals
io.wr.en <> ram.io.wr.en
io.wr.addr <> ram.io.wr.addr
io.wr.data <> ram.io.wr.data
io.rd.en <> ram.io.rd.en
io.rd.addr <> ram.io.rd.addr
io.rd.data <> ram.io.rd.data
}
object Main {
def main(args: Array[String]): Unit = {
SpinalVhdl(new TopLevel)
}
}
个人改进
在上例中,20~25行仍像在写RTL代码时进行连线,如果这个IP在整个工程例使用一次还好,如果调用很多次岂不是又回到Verilog里成了“连线工程师”?
解决这一麻烦的方式可借助Scala的伴生对象。下面给出一个小的example,在SpinalHDL例例化一个带层次结构的RTL代码(RTL代码本身没什么意义,主要展示带层次结构的RTL代码在SpinalHDL中设计仿真的实现)。
RTL代码分两个文件:
module add#(
parameter dataWidth=8
)(
input clk,
input rst,
input [dataWidth-1:0] data1,
input [dataWidth-1:0] data2,
output reg [dataWidth-1:0] sum
);
always @(posedge clk)begin
if(rst)
sum<=0;
else
sum<=data1+data2;
end
endmodule
module addTop#(parameter dataWidth=8)(
input clk,
input rst,
input [dataWidth-1:0] data1,
input [dataWidth-1:0] data2,
input [dataWidth-1:0] data3,
input [dataWidth-1:0] data4,
output[dataWidth-1:0] sum1,
output [dataWidth-1:0] sum2
);
add #(
.dataWidth(dataWidth)
)add1(
.clk(clk),
.rst(rst),
.data1(data1),
.data2(data2),
.sum(sum1)
);
add #(
.dataWidth(dataWidth)
)add2(
.clk(clk),
.rst(rst),
.data1(data3),
.data2(data4),
.sum(sum2)
);
endmodule
SpinalHDL IP集成:
object addTop{
def apply(dataWidht: Int)(data1:UInt,data2:UInt,data3:UInt,data4:UInt,sum1:UInt,sum2:UInt): addTop ={
val add=new addTop(dataWidht)
add.io.data1<>data1
add.io.data2<>data2
add.io.data3<>data3
add.io.data4<>data4
add.io.sum1<>sum1
add.io.sum2<>sum2
add
}
}
class addTop(dataWidht:Int) extends BlackBox{
addGeneric("dataWidth",dataWidht)
val io=new Bundle{
val clk=in Bool
val rst=in Bool
val data1=in UInt(dataWidht bits)
val data2=in UInt(dataWidht bits)
val data3=in UInt(dataWidht bits)
val data4=in UInt(dataWidht bits)
val sum1=out UInt(dataWidht bits)
val sum2=out UInt(dataWidht bits)
}
noIoPrefix()
mapClockDomain(clock = io.clk,reset = io.rst)
addRTLPath("./src/main/IP/addTop.sv")
addRTLPath("./src/main/IP/add.sv")
}
class addSpinal(dataWidht:Int) extends Component {
val io=new Bundle{
val data1=in UInt(dataWidht bits)
val data2=in UInt(dataWidht bits)
val data3=in UInt(dataWidht bits)
val data4=in UInt(dataWidht bits)
val sum1=out UInt(dataWidht bits)
val sum2=out UInt(dataWidht bits)
}
noIoPrefix()
val sum=addTop(dataWidht)(io.data1,io.data2,io.data3,io.data4,io.sum1,io.sum2)
}
object addInst extends App{
SpinalConfig(
defaultConfigForClockDomains = ClockDomainConfig(resetActiveLevel = HIGH,resetKind = SYNC)
).generateSystemVerilog(new addSpinal(8))
}
在上述SpinaHDL代码例,定义了addTop类用于集成Verilog 代码,同时定义伴生对象addTop,用于在IP例化时使用。伴生对象中的apply函数里进行IP端口连线,例化时仅需像40行所示将端口放进参数列表里即可。生成的RTL代码如下:
// Generator : SpinalHDL v1.4.1 git head : 99d6d471af204b6d7d9f63fae58757e9d3c7b944
// Component : addSpinal
module addSpinal (
input [7:0] data1,
input [7:0] data2,
input [7:0] data3,
input [7:0] data4,
output [7:0] sum1,
output [7:0] sum2,
input reset,
input clk
);
wire [7:0] sum_sum1;
wire [7:0] sum_sum2;
addTop #(
.dataWidth(8)
) sum (
.clk (clk ), //i
.rst (reset ), //i
.data1 (data1[7:0] ), //i
.data2 (data2[7:0] ), //i
.data3 (data3[7:0] ), //i
.data4 (data4[7:0] ), //i
.sum1 (sum_sum1[7:0] ), //o
.sum2 (sum_sum2[7:0] ) //o
);
assign sum1 = sum_sum1;
assign sum2 = sum_sum2;
endmodule
同时做仿真验证时所有的RTL IP代码里面的信号均可获取到:
END
作者:玉骐
原文链接:https://mp.weixin.qq.com/s/kfb43YpBd_30Qts1pTIgig
微信公众号:
推荐阅读
更多SpinalHDL技术干货请关注Spinal FPGA专栏。