十二 · 2021年10月22日

SpinalHDL——集成你的RTL代码

在当前的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外无差别):

006b74d79f96dcd75b4781ff59e3ea1c.png

指定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代码里面的信号均可获取到:

12ea54c7d684feec946f29f003b87e21.png

END

作者:玉骐
原文链接:https://mp.weixin.qq.com/s/kfb43YpBd_30Qts1pTIgig
微信公众号:
 title=

推荐阅读

更多SpinalHDL技术干货请关注Spinal FPGA专栏。
推荐阅读
关注数
1578
内容数
131
用SpinalHDL提升生产力
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息