什么是 chisel
关于“什么是
chisel”这个问题,在网上已经有了详尽的讨论:它是硬件描述语言,它是 scala
的包等等。在我的使用范畴下,我认为它是一个编译器,依托于 scala
包的形式存在,有些类似于 LLVM
以库的形式存在的特点。它具有编译器的特点:源语言是 scala,目标语言是
verilog,此外还具有中间表示 FIRRTL。chisel 的工作过程可以理解为:通过在
scala 代码中调用一些 chisel 包中的类和方法,再运行这段 scala
代码,产生可综合的 verilog。
chisel 的优势
chisel 的敏捷性源于它更高层次的抽象以及 scala
语言本身面向对象、函数式的特点。
丰富的数据类型
下面是 chisel 官网
中数据类型的继承关系图:
比较常用的数据类型有
UInt
,SInt
,Bool
,Clock
和 Reset
。此外还可以通过 Vec
和
Bundle
将它们聚集起来,Vec
用于聚集相同的类型,而 Bundle
用于聚集不同的类型。
简单的硬件模型
在 chisel 中的硬件模型只有两种:线网 Wire
和寄存器
Reg
,并且使用了 Reg
声明的变量最终一定会产生寄存器,而不像 verilog 一样不能确定。
声明一个硬件需要将数据类型和硬件模型结合起来使用:
val foo = Wire (UInt (32. W ))val bar = Reg (Bool ())
foo
描述了一个 32 位宽的线,bar
描述了一个
1 位宽的寄存器。需要注意的是,在 chisel
中的时钟和复位信号是隐含的,Reg
默认使用该寄存器所在模块的同步时钟和复位信号。
自定义类型
在编写 verilog
时,常常因为散乱的接口而难以维护,陷入“设计一小时,模块编码一小时,接线两小时”的窘境,chisel
的自定义类型解决了这一问题。
Bundle 继承
通过继承 Bundle
类,用户可以自定义线束类型,将不同类型的线聚集起来:
1 2 3 4 class MyBundle extends Bundle { val src = Vec (2 , UInt (32. W )) val op = Vec (12 , Bool ()) }
定义了由两个 32 位操作数 src
和一个 12 位操作码
op
所组成的线束。在模块中,可以使用自定义类型产生硬件并对其进行连接:
1 2 3 4 val myBundle = Wire (new MyBundle ) myBundle.src(0 ) := 0. U myBundle.src(1 ) := 1. U myBundle.op := "b000100" .U
Module 继承
Module 是电路中基本组成单元,chisel 中的模块同样抽象为 IO
接口和内部逻辑,通过继承 Module 可以自定义模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class MyModule extends Module { val io = IO (new Bundle (){ val in = Input (new MyBundle ) val out = Output (new Bundle (){ val res = UInt (32. W ) }) }) io.out.res := Mux1H (Seq ( io.in.op(0 ) -> io.in.src(0 ) + io.in.src(1 ), io.in.op(1 ) -> io.in.src(0 ) - io.in.src(1 ), ... )) }
定义了一个使用 12 位独热码作为操作码的 alu 模块。其中值
io
定义了该模块的 IO 接口。为了创建它,定义了一个匿名的
Bundle 对象,其中包含 in
和 out
分别定义输入接口和输出接口。定义 IO 接口中的 3 个方法
IO
,Input
和 Output
接受的参数都是 Bundle
的对象,因此在这里可以使用自定义的线束类型。MyModule
中剩余部分是该模块的内部逻辑,这里使用了原语 Mux1H
生成一个独热码选择器。
快速连接模块
chisel 中基本的连接算符是 :=
,信号的流动方向是从右向左。chisel 中还提供了一种 element-wise
的连接算符
<>
,它可以快速连接线束中的同名元素 ,并检查其类型是否相同:
1 2 3 4 5 6 7 8 9 10 11 class MyModuleWrap extends Module { val io = IO (new Bundle (){ val in = Input (new MyBundle ) val out = Output (new Bundle (){ val res = UInt (32. W ) }) }) val myAlu = Module (new MyModule ) myAlu.io<>io }
强大的语法检查
正如上文所提到的,在 chisel
进行编译时,编译器将检查连接左右硬件的数据类型是否相同,若不同则报错,停止编译。实际上,chisel
编译器还做了更多的事情,能够帮助在编码时就检查出一些设计功能无关的错误,节省开发时间。
参数化模块
由于所有类型的定义都是基于 scala
的类,因此可以使用类的初始化参数对模块进行参数化定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class MyBundle (xLen:Int = 32 ) extends Bundle { val src = Vec (2 , UInt (xLen.W )) val op = Vec (12 , Bool ()) }class MyModule (xLen:Int = 32 ) extends Module { val io = IO (new Bundle (){ val in = Input (new MyBundle (xLen)) val out = Output (new Bundle (){ val res = UInt (xLen.W ) }) }) io.out.res := Mux1H (Seq ( io.in.op(0 ) -> io.in.src(0 ) + io.in.src(1 ), io.in.op(1 ) -> io.in.src(0 ) - io.in.src(1 ), ... )) }
在实例化模块时,可以通过改变传入的参数定制不同的模块:
1 2 val myModule1 = Module (new MyModule (16 ))val myModule2 = Module (new MyModule (32 ))
scala 集合类型和函数式编程
集合类型和函数式编程都是 scala 的语言特性,由于 chisel 是 scala
的包,因此可以使用这些特性用于构造硬件生成器。
比如说对于设计 CPU
核而言,一个明显的集合是指令集 ,在 chisel
中,可以将所有指令放到一个查找表中,利用 scala 的
ListLookUp
方法生成译码逻辑:
首先,构造查找表,它使用一个指令作为索引,查找结果是控制信号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 abstract trait DecodeConstants { object CtrlIdx extends scala .Enumeration { val selSrc0, selSrc1, selImm, selWnum, fuType, fuOp, rfWen, illegal = Value } def default : List [UInt ] = List (rs, rt, nan, dzero, alu, sll, f, t) val table: Array [(BitPat , List [UInt ])] }object MIPS32I extends DecodeConstants { val table: Array [(BitPat , List [UInt ])] = Array ( SLL -> rTypeRAlu(sll), SRL -> rTypeRAlu(srl), SRA -> rTypeRAlu(sra), JR -> rTypeJr(jumpr), ADDU -> rTypeRAlu(add), SUBU -> rTypeRAlu(sub), AND -> rTypeRAlu(and), OR -> rTypeRAlu(or), XOR -> rTypeRAlu(xor), NOR -> rTypeRAlu(nor), SLT -> rTypeRAlu(slt), SLTU -> rTypeRAlu(sltu), JAL -> jTypeJal(jump), BEQ -> iTypeBr(beq), BNE -> iTypeBr(bne), ADDIU -> iTypeUAlu(add), LUI -> iTypeUAlu(lui), LW -> iTypeLd(lw), SW -> iTypeSt(sw) ) }
table
定义了所需要的查找表,它的表项是一系列键-值对,键-值对在 scala 中使用
->
表示,左边是键,右边是值。在查找表中,所有键的类型都是
BitPat
,它是 chisel
中的一种用于做二进制串模式匹配的类型;所有值的类型都是
List[UInt]
,即由 UInt
对象组成的列表,用于表示某条指令对应的控制信号的具体取值。
定义控制信号的线束:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class ControlSignals (xLen:Int = 32 ) extends Bundle { val selSrc0 = UInt () val selSrc1 = UInt () val selImm = UInt () val selWnum = UInt () val fuType = UInt () val fuOp = UInt () val rfWen = Bool () val illegal = Bool () def signals = Array ( MIPS32I .CtrlIdx .selSrc0 -> selSrc0, MIPS32I .CtrlIdx .selSrc1 -> selSrc1, MIPS32I .CtrlIdx .selImm -> selImm, MIPS32I .CtrlIdx .selWnum -> selWnum, MIPS32I .CtrlIdx .fuType -> fuType, MIPS32I .CtrlIdx .fuOp -> fuOp, MIPS32I .CtrlIdx .rfWen -> rfWen, MIPS32I .CtrlIdx .illegal -> illegal ) }
这里没有指定信号的位宽,在编译时 chisel
将自动推导出它们的宽度。另外,定义了 signal
方法,它返回一个
Array
,存储这些信号在查找结果中的索引和具体信号值的键-值对,方便在模块中对其进行迭代。
定义译码模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Instruction (xLen:Int = 32 ) extends Bundle { val instr = UInt (xLen.W ) val pc = UInt (xLen.W ) }class ControlIO (xLen:Int = 32 ) extends Bundle { val in = Input (new Instruction (xLen)) val out = Output (new ControlSignals (xLen)) }class Control (xLen:Int = 32 ) extends Module { val io = IO (new ControlIO (xLen)) val ctrlSignals = ListLookup (io.in.instr, MIPS32I .default , MIPS32I .table) io.out.signals.map(x => x._2 := ctrlSignals(x._1.id)) }
译码模块 Control
的输入是一条指令及其对应的
pc
,输出是控制信号。该模块的巧妙之处在于控制信号的生成。
首先,使用 ListLookup
方法对之前构建的查找表进行查找,查找的结果是一个 UInt
组成的 List
,将其存储在 ctrlSignals
中。
然后调用 Array
(io.out.signals
方法返回一个
Array
)的 map
方法进行迭代。该方法传入了一个匿名函数
x => x._2 := ctrlSignals(x._1.id)
,它将
Array
中的每一项取出,使用该项的键索引查找结果,然后将其连接到对应的值上。最终的效果是将查找结果的每一项都连接到了对应的控制信号上。
scala 泛型
由于 scala 泛型的存在,可以对电路进行更高层次的抽象。
比如说对于流水线寄存器而言,可以将其抽象为:
IO 接口:
io.in.data.valid
input
输入数据有效
io.in.data.bits
input
输入数据
io.in.stall
input
流水级暂停
io.in.flush
input
流水级冲刷
io.out.data.valid
output
输出数据有效
io.out.data.bits
outptu
输出数据
内部逻辑(伪代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class StageReg extends Module { val io = IO (new StageRegIO ) val validReg = Reg (Bool ()) val dataReg = Reg (DataType ()) when(reset.asBool || io.in.flush) { validReg := 0. U } .elsewhen(!io.in.stall) { validReg := io.in.data.valid } when (!io.in.stall) { dataReg := io.in.data.bits } io.out.data.valid := validReg io.out.data.bits := dataReg }
在实际使用中,IO 接口和内部逻辑都是不变的,发生变化的是输入数据的类型
DataType
,因此可以对于输入的数据类型使用泛型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class StageRegIn [T <:Bundle ](xLen:Int = 32, dataType: T ) extends Bundle { val stall = Input (Bool ()) val flush = Input (Bool ()) val data = Flipped (Valid (dataType)) }class StageRegOut [T <:Bundle ](xLen:Int = 32, dataType: T ) extends Bundle { val data = Valid (dataType) }class StageRegIO [T <:Bundle ](xLen:Int = 32, dataType: T ) extends Bundle { val in = new StageRegIn (xLen, dataType) val out = new StageRegOut (xLen, dataType) }class StageReg [T <:Bundle ](xLen:Int = 32, dataType: T ) extends Module { val io = IO (new StageRegIO (xLen, dataType)) val validReg = Reg (Bool ()) val dataReg = Reg (dataType) when(reset.asBool || io.in.flush) { validReg := 0. U } .elsewhen(!io.in.stall) { validReg := io.in.data.valid } when (!io.in.stall) { dataReg := io.in.data.bits } io.out.data.valid := validReg io.out.data.bits := dataReg }
在实例化一个流水级寄存器时,可以传入自定义类型以定制寄存器的输入输出数据类型:
1 val myStageReg = Module (new StageReg (32 , new MyBundle ))
chisel 的劣势
学习成本高
chisel 是基于 scala 开发的包,因此使用 chisel 需要先学习
scala。相比于 C 而言,scala
是一门完全不同的语言,其中面向对象、函数式编程的特性是 C
所不具备的,需要从头学习。之所以需要学习这些高级特性,是因为只有使用了这些特性才能充分利用
chisel 的优势,否则 chisel 的使用体验和 verilog 差不多。
对工业级开发流程支持尚不完善
目前,我只把 chisel 当作一个工具,用于使用更少、更易于维护的代码生成
verilog,然后将 verilog 导入 vivado 工程,作为 SOC
的一部分进行仿真、综合、实现、比特流生成和上板。虽然 chisel
中包含测试模块,但是它完全不支持对于 IP 核的调用。
参考资料