如何使用spinalHDL/chisel帮助我们灵活的开发

NutShell 果壳如何利用chisel开发

接口

写verilog的时候必须要写接口信号,但是使用chisel开发的不同是chisel或者spinalHDL开发的不同在于,接口是一个in或者out拥有方向属性的一个信号。
可以使用Bundle捆绑信号,将接口信号放到同一个对象中。

class CtrlSignalIO extends NutCoreBundle {
  val src1Type = Output(SrcType())
  val src2Type = Output(SrcType())
  val fuType = Output(FuType())
  val fuOpType = Output(FuOpType())
  val rfSrc1 = Output(UInt(5.W))
  val rfSrc2 = Output(UInt(5.W))
  val rfWen = Output(Bool())
  val rfDest = Output(UInt(5.W))
  val isNutCoreTrap = Output(Bool())
  val isSrc1Forward = Output(Bool())
  val isSrc2Forward = Output(Bool())
  val noSpecExec = Output(Bool())  // This inst can not be speculated
  val isBlocked = Output(Bool())   // This inst requires pipeline to be blocked
}

class DataSrcIO extends NutCoreBundle {
  val src1 = Output(UInt(XLEN.W))
  val src2 = Output(UInt(XLEN.W))
  val imm  = Output(UInt(XLEN.W))
}

class RedirectIO extends NutCoreBundle {
  val target = Output(UInt(VAddrBits.W))
  val rtype = Output(UInt(1.W)) // 1: branch mispredict: only need to flush frontend  0: others: flush the whole pipeline
  val valid = Output(Bool())
}

这是NutShell中的Bundle文件的代码,这里面通过定义类,定义了很多接口信号。

class DecodeIO extends NutCoreBundle {
  val cf = new CtrlFlowIO
  val ctrl = new CtrlSignalIO
  val data = new DataSrcIO
}

当我们具体实现某一个模块的接口时就可以直接使用这些信号。
这样的好处就是重复使用了代码,简化了设计。

可以使用new新创建一个对象或者使用继承,继承某个接口这样就不用实现这个代码了。

以总线为例:
IFU单元有以下需求

需求接口
访问内存AXI协议
访问cache内部信号
访问TLB内部信号

IFU只有一套接口如何在不同的需求下访问三个不同的模块?

答案就是抽象,提供统一接口,避免内部额外的信息,为IFU提供一个Bus模块,这个总线模块可以承载连接三个模块的需求。例如叫BaseBus接口,连接TLB和Cache使用BaseBus。访问内存在IFU单元和内存直接添加一个BaseBusToAxi的桥可以转换为AXI协议。这样就可以不改动IFU模块的情况下,连接不同的模块,满足不同的需求。

模块
IFUBaseBus控制流
IDU数据流控制流控制信号流
ISU数据流控制流控制信号流

其实很多地址需要用到相同的接口,通过定义接口类,new这个类就可以直接使用,避免重复定义。但是需要对架构理解要好,不然可能会实现的很乱,浪费很多资源。

接口如何连线?在spinalHDL使用Bundle定义一系列的信号。
class Bundle extends MultiData with Nameable with ValCallbackRec
Bundle 继承 MultiData MultiData继承Data。

Data内部存在以下内容
1、与方向相关函数,设置in,out,flip翻转方向
2、信号线之间的连接
3、类型转换
MultiData是关于多个数据其中包含
def elements: ArrayBuffer[(String, Data)]
他的内部包含多个元素,每个元素使用一个字符串索引,可以叫name

  def find(name: String): Data = {
    val temp = elements.find((tuple) => tuple._1 == name).getOrElse(null)
    if (temp == null) return null
    temp._2
  }
  以find为例,查找元素根据名称,在使用他的作用与.类似

Bundle内部存在自动连线的功能,通过这个功能就可以帮助我们简化连线。

  /** Assign the bundle with an other bundle by name */
  def assignAllByName(that: Bundle): Unit = {
    for ((name, element) <- elements) {
      val other = that.find(name)
      if (other == null)
        LocatedPendingError(s"Bundle assignment is not complete. Missing $name")
      else element match {
        case b: Bundle => b.assignAllByName(other.asInstanceOf[Bundle])
        case _         => element := other
      }
    }
  }

  /** Assign all possible signal fo the bundle with an other bundle by name */
  def assignSomeByName(that: Bundle): Unit = {
    for ((name, element) <- elements) {
      val other = that.find(name)
      if (other != null) {
        element match {
          case b: Bundle => b.assignSomeByName(other.asInstanceOf[Bundle])
          case _         => element := other
        }
      }
    }
  }

  def bundleAssign(that : Bundle)(f : (Data, Data) => Unit): Unit ={
    for ((name, element) <- elements) {
      val other = that.find(name)
      if (other == null) {
        LocatedPendingError(s"Bundle assignment is not complete. $this need '$name' but $that doesn't provide it.")
      }
      else {
        f(element, other)
      }
    }
  }

最后一种应该是Bundle之间使用:=默认调用的连线函数,其余是为我们提供的。Bundle根据名称查找,data对data之间连线。

因此使用Bundle定义接口存在另外一个好处就是自动连线功能。当然如果Bundle不根据功能划分,每个模块之间使用的接口都不太相同,这样很难使用自动连线的功能。因此比较好的方式就是根据功能模块划分,以上内容。

使用Bundle根据功能模块划分模块接口信号有两个好处
1、复用接口信号代码
2、自动连线
这样就可以使用chisel/spinalHDL简化代码

流水线

流水线一般为了满足不同的需求,会做成可配置的,可以有多级的流水线也可以,让流水线的级数少一点。这样就要求从流水线的代码从模块中分离出来,单独实现。

class Frontend_inorder(implicit val p: NutCoreConfig) extends NutCoreModule with HasFrontendIO {
  val ifu  = Module(new IFU_inorder)
  val ibf = Module(new NaiveRVCAlignBuffer)
  val idu  = Module(new IDU)

  def PipelineConnect2[T <: Data](left: DecoupledIO[T], right: DecoupledIO[T],
    isFlush: Bool, entries: Int = 4, pipe: Boolean = false) = {
    // NOTE: depend on https://github.com/chipsalliance/chisel3/pull/2245
    // right <> Queue(left,  entries = entries, pipe = pipe, flush = Some(isFlush))
    right <> FlushableQueue(left, isFlush, entries = entries, pipe = pipe)
  }

  PipelineConnect2(ifu.io.out, ibf.io.in, ifu.io.flushVec(0))
  PipelineConnect(ibf.io.out, idu.io.in(0), idu.io.out(0).fire(), ifu.io.flushVec(1))
  idu.io.in(1) := DontCare

  ibf.io.flush := ifu.io.flushVec(1)
  io.out <> idu.io.out
  io.redirect <> ifu.io.redirect
  io.flushVec <> ifu.io.flushVec
  io.bpFlush <> ifu.io.bpFlush
  io.ipf <> ifu.io.ipf
  io.imem <> ifu.io.imem

  Debug("------------------------ FRONTEND:------------------------\n")
  Debug("flush = %b, ifu:(%d,%d), idu:(%d,%d)\n",
    ifu.io.flushVec.asUInt, ifu.io.out.valid, ifu.io.out.ready, idu.io.in(0).valid, idu.io.in(0).ready)
  Debug(ifu.io.out.valid, "IFU: pc = 0x%x, instr = 0x%x\n", ifu.io.out.bits.pc, ifu.io.out.bits.instr)
  Debug(idu.io.in(0).valid, "IDU1: pc = 0x%x, instr = 0x%x, pnpc = 0x%x\n", idu.io.in(0).bits.pc, idu.io.in(0).bits.instr, idu.io.in(0).bits.pnpc)
}

以果壳的前端为例,
val ifu = Module(new IFU_inorder)
val ibf = Module(new NaiveRVCAlignBuffer)
val idu = Module(new IDU)
这里定义了三个Module分别是取指单元,RVC对齐单元,译码单元

object PipelineConnect {
  def apply[T <: Data](left: DecoupledIO[T], right: DecoupledIO[T], rightOutFire: Bool, isFlush: Bool) = {
    val valid = RegInit(false.B)
    when (rightOutFire) { valid := false.B }
    when (left.valid && right.ready) { valid := true.B }
    when (isFlush) { valid := false.B }

    left.ready := right.ready
    right.bits := RegEnable(left.bits, left.valid && right.ready)
    right.valid := valid //&& !isFlush
  }
}

单独使用了PipeLineConnect来定义流水线。

这样当我们实现有流水和无流水的硬件代码,只需要根据配置信息,来判断使用PipeLine连接还是使用自动连线连接

这里有一个前提就是实现的接口信号可以完成自动连线功能。如果不能完成自动连线功能对于不同的PipeLine,他们的接口连线需要手动实现这样就需要手动连线,实现很多PipeLine模块。

甚至我们可以使用队列,FIFO等模块,而不是PipeLine。实现自动连线就可以使用这样的可选的流水线,FIFO等。因为自动连线的原因,所以我们可以在接口与接口之间添加任意的无关具体信号的代码。比如说我们可以要求信号线带有Valid或者Ready,其余信号无关。使用Valid和Ready完成信号的传递和反压。数据可以直接连接,可以使用流水,可以使用FIFO。一切都根据需求实现即可。

这就是Chisel/spinalHDL带来的一个好处。

代码与逻辑分离

在使用verilog时一般很常用if else,case等语句。如果希望根据需求产生if else,case语句就很难。例如在riscv中,指令集是模块的形式,可选可不选,例如I指令,M指令。我希望配置选择是否添加M指令应该怎么做呢?
在spinalHDL/chisel中可以,在集合中定义每个case中匹配的值和匹配结果。然后传递给调用switch语句。如果我们希望选择M指令,那么就将M指令的匹配值和匹配结果添加到,译码集合中。

在spinalHDL中可以看Mux,MuxList等。我希望返回集合所以扩展了集合实现了MyMuxSeqPara。在经过一次封装就可以直接进行译码了,这样switch的代码和逻辑分离。更容易扩展,删除。

object MyMuxSeqPara{

    def apply[K <: BaseType, T <: Data](addr: K, defaultValue:Seq[T],mappings: Seq[(Any, Seq[T])]): Seq[T] = {
        val map:Seq[(Any, Seq[T])] = (mappings ++ Seq(default -> defaultValue))
        apply(addr ,map)
    }

    def apply[K <: BaseType, T <: Data](addr: K, mappings: Seq[(Any, Seq[T])]): Seq[T] = {
        val result: Seq[T] = mappings(0)._2.map((a) => weakCloneOf(a))
        switch(addr) {
            for ((cond, value) <- mappings) {
                cond match {
                    case product: Product =>
                        is.list(product.productIterator) {
                            result.foreach(d => d := value(result.indexOf(d)))
                        }
                    case `default` =>
                        default {
                            result.foreach(d => d := value(result.indexOf(d)))
                        }
                    case _ =>
                        is(cond) {
                            result.foreach(d => d := value(result.indexOf(d)))
                        }
                }
            }
        }
        result
    }
}


/**
 * @dontName var temp1 = Seq(
 *                      (U"5'b00000" , B"3'b001"),
 *                      (U"5'b00001" , B"3'b010"),
 *                      (U"5'b00010" , B"3'b011"),
 *                      (U"5'b00100" , B"3'b100"),
 *                      (U"5'b01000" , B"3'b101"),
 *                      (U"5'b10000" , B"3'b110")
 *                      )
 * MyMux(sel, temp1)
 * 根据sel于temp1的元素中,第一个元素判断,相等的话返回对应的Seq
 * 这个和MyMuxSeq的区别是这个得到的数据是T
 */
object MyMuxPara{
    def apply[K <: BaseType, T <: Data](addr: K, defaultValue: T, mappings: Seq[(Any, T)]): T = {
        apply(addr, mappings ++ Seq(default -> defaultValue))
    }

    def apply[K <: BaseType, T <: Data](addr: K, mappings: Seq[(Any, T)]): T = {
        MyMuxSeqPara(addr, mappings.map(p => (p._1 -> Seq(p._2))))(0)
    }
}

重复逻辑反复使用

有很多内容会被反复调用,在scala中可以用以下得方式
1、使用trait,定义一些逻辑和内容,然后with继承这个特质得属性和方法,直接使用即可。

2、使用extends,通过继承,子类可以继承父类的属性和方法,并可以在子类中添加新的属性和方法,或者覆盖父类的方法。

3、使用object在 Scala 中,object 是一个特殊的类,它只有一个实例,称为单例对象。相比之下,class 和 case class 是可以创建多个实例的。这意味着,如果我们使用 object 来定义某个功能,那么它就可以直接访问,而不需要实例化该对象,并且我们可以在代码中共享单个实例,从而提高性能和效率。

object LookupTree {
  def apply[T <: Data](key: UInt, mapping: Iterable[(UInt, T)]): T =
    Mux1H(mapping.map(p => (p._1 === key, p._2)))
}

以这个代码为例,object是一个单例类,只有一个实例,可以考虑以下使用场景,1、定义所需要的数据,被不同模块使用,2、定义在object内部实现函数,直接调用就可以实例化某个硬件。例如上述代码,调用它就会生成一个选择器。定义属性,如果是信号线在连线的时候就会出现问题。无法确定连接哪一个信号线。

4、class定义一个类,然后调用他,如果实现的模块存在变量(信号线),或者比较复杂。这时用object因为是单例对象只会创建一个就无法满足我们的需求,这是需要时使用class(case class)会方便一些,例如使用class 继承Bundle,来定义我们所需要的端口。如果使用object实际上只存在一个端口,无法例化到每个模块中。

5、利用scala语法,for,if else这种是最基本的主要是scala中的集合操作。通过集合操作可以定义多个模块,然后非常灵活的连线,实现某个模块。

总之可以利用各种scala的语法,来灵活的使用spinalHDL/chisel中的硬件代码。可以帮助我们化简,可配置,可扩展的生成硬件代码。但是对编程要求,架构理解更高,否则难以发现硬件模块之间的联系。不学习scala,单独的spinalHDL/chisel使用起来与verilog相差无几,虽然写代码没有verilog那么繁琐,但是很多时候没有verilog那么灵活,很多厂商都是提供verilog,VHDL的支持,很多不太常用的模块都需要写黑盒实现,scala为两个硬件描述语言提供了灵魂。要学会使用scala。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Chisel 是一种基于 Scala 语言的硬件描述语言,它支持硬件描述与数字系统设计。与传统的硬件描述语言相比,Chisel 使用了更加现代化的语法结构,使得数字系统的设计更加简洁、灵活Chisel 与数字系统设计 pdf 的关系在于,它可以帮助工程师们在数字系统设计过程中更加高效地进行开发,提高设计的灵活性和可重用性。 Chisel 语言的特点之一是支持硬件生成,这意味着它能够生成 Verilog 或者 VHDL 代码,从而可以与现有的数字系统设计工具兼容,同时也可以很好地与其他硬件描述语言一起协同工作。此外,Chisel 还提供了更加强大的抽象能力,支持参数化的模块化设计,从而可以更加高效地进行硬件设计和验证。 数字系统设计 pdf 是一本介绍数字系统设计原理和实践的教材,它包含了数字系统设计的基本概念、原理和方法。Chisel 与数字系统设计 pdf 的关系在于,它可以作为一种工具,帮助读者更好地理解和应用数字系统设计的知识。通过使用 Chisel 进行硬件描述和设计,读者可以在实践中加深对数字系统设计 pdf 中所学内容的理解,并将其应用到实际的硬件开发项目中去。 总的来说,Chisel 语言与数字系统设计 pdf 有着密切的关系,它们可以相互促进,帮助工程师和学习者更加高效地进行数字系统设计和开发。通过掌握 Chisel 语言并结合数字系统设计 pdf 的知识,可以使数字系统设计的学习和实践变得更加顺畅和高效。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值