risc-v中文社区

 找回密码
 立即注册
查看: 3007|回复: 0

[原创] SpinalHDL—像软件调用方法般例化模块

[复制链接]

347

主题

564

帖子

2237

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
2237
发表于 2022-6-6 16:07:00 | 显示全部楼层 |阅读模式
 在编写Verilog代码时最痛苦的事情便是例化模块时端口的连接,这时候的你我便成了连线工程师,本节就在SpinalHDL中如何像软件调用方法那样优雅地例化端口进行探讨。
前言
    习惯了写Verilog的小伙伴们在做大型工程时是否有遇到过连续数天时间化身“连线工程师”去例化模块、为端口赋值连接的场景(关键是这些工作量老板他也不认)。尽管在SystemVerilog中提供了Interface接口的概念,但是从事FPGA的小伙伴都清楚无论是Xilinx的Vivado还是Intel Quartus虽然支持SystemVerilog但远没有做到像软件代码编辑器那般做到自动联想与提示。最近分析一个Intel的大型源码工程其中用到了大量的SystemVerilog中的interface及struct,但自动关联提示做的真是一团糟,导致阅读体验真是差的一匹……    本文以一个简单的加法器的例子来看如何在SpinalHDL中如何避免成为连线工程师。
    加法器端口列表如下所示:
端口名
方向位宽
说明
valid_in
input
1
输入有效标志
data1input
8
输入数据
data2input
8
输入数据
sum
output8
sum_valid
output
1
和有效标志
初阶
    刚开始接触SpinalHDL时这个加法器我们可能会这么来写:

  1. class add(dataWidth:Int) extends Component{
  2.   val validIn=in Bool()
  3.   val data1=in UInt(dataWidth bits)
  4.   val data2=in UInt(dataWidth bits)
  5.   val sum=out UInt(dataWidth bits)
  6.   val sumValid=out Bool()
  7.   sum:=RegNextWhen(data1+data2,validIn)
  8.   sumValid:=RegNext(validIn,False)
  9. }
复制代码


    这里针对端口的实现形式和我们在Verilog中的方式基本相同。那么当我们在例化这个模块时,我们可能会这么来写:
  1. class addInst(dataWidth:Int) extends Component {
  2.   val io=new Bundle{
  3.     val validIn_0=in Bool()
  4.     val data1_0=in UInt(dataWidth bits)
  5.     val data2_0=in UInt(dataWidth bits)
  6.     val sum_0=out UInt(dataWidth bits)
  7.     val sumValid_0=out Bool()

  8.     val validIn_1=in Bool()
  9.     val data1_1=in UInt(dataWidth bits)
  10.     val data2_1=in UInt(dataWidth bits)
  11.     val sum_1=out UInt(dataWidth bits)
  12.     val sumValid_1=out Bool()
  13.   }
  14.   val add0=new add(dataWidth)
  15.   val add1=new add(dataWidth)
  16.   add0.validIn<>io.validIn_0
  17.   add0.data1<>io.data1_0
  18.   add0.data2<>io.data2_0
  19.   add0.sum<>io.sum_0
  20.   add0.sumValid<>io.sumValid_0
  21.   
  22.   add1.validIn<>io.validIn_1
  23.   add1.data1<>io.data1_1
  24.   add1.data2<>io.data2_1
  25.   add1.sum<>io.sum_1
  26.   add1.sumValid<>io.sumValid_1
  27. }
复制代码


    这里例化了两个加法器,可以看到,这里如同我们写Verilog代码般一根根连线,当有众多模块需要去例化时还是蛮痛苦的。
中阶
    在SystemVerilog中提供了Interface的概念用于封装接口,在SpinalHDL中,我们可以借助软件面向对象的思想把接口给抽象出来:

  1. case class sumPort(dataWidth:Int=8) extends Bundle with IMasterSlave{
  2.   case class dataPort(dataWidth:Int=8) extends Bundle{
  3.     val data1=UInt(dataWidth bits)
  4.     val data2=UInt(dataWidth bits)
  5.   }
  6.   val dataIn=Flow(dataPort(dataWidth))
  7.   val sum=Flow(UInt(dataWidth bits))

  8.   override def asMaster(): Unit = {
  9.     master(dataIn)
  10.     slave(sum)
  11.   }
  12. }
复制代码


    这里我们将加法器的端口抽象成sumPort端口。其中包含两个Flow类型:dataIn、sum。并声明当作为master端口时dataIn为master、sum为slave。这样,我们的加法器便可以这么来写:

  1. case class add2(dataWidth:Int=8)extends Component{
  2.   val io=new Bundle{
  3.     val sumport=slave(sumPort(dataWidth))
  4.   }
  5.   io.sumport.sum.payload:=RegNextWhen(io.sumport.dataIn.data1+io.sumport.dataIn.data2,io.sumport.dataIn.valid)
  6.   io.sumport.sum.valid:=RegNext(io.sumport.dataIn.valid,False)
  7. }
复制代码

    而我们在例化时,便可以简洁地例化:
  1. class addInst1(dataWidth:Int) extends Component{
  2.   val io=new Bundle{
  3.     val sumport0=slave(sumPort(dataWidth))
  4.     val sumport1=slave(sumPort(dataWidth))
  5.   }
  6.   val addInst_0=add2(dataWidth)
  7.   val addInst_1=add2(dataWidth)
  8.   io.sumport0<>addInst_0.io.sumport
  9.   io.sumport1<>addInst_1.io.sumport
  10. }
复制代码

    如此我们便能简洁地例化加法器。虽然这里地做法思想和SystemVerilog中地思想基本一致,但好处是我们能够在IDEA中像阅读软件代码那般快速地跳转和定位,相较于厂商工具中那样分析工程地痛苦实在是好太多。
高阶
    在中阶例,我们采用了类似SystemVerilog中Interface及struct概念,但可以发现,我们这里依旧存在连线行为。一个模块例化一次要连线一次,要例化N次还是要……
    在软件代码中,调用一个方法或者模块往往一行代码了事:声明调用函数并将参数放在括号列表里。那么在这里,我们能否像软件调用那样一行代码搞定呢?
    可以的!由于SpinalHDL是基于Scala的,因此我们可以将端口列表当成参数列表来传递。这里我们先为我们的加法器定义一个伴生对象:
  1. object add2{
  2.   def apply(dataWidth: Int,port:sumPort): Unit = {
  3.     val addInst=new add2(dataWidth)
  4.     addInst.io.sumport<>port
  5.   }
  6. }
复制代码

    这里我们为加法器add2定义了一个伴生对象(伴生对象声明为object,名字与类名相同)。并在其中定义了一个apply方法,传入两个参数:位宽dataWidth及端口port,并在apply实现中完成模块例化及端口连接(一次连线,终身使用)。随后我们在例化时便可以像软件调用方法那样例化模块了:
  1. class addInst1(dataWidth:Int) extends Component{
  2.   val io=new Bundle{
  3.     val sumport0=slave(sumPort(dataWidth))
  4.     val sumport1=slave(sumPort(dataWidth))
  5.   }
  6.   add2(dataWidth,io.sumport0)
  7.   add2(dataWidth,io.sumport0)
  8. }
复制代码

    一行代码搞定一个模块的一次例化和端口连接!

本帖来源微信公从号:Spinal FPGA

网页地址:链接

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



Archiver|手机版|小黑屋|risc-v中文社区

GMT+8, 2024-4-25 19:06 , Processed in 0.019046 second(s), 17 queries .

risc-v中文社区论坛 官方网站

Copyright © 2018-2021, risc-v open source

快速回复 返回顶部 返回列表