什么是 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 )0 ) := 0. U 1 ) := 1. U "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 )Mux1H (Seq (0 ) -> io.in.src(0 ) + io.in.src(1 ),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 )
强大的语法检查 
正如上文所提到的,在 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 )Mux1H (Seq (0 ) -> io.in.src(0 ) + io.in.src(1 ),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)
译码模块 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 ())0. U 
在实际使用中,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)0. U 
在实例化一个流水级寄存器时,可以传入自定义类型以定制寄存器的输入输出数据类型:
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 核的调用。
参考资料