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模块的情况下,连接不同的模块,满足不同的需求。
模块 | |||
---|---|---|---|
IFU | BaseBus | 控制流 | |
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。