ChiSel 学习笔记

原文链接

https://blog.csdn.net/qq_34291505/article/details/87365907

第十七章 Chisel基础——数据类型

为了从Chisel转变成Verilog,语言开发人员开发了一个中间的标准交换格式——Firrtl,它跟Vrilog是同一级别的,两者都比Chisel低一级。编写的Chisel代码首先会经过Firrtl编译器,生成Firrtl代码,也就是一个后缀格式为“.fir”的文件,然后由这个Firrtl文件再去生成对应的Verilog代码。如果读者有兴趣看一看Firrtl的格式,其实与Verilog很接近,只不过是由机器生成的、很死板的代码。Firrtl编译器也并不是只针对Chisel
Verilog的最初目的是用于电路验证,所以它有很多不可综合的语法。Firrtl在转变成Verilog时,只会采用可综合的语法,因此读者完全不用担心用Chisel写出来的电路不可综合。只要能正确生成Verilog,那就能被综合器生成电路

Chisel目前不支持四态逻辑里的x和z,只支持0和1。由于只有芯片对外的IO处才能出现三态门,所以内部设计几乎用不到x和z。而且x和z在设计中会带来危害,忽略掉它们也不影响大多数设计,还简化了模型。当然,如果确实需要,可以通过黑盒语法与外部的Verilog代码互动,也可以在下游工具链里添加四态逻辑
Chisel会对未被驱动的输出型端口和线网进行检测,如果存在,会进行报错

Chisel的代码包并不会像Scala的标准库那样被编译器隐式导入,所以每个Chisel文件都应该在开头至少写一句“import chisel3._”。这个包包含了基本的语法,对于某些高级语法,则可能需要“import chisel3.util._”、“import chisel3.experimental._”、 “import chisel3.testers._”等等

Chisel的数据类型

hisel定义了自己的一套数据类型,读者应该跟Scala的九种基本值类区分开来。而且Chisel也能使用Scala的数据类型,但是Scala的数据类型都是用于参数和内建控制结构,构建硬件电路还是得用Chisel自己的数据类型,在使用时千万不要混

 在实际硬件构成里,并不会用到Data,读者也不用关心它的具体实现细节,应该关注Data类的两大子类:聚合类Aggregate和元素类Element

聚合类Aggregate的常用子类是向量类Vec[T]和包裹类Bundle

Vec[T]类用于包含相同的元素,元素类型T可以是任意的Data子类。

Bundle类用于被自定义的类继承,这样自定义的类就能包含任意Data的子类对象,常用于协助构造模块的端口,故而衍生出了一些预定义的端口子类

Element类衍生出了Analog、Bits和Clock三个子类,单例对象DontCare和特质Reset

Analog用于在黑盒中模拟inout端口,目前在实际Chisel里并无其他用途。Bits类的两个子类SInt和UInt是最常用的两个数据类型,它们是用补码表示的有符号整数和无符号整数。不仅用来协助定义端口位宽,还用来进行赋值

FixedPoint类提供的API带有试验性质,而且将来可能会发生改变,所以不常用。Bool类是Chisel自己的布尔类型,区别于Scala的Boolean。Bool类是UInt类的子类

Clock类表示时钟,Chisel里的时钟是专门的一个类型,并不像Verilog里那样是1bit的线网。复位类型Reset也是如此。单例对象DontCare用于赋值给未驱动的端口或线网,防止编译器报错

数据字面量

能够表示具体值的数据类型为UInt、SInt和Bool。实际可综合的电路都是若干个bit,所以只能表示整数,这与Verilog是一致的。要表示浮点数,本质还是用多个bit来构建,而且要遵循IEEE的浮点标准。对于UInt,可以构成任意位宽的线网或寄存器。对于SInt,在Chisel里会按补码解读,转换成Verilog后会使用系统函数$signed,这是可综合的。对于Bool,转换成Verilog后就是1bit的线网或寄存器。

Chisel定义了一系列隐式类:fromBigIntToLiteral、fromtIntToLiteral、fromtLongToLiteral、fromStringToLiteral、fromBooleanToLiteral。回顾前面讲述的隐式类的内容,也就是会有相应的隐式转换。以隐式类fromtIntToLiteral为例,存在一个同名的隐式转换,把相应的Scala的Int对象转换成一个fromtIntToLiteral的对象。而fromtIntToLiteral类有两个方法U和S,分别构造一个等值的UInt对象和SInt对象。再加上Scala的基本值类都是用字面量构造对象,所以要表示一个UInt对象,可以写成“1.U”的格式,这样编译器会插入隐式转换,变成“fromtIntToLiteral(1).U”,进而构造出字面值为“1”的UInt对象。同理,也可以构造SInt。还有相同行为的方法asUInt和asSInt。

1.U                // 字面值为“1”的UInt对象

-8.S               // 字面值为“-8”的SInt对象

“b0101”.U     // 字面值为“5”的UInt对象

true.B            // 字面值为“true”的Bool对象 

数据宽度

Chisel3专门设计了宽度类Width。还有一个隐式类fromIntToWidth,就是把Int对象转换成fromIntToWidth类型的对象,然后通过方法W返回一个Width对象

1.U              // 字面值为“1”、宽度为1bit的UInt对象

1.U(32.W)   // 字面值为“1”、宽度为32bit的UInt对象

UInt、SInt和Bool都不是抽象类,除了可以通过字面量构造对象以外,也可以直接通过apply工厂方法构造没有字面量的对象

有字面量的数据类型用于赋值、初始化寄存器等操作,而无字面量的数据类型则用于声明端口、构造向量等

类型转换

UInt、SInt和Bool三个类都包含四个方法:asUInt、asSInt、toBool和toBools

toBool会把1bit的“1”转换成Bool类型的true,“0”转换成false。如果位宽超过1bit,则用toBools转换成Bool类型的序列Seq[Bool]

Bool类还有一个方法asClock,把true转换成电压常高的时钟,false转换成电压常低的时钟。Clock类只有一个方法asUInt,转换成对应的0或1

向量

如果需要一个集合类型的数据,除了可以使用Scala内建的数组、列表、集等数据结构外,还可以使用Chisel专属的Vec[T]。T必须是Data的子类,而且每个元素的类型、位宽必须一样。Vec[T]的伴生对象里有一个apply工厂方法,接收两个参数,第一个是Int类型,表示元素的个数,第二个是元素

val myVec = Wire(Vec(3, UInt(32.W)))

val myReg = myVec(0)

还有一个工厂方法VecInit[T],通过接收一个Seq[T]作为参数来构造向量,或者是多个重复参数。不过,这个工厂方法常把有字面值的数据作为参数,用于初始化寄存器组、ROM、RAM等,或者用来构造多个模块。

因为Vec[T]也是一种序列,所以它也定义了诸如map、flatMap、zip、foreach、filter、exists、contains等方法。尽管这些方法应该出现在软件里,但是它们也可以简化硬件逻辑的编写,减少手工代码量。

混合向量

混合向量MixedVec[T]与普通的向量Vec[T]类似,只不过包含的元素可以不全都一样

对于构造Vec[T]和MixedVec[T]的序列,并不一定要逐个手写,可以通过Scala的函数,比如fill、map、flatMap、to、until等来生成

val mixVec = Wire(MixedVec((1 to 10) map { i => UInt(i.W) }))

包裹

抽象类Bundle很像C语言的结构体(struct),用户可以编写一个自定义类来继承自它,然后在自定义的类里包含其它各种Data类型的字段。它可以协助构建线网或寄存器,但是最常见的用途是用于构建一个模块的端口列表,或者一部分端口

class MyModule extends Module {
   val io = IO(new Bundle {
       val in = Input(UInt(32.W))
       val out = Output(UInt(32.W))
   })

Chisel的内建操作符

 

 

位宽推断

第十八章 Chisel基础——模块与硬件类型

实际的电路应该是由硬件类型的对象构成的,不管是信号的声明,还是用赋值进行信号传递,都是由硬件类型的对象来完成的。数据类型和硬件类型融合在一起,才能构成完整、可运行的组件。

Chisel是如何赋值的

在Chisel里,所有对象都应该由val类型的变量来引用,因为硬件电路的不可变性。

引用的对象很可能需要被重新赋值。例如,输出端口在定义时使用了“=”与端口变量名进行了绑定,那等到驱动该端口时,就需要通过变量名来进行赋值操作,更新数据

Chisel类都定义了方法“:=”,作为等号赋值的代替。所以首次创建变量时用等号初始化,如果变量引用的对象不能立即确定状态或本身就是可变对象,则在后续更新状态时应该用“:=”

val x = Wire(UInt(4.W))

val y = Wire(UInt(4.W))

x := "b1010".U  // 向4bit的线网x赋予了无符号数10

y := ~x  // 把x按位取反,传递给y

端口

定义端口列表

整个端口列表是由方法“IO[T <: Data](iodef: T)”来定义的,通常其参数是一个Bundle类型的对象,而且引用的字段名称必须是“io”。因为端口存在方向,所以还需要方法“Input[T <: Data](source: T)”和“Output[T <: Data](source: T)”来为每个端口表明具体的方向。

目前Chisel还不支持双向端口inout,只能通过黑盒里的Analog端口来模拟外部Verilog的双向端口

class MyIO extends Bundle {
   val in = Input(Vec(5, UInt(32.W)))
   val out = Output(UInt(32.W))
}

......
   val io = IO(new MyIO)  // 模块的端口列表
......

翻转端口列表的方向

对于两个相连的模块,可能存在大量同名但方向相反的端口。仅仅为了翻转方向而不得不重写一遍端口显得费时费力,所以Chisel提供了“Flipped[T <: Data](source: T)”方法,可以把参数里所有的输入转输出,输出转输入。如果是黑盒里的Analog端口,则仍是双向的。

class MyIO extends Bundle {
   val in = Input(Vec(5, UInt(32.W)))
   val out = Output(UInt(32.W))
}

......
   val io = IO(new MyIO)  // in是输入,out是输出
......
   val io = IO(Flipped(new MyIO))  // out是输入,in是输出

整体连接

翻转方向的端口列表通常配合整体连接符号“<>”使用。该操作符会把左右两边的端口列表里所有同名的端口进行连接,而且同一级的端口方向必须是输入连输出、输出连输入,父级和子级的端口方向则是输入连输入、输出连输出。

方向必须按这个规则匹配,而且不能存在端口名字、数量、类型不同的情况。这样就省去了大量连线的代码

模块

定义模块

Chisel里面是用一个自定义的类来定义模块的,这个类有以下三个特点:

①继承自Module类。

②有一个抽象字段“io”需要实现,该字段必须引用前面所说的端口对象。

③在类的主构造器里进行内部电路连线。

这样定义的模块会继承一个字段“clock”,类型是Clock,它表示全局时钟,在整个模块内都可见。对于组合逻辑,是用不上它的,而时序逻辑虽然需要这个时钟,但也不用显式声明。还有一个继承的字段“reset”,类型是Reset,表示全局复位信号,在整个模块内可见。对于需要复位的时序元件,也可以不用显式使用该字段。

// mux2.scala
package test
 
import chisel3._
 
class Mux2 extends Module {
  val io = IO(new Bundle{
    val sel = Input(UInt(1.W))
    val in0 = Input(UInt(1.W))
    val in1 = Input(UInt(1.W))
    val out = Output(UInt(1.W))
  })
 
  io.out := (io.sel & io.in1) | (~io.sel & io.in0)
}

“new Bundle { … }”的写法是声明一个匿名类继承自Bundle,然后实例化匿名类。对于短小、简单的端口列表,可以使用这种简便写法。对于大的公用接口,应该单独写成具名的Bundle子类,方便修改。“io.out := …”其实就是主构造方法的一部分

例化模块

并不是直接用new生成一个实例对象就完成了,还需要再把实例的对象传递给单例对象Module的apply方法。

  val m0 = Module(new Mux2)

例化多个模块

对于要多次例化的重复模块,可以利用向量的工厂方法VecInit[T <: Data]。

所以可以把待例化模块的io字段组成一个序列

生成序列的一种方法是调用单例对象Seq里的方法fill,该方法的一个重载版本有两个单参数列表,第一个接收Int类型的对象,表示序列的元素个数,第二个是传名参数,接收序列的元素。

// mux4_2.scala
package test
 
import chisel3._
 
class Mux4_2 extends Module {
  val io = IO(new Bundle {
    val in0 = Input(UInt(1.W))
    val in1 = Input(UInt(1.W))
    val in2 = Input(UInt(1.W))
    val in3 = Input(UInt(1.W))
    val sel = Input(UInt(2.W))
    val out = Output(UInt(1.W))
  })
 
  val m = VecInit(Seq.fill(3)(Module(new Mux2).io))  // 例化了三个Mux2,并且参数是端口字段io
  m(0).sel := io.sel(0)  // 模块的端口通过下标索引,并且路径里没有“io”
  m(0).in0 := io.in0
  m(0).in1 := io.in1
 
  m(1).sel := io.sel(0)
  m(1).in0 := io.in2
  m(1).in1 := io.in3
 
  m(2).sel := io.sel(1)
  m(2).in0 := m(0).out
  m(2).in1 := m(1).out
 
  io.out := m(2).out
}

线网

Chisel把线网作为电路的节点,通过工厂方法“Wire[T <: Data](t: T)”来定义。可以对线网进行赋值,也可以连接到其他电路节点,这是组成组合逻辑的基本硬件类型。

val myNode = Wire(UInt(8.W))

myNode := 0.U 

因为Scala作为软件语言是顺序执行的,定义具有覆盖性,所以如果对同一个线网多次赋值,则只有最后一次有效

寄存器

如果模块里没有多时钟域的语句块,那么寄存器都是由隐式的全局时钟来控制。对于有复位信号的寄存器,如果不在多时钟域语句块里,则由隐式的全局复位来控制,并且高有效

目前Chisel所有的复位都是同步复位,异步复位功能还在开发中。如果需要异步复位寄存器,则需要通过黑盒引入

有五种内建的寄存器,

第一种是跟随寄存器“RegNext[T <: Data](next: T)”,在每个时钟上升沿,它都会采样一次传入的参数,并且没有复位信号。它的另一个版本的apply工厂方法是“RegNext[T <: Data](next: T, init: T)”,也就是由复位信号控制,当复位信号有效时,复位到指定值,否则就跟随。
第二种是复位到指定值的寄存器“RegInit[T <: Data](init: T)”,参数需要声明位宽,否则就是默认位宽。可以用内建的when语句进行条件赋值。

第三种是普通的寄存器“Reg[T <: Data](t: T)”,它可以在when语句里用全局reset信号进行同步复位(reset信号是Reset类型,要用toBool进行类型转换),也可以进行条件赋值或无条件跟随。参数同样要指定位宽。

第四种是util包里的带一个使能端的寄存器“RegEnable[T <: Data](next: T, init: T, enable: Bool)”,如果不需要复位信号,则第二个参数可以省略给出。

第五种是util包里的移位寄存器“ShiftRegister[T <: Data](in: T, n: Int, resetData: T, en: Bool)”,其中第一个参数in是带移位的数据,第二个参数n是需要延迟的周期数,第三个参数resetData是指定的复位值,可以省略,第四个参数en是使能移位的信号,默认为true.B。

// reg.scala
package test
 
import chisel3._
import chisel3.util._
 
class REG extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(8.W))
    val en = Input(Bool())
    val c = Output(UInt(1.W))
  })
 
  val reg0 = RegNext(io.a)
  val reg1 = RegNext(io.a, 0.U)
  val reg2 = RegInit(0.U(8.W))
  val reg3 = Reg(UInt(8.W))
  val reg4 = Reg(UInt(8.W))
  val reg5 = RegEnable(io.a + 1.U, 0.U, io.en)
  val reg6 = RegEnable(io.a - 1.U, io.en)
  val reg7 = ShiftRegister(io.a, 3, 0.U, io.en)
  val reg8 = ShiftRegister(io.a, 3, io.en)
  
  reg2 := io.a.andR
  reg3 := io.a.orR
 
  when(reset.toBool) {
    reg4 := 0.U
  } .otherwise {
    reg4 := 1.U
  }
 
  io.c := reg0(0) & reg1(0) & reg2(0) & reg3(0) & reg4(0) & reg5(0) & reg6(0) & reg7(0) & reg8(0)
}

寄存器组

如果把子类型Vec[T]作为参数传递进去,就会生成多个位宽相同、行为相同、名字前缀相同的寄存器。同样,寄存器组在Chisel代码里可以通过下标索引。

// reg2.scala
package test
 
import chisel3._
import chisel3.util._
 
class REG2 extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(8.W))
    val en = Input(Bool())
    val c = Output(UInt(1.W))
  })
 
  val reg0 = RegNext(VecInit(io.a, io.a))
  val reg1 = RegNext(VecInit(io.a, io.a), VecInit(0.U, 0.U))
  val reg2 = RegInit(VecInit(0.U(8.W), 0.U(8.W)))
  val reg3 = Reg(Vec(2, UInt(8.W)))
  val reg4 = Reg(Vec(2, UInt(8.W)))
  val reg5 = RegEnable(VecInit(io.a + 1.U, io.a + 1.U), VecInit(0.U(8.W), 0.U(8.W)), io.en)
  val reg6 = RegEnable(VecInit(io.a - 1.U, io.a - 1.U), io.en)
  val reg7 = ShiftRegister(VecInit(io.a, io.a), 3, VecInit(0.U(8.W), 0.U(8.W)), io.en)
  val reg8 = ShiftRegister(VecInit(io.a, io.a), 3, io.en)
  
  reg2(0) := io.a.andR
  reg2(1) := io.a.andR
  reg3(0) := io.a.orR
  reg3(1) := io.a.orR
 
  when(reset.toBool) {
    reg4(0) := 0.U
    reg4(1) := 0.U
  } .otherwise {
    reg4(0) := 1.U
    reg4(1) := 1.U
  }
 
  io.c := reg0(0)(0) & reg1(0)(0) & reg2(0)(0) & reg3(0)(0) & reg4(0)(0) & reg5(0)(0) & reg6(0)(0) & reg7(0)(0) & reg8(0)(0) &
          reg0(1)(0) & reg1(1)(0) & reg2(1)(0) & reg3(1)(0) & reg4(1)(0) & reg5(1)(0) & reg6(1)(0) & reg7(1)(0) & reg8(1)(0)
}

用when给电路赋值

由于Scala已经占用了“if…else if…else”语法,所以相应的Chisel控制结构改成了when语句,其语法如下:

when用于给带使能信号的寄存器更新数据,组合逻辑不常用。对于有复位信号的寄存器,推荐使用RegInit来声明,这样生成的Verilog会自动根据当前的时钟域来同步复位,尽量不要在when语句里用“reset.toBool”作为复位条件

除了when结构,util包里还有一个与之对偶的结构“unless”,如果unless的判定条件为false.B则一直执行,否则不执行

数据类型与硬件类型的区别

hisel的数据类型,其中常用的就五种:UInt、SInt、Bool、Bundle和Vec[T]。本章介绍了硬件类型,最基本的是IO、Wire和Reg三种,还有指明端口方向的Input、Output和Flipped

在编写Chisel时,要注意哪些地方是数据类型,哪些地方又是硬件类型。这时,静态语言的优势便体现出来了,因为编译器会帮助程序员检查类型是否匹配。如果在需要数据类型的地方出现了硬件类型、在需要硬件类型的地方出现了数据类型

为VecInit专门接收硬件类型的参数来构造硬件向量,给VecInit传入数据类型反而会报错

第十九章 Chisel基础——常用的硬件原语

Chisel在语言库里定义了很多常用的硬件原语,读者可以直接导入相应的包来使用

多路选择器

第一种形式是二输入多路选择器“Mux(sel, in1, in2)”。sel是Bool类型,in1和in2的类型相同,都是Data的任意子类型。当sel为true.B时,返回in1,否则返回in2

所以Mux可以内嵌Mux,Mux(c1, a, Mux(c2, b, Mux(…, default)))

第二种就是针对上述n输入多路选择器的简便写法,形式为“MuxCase(default, Array(c1 -> a, c2 -> b, …))”,它的展开与嵌套的Mux是一样的。第一个参数是默认情况下返回的结果,第二个参数是一个数组,数组的元素是对偶“(成立条件,被选择的输入)”。MuxCase在chisel3.util包里

第三种是MuxCase的变体,它相当于把MuxCase的成立条件依次换成从0开始的索引值,就好像一个查找表,其形式为“MuxLookup(idx, default, Array(0.U -> a, 1.U -> b, …))”。它的展开相当于“MuxCase(default, Array((idx === 0.U) -> a, (idx === 1.U) -> b, …))”。MuxLookup也在chisel3.util包里。

第四种是chisel3.util包里的独热码多路选择器,它的选择信号是一个独热码。如果零个或多个选择信号有效,则行为未定义

val hotValue = Mux1H(Seq(
    io.selector(0) -> 2.U,
    io.selector(1) -> 4.U,
    io.selector(2) -> 8.U,
    io.selector(4) -> 11.U
))

ROM

可以通过工厂方法“VecInit[T <: Data](elt0: T, elts: T*)”或“VecInit[T <: Data](elts: Seq[T])”来创建一个只读存储器,参数就是ROM里的常量数值,对应的Verilog代码就是给读取ROM的线网或寄存器赋予常量值

// rom.scala
package test
 
import chisel3._
 
class ROM extends Module {
  val io = IO(new Bundle {
    val sel = Input(UInt(2.W))
    val out = Output(UInt(8.W))  
  })
 
  val rom = VecInit(1.U, 2.U, 3.U, 4.U)
 
  io.out := rom(io.sel)
}

RAM

Chisel支持两种类型的RAM。第一种RAM是同步(时序)写,异步(组合逻辑)读,通过工厂方法“Mem[T <: Data](size: Int, t: T)”来构建

val asyncMem = Mem(16, UInt(32.W)) 

由于现代的FPGA和ASIC技术已经不再支持异步读RAM,所以这种RAM会被综合成寄存器阵列。第二种RAM则是同步(时序)读、写,通过工厂方法“SyncReadMem[T <: Data](size: Int, t: T)”来构建,这种RAM会被综合成实际的SRAM

val syncMem = SyncReadMem(16, UInt(32.W))

写RAM的语法是

when(wr_en) {
     mem.write(address, dataIn) 
     out := DontCare
}

读RAM的语法是

out := mem.read(address, rd_en)

带写掩模的RAM

RAM通常都具备按字节写入的功能,比如数据写入端口的位宽是32bit,那么就应该有4bit的写掩模信号,只有当写掩模比特有效时,对应的字节才会写入。Chisel也具备构建带写掩模的RAM的功能。

而write方法有一个重载版本,就是第三个参数是接收写掩模信号的。当下标为0的写掩模比特是true.B时,最低的那个字节会被写入,依次类推。下面是一个带写掩模的单端口RAM

// maskram.scala
package test
 
import chisel3._
import chisel3.util._
 
class MaskRAM extends Module {
  val io = IO(new Bundle {
    val addr = Input(UInt(10.W))
    val dataIn = Input(UInt(32.W))
    val en = Input(Bool())
    val we = Input(UInt(4.W))
    val dataOut = Output(UInt(32.W))  
  })
 
  val dataIn_temp = Wire(Vec(4, UInt(8.W)))
  val dataOut_temp = Wire(Vec(4, UInt(8.W)))
  val mask = Wire(Vec(4, Bool()))
 
  val syncRAM = SyncReadMem(1024, Vec(4, UInt(8.W)))
 
  when(io.en) {
    syncRAM.write(io.addr, dataIn_temp, mask)
    dataOut_temp := syncRAM.read(io.addr)
  } .otherwise {
    dataOut_temp := DontCare
  } 
 
  for(i <- 0 until 4) {
    dataIn_temp(i) := io.dataIn(8*i+7, 8*i)
    mask(i) := io.we(i).toBool
    io.dataOut := Cat(dataOut_temp(3), dataOut_temp(2), dataOut_temp(1), dataOut_temp(0))
  }
}

从文件读取数据到RAM

在experimental包里有一个单例对象loadMemoryFromFile,它的apply方法可以在Chisel层面上从txt文件读取数据到RAM里、、

MemBase[T]类型的,也就是Mem[T]和SyncReadMem[T]的超类,该参数接收一个自定义的RAM对象。第二个参数是文件的名字及路径,用字符串表示。第三个参数表示读取的方式为十六进制或二进制,默认是MemoryLoadFileType.Hex,也可以改成MemoryLoadFileType.Binary。注意,没有十进制和八进制

计数器

Chisel在util包里定义了一个自增计数器原语Counter,它的工厂方法接收两个参数:第一个参数是Bool类型的使能信号,为true.B时计数器从0开始每个时钟上升沿加1自增,为false.B时则计数器保持不变;第二个参数需要一个Int类型的具体正数,当计数到该值时归零。该方法返回一个二元组,其第一个元素是计数器的计数值,第二个元素是判断计数值是否等于期望值的结果。

// counter.scala

package test

import chisel3._

import chisel3.util._

class MyCounter extends Module {

  val io = IO(new Bundle {

    val en = Input(Bool())

    val out = Output(UInt(8.W))

    val valid = Output(Bool())  

  })

  val (a, b) = Counter(io.en, 233)

  io.out := a

  io.valid := b

}

16位线性反馈移位寄存器

如果要产生伪随机数,可以使用util包里的16位线性反馈移位寄存器原语LFSR16,它接收一个Bool类型的使能信号,用于控制寄存器是否移位,缺省值为true.B。它返回一个UInt(16.W)类型的结果。

// lfsr.scala
package test
 
import chisel3._
import chisel3.util._
 
class LFSR extends Module {
  val io = IO(new Bundle {
    val en = Input(Bool())
    val out = Output(UInt(16.W))  
  })
 
  io.out := LFSR16(io.en)
}

状态机

Chisel没有直接构建状态机的原语。不过,util包里定义了一个Enum特质及其伴生对象。伴生对象里的apply方法定义如下

def apply(n: Int): List[UInt]

参数n返回对应元素数的List[UInt],每个元素都是不同的,所以可以作为枚举值来使用。最好把枚举状态的变量名也组成一个列表,然后用列表的模式匹配来进行赋值。有了枚举值后,可以通过“switch…is…is”语句来使用

// fsm.scala
package test
 
import chisel3._
import chisel3.util._
 
class DetectTwoOnes extends Module {
  val io = IO(new Bundle {
    val in = Input(Bool())
    val out = Output(Bool())
  })
 
  val sNone :: sOne1 :: sTwo1s :: Nil = Enum(3)
  val state = RegInit(sNone)
 
  io.out := (state === sTwo1s)
 
  switch (state) {
    is (sNone) {
      when (io.in) {
        state := sOne1
      }
    }
    is (sOne1) {
      when (io.in) {
        state := sTwo1s
      } .otherwise {
        state := sNone
      }
    }
    is (sTwo1s) {
      when (!io.in) {
        state := sNone
      }
    }
  }
}

枚举状态名的首字母要小写,这样Scala的编译器才能识别成变量模式匹配。

Chisel基础——生成Verilog与基本测试

把一个Chisel模块编译成Verilog代码,并进一步使用Verilator做一些简单的测试

生成Verilog

生成Verilog的程序自然是在主函数里例化待编译的模块,然后运行这个主函数。例化待编译模块需要特殊的方法调用。chisel3包里有一个单例对象Driver,它包含一个方法execute,该方法接收两个参数,第一个参数是命令行传入的实参即字符串数组args,第二个是返回待编译模块的对象的无参函数。运行这个execute方法,就能得到Verilog代码。

接着,读者需要在src/test/scala文件夹下编写对应的主函数文件

// fullAdderGen.scala
package test
 
object FullAdderGen extends App {
  chisel3.Driver.execute(args, () => new FullAdder)
}

在这个主函数里,只有一个execute函数的调用,第一个参数固定是“args”,第二个参数则是无参的函数字面量“() => new FullAdder”。因为Chisel的模块本质上还是Scala的class,所以只需用new构造一个对象作为返回结果即可。主函数里可以包括多个execute函数,也可以包含其它代码。还有一点要注意的是,建议把设计文件和主函数放在一个包里,比如这里的“package test”,这样省去了编写路径的麻烦。

要运行这个主函数,需要在build.sbt文件所在的路径下打开终端,然后执行命令

:~/chisel-template$ sbt ‘test:runMain test.FullAdderGen’

sbt后面有空格,再后面的内容都是被单引号对或双引号对包起来。其中,test:runMain是让sbt执行主函数的命令,而test.FullAdderGen就是要执行的那个主函数

终端的路径下就会生成三个文件:FullAdder.anno.json、FullAdder.fir和FullAdder.v。

第二个后缀为“.fir”的文件就是对应的Firrtl代码,第三个自然是对应的Verilog文件。

在命令里增加参数

给Firrtl传递参数

命令后面继续增加可选的参数。例如,增加参数“–help”查看帮助菜单

最常用的是参数“-td”,可以在后面指定一个文件夹,这样之前生成的三个文件就在该文件夹里,而不是在当前路径下

给主函数传递参数

Scala的类可以接收参数,自然Chisel的模块也可以接收参数。假设要构建一个n位的加法器,具体位宽不确定,根据需要而定。那么,就可以把端口位宽参数化,例化时传入想要的参数即可。

package test
 
import chisel3._
 
class Adder(n: Int) extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(n.W))
    val b = Input(UInt(n.W))
    val s = Output(UInt(n.W))
    val cout = Output(UInt(1.W))  
  })
 
  io.s := (io.a +& io.b)(n-1, 0)
  io.cout := (io.a +& io.b)(n)
}
 
// adderGen.scala
package test
 
object AdderGen extends App {
  chisel3.Driver.execute(args, () => new Adder(args(0).toInt))
}

比如例子中的主函数期望第一个参数即args(0)是一个数字字符串,这样就能通过方法toInt转换成Adder所需的参数。

~/chisel-template$  sbt ‘test:runMain test.AdderGen 8 -td ./generated/adder’

编写简单的测试

Chisel的测试有两种,第一种是利用Scala的测试来验证Chisel级别的代码逻辑有没有错误。因为这部分内容比较复杂,而且笔者目前也没有深入学习有关Scala测试的内容,所以这部分内容可有读者自行选择研究。第二种是利用Chisel库里的peek和poke函数,给模块的端口加激励、查看信号值,并交由下游的Verilator来仿真、产生波形。这种方式比较简单,类似于Verilog的testbench,适合小型电路的验证。对于超大型的系统级电路,最好还是生成Verilog,交由成熟的EDA工具,用UVM进行验证。

要编写一个简单的testbench,首先也是定义一个类,这个类的主构造方法接收一个参数,参数类型就是待测模块的类名。其次,这个类继承自PeekPokeTester类,并且把接收的待测模块也传递给此超类。最后,测试类内部有四种方法可用:①“poke(端口,激励值)”方法给相应的端口添加想要的激励值,激励值是Int类型的;②“peek(端口)”方法返回相应的端口的当前值;③“expect(端口,期望值)”方法会对第一个参数(端口)使用peek方法,然后与Int类型的期望值进行对比,如果两者不相等则出错;④“step(n)”方法则让仿真前进n个时钟周期。

 

package test
 
import scala.util._
import chisel3.iotesters._
 
class AdderTest(c: Adder) extends PeekPokeTester(c) {
  val randNum = new Random
  for(i <- 0 until 10) {
    val a = randNum.nextInt(256)
    val b = randNum.nextInt(256)
    poke(c.io.a, a)
    poke(c.io.b, b)
    step(1)
    expect(c.io.s, (a + b) & 0xff)
    expect(c.io.cout, ((a + b) & 0x100) >> 8)
  }
}

 

第一个包scala.util里包含了Scala生成伪随机数的类Random,第二个包chisel3.iotesters包含了测试类PeekPokeTester

运行测试

 自然也是通过主函数,但是这次是使用iotesters包里的execute方法。该方法与前面生成Verilog的方法类似,仅仅是多了一个参数列表,多出的第二个参数列表接收一个返回测试类的对象的函数:

// addertest.scala
object AdderTestGen extends App {
  chisel3.iotesters.Driver.execute(args, () => new Adder(8))(c => new AdderTest(c))
}

~/chisel-template$  sbt ‘test:runMain test.AdderTestGen -td ./generated/addertest –backend-name verilator’ 

执行成功后,就能在相应文件夹里看到一个新生成的文件夹,里面是仿真生成的文件。其中,“Adder.vcd”文件就是波形文件,使用GTKWave软件打开就能查看,将相应的端口拖拽到右侧就能显示波形。

第二十一章 Chisel基础——黑盒

例化黑盒

如果定义Dut类时,不是继承自Module,而是继承自BlackBox,则允许只有端口定义,也只需要端口定义。此外,在别的模块里例化黑盒时,编译器不会给黑盒的端口名加上“io_”

// blackbox.scala
package test
 
import chisel3._
 
class Dut extends BlackBox {
  val io = IO(new Bundle {
    val a = Input(UInt(32.W))
    val clk = Input(Clock())
    val reset = Input(Bool())
    val b = Output(UInt(4.W))  
  })
}
 
class UseDut extends Module {
  val io = IO(new Bundle {
    val toDut_a = Input(UInt(32.W))
    val toDut_b = Output(UInt(4.W))  
  })
 
  val u0 = Module(new Dut)
 
  u0.io.a := io.toDut_a
  u0.io.clk := clock
  u0.io.reset := reset
  io.toDut_b := u0.io.b
}
 
object UseDutTest extends App {
  chisel3.Driver.execute(args, () => new UseDut)
}

BlackBox的构造方法可以接收一个Map[String, Param]类型的参数,这会使得例化外部的Verilog模块时具有配置模块的“#(参数配置)”。映射的键固定是字符串类型,它对应Verilog里声明的参数名;映射的值对应传入的配置参数,可以是字符串,也可以是整数和浮点数。虽然值的类型是Param,这是一个Chisel的印章类,但是单例对象chisel3.experimental里定义了相应的隐式转换,可以把BigInt、Int、Long、Double和String转换成对应的Param类型

...
import chisel3.experimental._
 
class Dut extends BlackBox(Map("DATA_WIDTH" -> 32,
                               "MODE" -> "Sequential",
                               "RESET" -> "Asynchronous")) {
  val io = IO(new Bundle {
    val a = Input(UInt(32.W))
    val clk = Input(Clock())
    val reset = Input(Bool())
    val b = Output(UInt(4.W))  
  })
}
..

复制Verilog文件

chisel3.util包里有一个特质HasBlackBoxResource,如果在黑盒类里混入这个特质,并且在src/main/resources文件夹里有对应的Verilog源文件,那么在Chisel转换成Verilog时,就会把Verilog文件一起复制到目标文件夹。


import chisel3.util._

class Dut extends BlackBox with HasBlackBoxResource {
val io = IO(new Bundle {
val a = Input(UInt(32.W))
val clk = Input(Clock())
val reset = Input(Bool())
val b = Output(UInt(4.W))
})

setResource(“/dut.v”)
}

注意,相比一般的黑盒,除了端口列表的声明,还多了一个特质里的setResource方法的调用。方法的入参是Verilog文件的相对地址,即相对src/main/resources的地址

内联Verilog文件

hisel3.util包里还有有一个特质HasBlackBoxInline,混入该特质的黑盒类可以把Verilog代码直接内嵌进去。内嵌的方式是调用特质里的方法“setInline(blackBoxName: String, blackBoxInline: String)”,类似于setResource的用法。这样,目标文件夹里就会生成一个单独的Verilog文件,复制内嵌的代码。该方法适合小型Verilog设计。

inout端口

Chisel目前只支持在黑盒中引入Verilog的inout端口。Bundle中使用 “Analog(位宽)”声明Analog类型的端口,经过编译后变成Verilog的inout端口

模块里的端口可以声明成Analog类型,但只能用于与黑盒连接,不能在Chisel代码中进行读写。

使用前,要先用“chisel3.experimental._”进行导入。

第二十二章 Chisel基础——多时钟域设计

// inout.scala
package test

import chisel3._
import chisel3.util._
import chisel3.experimental._

class InoutIO extends Bundle {
val a = Analog(16.W)
val b = Input(UInt(16.W))
val sel = Input(Bool())
val c = Output(UInt(16.W))
}

class InoutPort extends BlackBox with HasBlackBoxInline {
val io = IO(new InoutIO)

setInline(“InoutPort.v”,
“””
|module InoutPort( inout [15:0] a,
| input [15:0] b,
| input sel,
| output [15:0] c);
| assign a = sel ? ‘bz : b;
| assign c = sel ? a : ‘bz;
|endmodule
“””.stripMargin)
}

class MakeInout extends Module {
val io = IO(new InoutIO)

val m = Module(new InoutPort)

m.io <> io
}

object InoutGen extends App {
chisel3.Driver.execute(args, () => new MakeInout)
}

第二十二章 Chisel基础——多时钟域设计

数字电路中免不了用到多时钟域设计,尤其是设计异步FIFO这样的同步元件

在Chisel里,则相对复杂一些,因为这与Scala的变量作用域相关,而且时序元件在编译时都是自动地隐式跟随当前时钟域。

没有隐式端口的模块

继承自Module的模块类会获得隐式的全局时钟与同步复位信号,即使在设计中用不上它们也没关系。如果读者确实不喜欢这两个隐式端口,则可以选择继承自RawModule,这样在转换成Verilog时就没有隐式端口。

// module.scala
package test
 
import chisel3._
import chisel3.experimental._
 
class MyModule extends RawModule {
  val io = IO(new Bundle {
    val a = Input(UInt(4.W))
    val b = Input(UInt(4.W))
    val c = Output(UInt(4.W))
  })
 
  io.c := io.a & io.b
}
 
object ModuleGen extends App {
  chisel3.Driver.execute(args, () => new MyModule)
}

RawModule也可以包含时序逻辑,但要使用多时钟域语法。

定义一个时钟域和复位域

chisel3.core包里有一个单例对象withClockAndReset,其apply方法定义如下:

def apply[T](clock: Clock, reset: Reset)(block: ⇒ T): T

在编写代码时不能写成“import chisel3.core._”,这会扰乱“import chisel3._”的导入内容。正确做法是用“import chisel3.experimental._”导入experimental对象,它里面用同名字段引用了单例对象chisel3.core.withClockAndReset,这样就不需要再导入core包。

class MultiClockModule extends Module {
   val io = IO(new Bundle {
       val clockB = Input(Clock())
       val resetB = Input(Bool())
       val stuff = Input(Bool())
   })
   // 这个寄存器跟随当前模块的隐式全局时钟clock
   val regClock1 = RegNext(io.stuff)

   withClockAndReset(io.clockB, io.resetB) {
       // 在该花括号内,所有时序元件都跟随时钟io.clockB
       // 所有寄存器的复位信号都是io.resetB

       // 这个寄存器跟随io.clockB
       val regClockB = RegNext(io.stuff)
       // 还可以例化其它模块
       val m = Module(new ChildModule)
    }

   // 这个寄存器跟随当前模块的隐式全局时钟clock
   val regClock2 = RegNext(io.stuff)
}
————————————————

因为第二个参数列表只有一个传名参数,所以可以把圆括号写成花括号,这样还有自动的分号推断。再加上传名参数的特性,尽管需要一个无参函数,但是可以省略书写“() =>”

withClockAndReset(io.clockB, io.resetB) {
    sentence1
    sentence2
    …
    sentenceN
}

实际上相当于:

withClockAndReset(io.clockB, io.resetB)( () => (sentence1; sentence2; …; sentenceN) )

读者再仔细看一看apply方法的定义,它的第二个参数是一个函数,同时该函数的返回结果也是整个apply方法的返回结果

class MultiClockModule extends Module {
   val io = IO(new Bundle {
       val clockB = Input(Clock())
       val resetB = Input(Bool())
       val stuff = Input(Bool())
   })
   
   val clockB_child = withClockAndReset(io.clockB, io.resetB) {
       Module(new ChildModule)
    }

   clockB_child.io.in := io.stuff  
} 

如果传名参数全都是定义,最后没有表达式用于返回,那么apply的返回结果类型自然就是Unit。

class MultiClockModule extends Module {
   val io = IO(new Bundle {
       val clockB = Input(Clock())
       val resetB = Input(Bool())
       val stuff = Input(Bool())
   })
   
   val clockB_child = withClockAndReset(io.clockB, io.resetB) {
       val m = Module(new ChildModule)
    }

   clockB_child.m.io.in := io.stuff  
} 

除了单例对象withClockAndReset,还有单例对象withClock和withReset

使用时钟负沿和低有效的复位信号

可以改变其行为。复位信号比较简单,只需要加上取反符号或逻辑非符号。时钟信号稍微麻烦一些,需要先用asUInt方法把Clock类型转换成UInt类型,再用toBool转换成Bool类型,此时可以加上取反符号或逻辑非符号,最后再用asClock变回Clock类型

// negclkrst.scala
package test
 
import chisel3._
import chisel3.experimental._
 
class NegativeClkRst extends RawModule {
  val io = IO(new Bundle {
    val in = Input(UInt(4.W))
    val myClk = Input(Clock())
    val myRst = Input(Bool())
    val out = Output(UInt(4.W))
  })
  
  withClockAndReset((~io.myClk.asUInt.toBool).asClock, ~io.myRst) {
    val temp = RegInit(0.U(4.W))
    temp := io.in
    io.out := temp
  }
}
 
object NegClkRstGen extends App {
  chisel3.Driver.execute(args, () => new NegativeClkRst)
}

示例:异步FIFO

// FIFO.scala
package fifo
 
import chisel3._
import chisel3.util._
import chisel3.experimental._
 
class FIFO( Int, depth: Int) extends RawModule {
  val io = IO(new Bundle {
    // write-domain
    val dataIn = Input(UInt(width.W))
    val writeEn = Input(Bool())
    val writeClk = Input(Clock())
    val full = Output(Bool())
    // read-domain
    val dataOut = Output(UInt(width.W))
    val readEn = Input(Bool())
    val readClk = Input(Clock())
    val empty = Output(Bool())
    // reset
    val systemRst = Input(Bool())
  })
 
  val ram = SyncReadMem(1 << depth, UInt(width.W))   // 2^depth
  val writeToReadPtr = Wire(UInt((depth + 1).W))  // to read clock domain
  val readToWritePtr = Wire(UInt((depth + 1).W))  // to write clock domain
 
  // write clock domain
  withClockAndReset(io.writeClk, io.systemRst) {
    val binaryWritePtr = RegInit(0.U((depth + 1).W))
    val binaryWritePtrNext = Wire(UInt((depth + 1).W))
    val grayWritePtr = RegInit(0.U((depth + 1).W))
    val grayWritePtrNext = Wire(UInt((depth + 1).W))
    val isFull = RegInit(false.B)
    val fullValue = Wire(Bool())
    val grayReadPtrDelay0 = RegNext(readToWritePtr)
    val grayReadPtrDelay1 = RegNext(grayReadPtrDelay0)
 
    binaryWritePtrNext := binaryWritePtr + (io.writeEn && !isFull).asUInt
    binaryWritePtr := binaryWritePtrNext
    grayWritePtrNext := (binaryWritePtrNext >> 1) ^ binaryWritePtrNext
    grayWritePtr := grayWritePtrNext
    writeToReadPtr := grayWritePtr
    fullValue := (grayWritePtrNext === Cat(~grayReadPtrDelay1(depth, depth - 1), grayReadPtrDelay1(depth - 2, 0)))
    isFull := fullValue
 
    when(io.writeEn && !isFull) {
      ram.write(binaryWritePtr(depth - 1, 0), io.dataIn)
    }
 
    io.full := isFull    
  }
  // read clock domain
  withClockAndReset(io.readClk, io.systemRst) {
    val binaryReadPtr = RegInit(0.U((depth + 1).W))
    val binaryReadPtrNext = Wire(UInt((depth + 1).W))
    val grayReadPtr = RegInit(0.U((depth + 1).W))
    val grayReadPtrNext = Wire(UInt((depth + 1).W))
    val isEmpty = RegInit(true.B)
    val emptyValue = Wire(Bool())
    val grayWritePtrDelay0 = RegNext(writeToReadPtr)
    val grayWritePtrDelay1 = RegNext(grayWritePtrDelay0)
 
    binaryReadPtrNext := binaryReadPtr + (io.readEn && !isEmpty).asUInt
    binaryReadPtr := binaryReadPtrNext
    grayReadPtrNext := (binaryReadPtrNext >> 1) ^ binaryReadPtrNext
    grayReadPtr := grayReadPtrNext
    readToWritePtr := grayReadPtr
    emptyValue := (grayReadPtrNext === grayWritePtrDelay1)
    isEmpty := emptyValue
 
    io.dataOut := ram.read(binaryReadPtr(depth - 1, 0), io.readEn && !isEmpty)
    io.empty := isEmpty
  }  
}
 
object FIFOGen extends App {
  chisel3.Driver.execute(args, () => new FIFO(args(0).toInt, args(1).toInt))
}

第二十三章 Chisel基础——函数的应用

对于Chisel这样的高级语言,函数的使用更加方便,还能节省不少代码量。不管是用户自己写的函数、Chisel语言库里的函数还是Scala标准库里的函数,都能帮助用户节省构建电路的时间

用函数抽象组合逻辑

与Verilog一样,对于频繁使用的组合逻辑电路,可以定义成Scala的函数形式,然后通过函数调用的方式来使用它。这些函数既可以定义在某个单例对象里,供多个模块重复使用,也可以直接定义在电路模块里。

// function.scala
import chisel3._
 
class UseFunc extends Module {
  val io = IO(new Bundle {
    val in = Input(UInt(4.W))
    val out1 = Output(Bool())
    val out2 = Output(Bool())
  })
 
  def clb(a: UInt, b: UInt, c: UInt, d: UInt): UInt =
    (a & b) | (~c & d)
 
  io.out1 := clb(io.in(0), io.in(1), io.in(2), io.in(3))
  io.out2 := clb(io.in(0), io.in(2), io.in(3), io.in(1))
}

用工厂方法简化模块的例化

在Scala里,往往在类的伴生对象里定义一个工厂方法,来简化类的实例化。同样,Chisel的模块也是Scala的类,也可以在其伴生对象里定义工厂方法来简化例化、连线模块

// mux4.scala
import chisel3._
 
class Mux2 extends Module {
  val io = IO(new Bundle {
    val sel = Input(UInt(1.W))
    val in0 = Input(UInt(1.W))
    val in1 = Input(UInt(1.W))
    val out = Output(UInt(1.W))
  })
 
  io.out := (io.sel & io.in1) | (~io.sel & io.in0)
}
 
object Mux2 {
  def apply(sel: UInt, in0: UInt, in1: UInt) = {
    val m = Module(new Mux2)
    m.io.in0 := in0
    m.io.in1 := in1
    m.io.sel := sel
    m.io.out
  }
}
 
class Mux4 extends Module {
  val io = IO(new Bundle {
    val sel = Input(UInt(2.W))
    val in0 = Input(UInt(1.W))
    val in1 = Input(UInt(1.W))
    val in2 = Input(UInt(1.W))
    val in3 = Input(UInt(1.W))
    val out = Output(UInt(1.W))
  })
 
  io.out := Mux2(io.sel(1),
                 Mux2(io.sel(0), io.in0, io.in1),
                 Mux2(io.sel(0), io.in2, io.in3))
}

用Scala的函数简化代码

比如在生成长的序列上,利用Scala的函数就能减少大量的代码。

利用Scala的for、yield组合可以产生相应的判断条件与输出结果的序列,再用zip函数将两个序列组成一个对偶序列,再把对偶序列作为MuxCase的参数,就能用几行代码构造出任意位数的译码器。

// decoder.scala
package decoder

import chisel3._
import chisel3.util._
import chisel3.experimental._

class Decoder(n: Int) extends RawModule {
val io = IO(new Bundle {
val sel = Input(UInt(n.W))
val out = Output(UInt((1 << n).W))
})

val x = for(i <- 0 until (1 << n)) yield io.sel === i.U
val y = for(i <- 0 until (1 << n)) yield 1.U << i
io.out := MuxCase(0.U, x zip y)
}

object DecoderGen extends App {
chisel3.Driver.execute(args, () => new Decoder(args(0).toInt))
}

Chisel的打印函数

printf函数只能在Chisel的模块里使用,并且会转换成Verilog的系统函数“$fwrite”,包含在宏定义块“ `ifndef SYNTHESIS……`endif ”里。通过Verilog的宏定义,可以取消这部分不可综合的代码。因为后导入的chisel3包覆盖了Scala的标准包,所以Scala里的printf函数要写成“Predef.printf”的完整路径形式。
————————————————

Scala风格

Chisel自定义了一个p插值器,该插值器可以对字符串内的一些自定义表达式进行求值、Chiel类型转化成字符串类型等

val myUInt = 33.U
// 显示Chisel自定义的类型的数据
printf(p"myUInt = $myUInt") // myUInt = 33
// 显示成十六进制
printf(p"myUInt = 0x${Hexadecimal(myUInt)}") // myUInt = 0x21
// 显示成二进制
printf(p"myUInt = ${Binary(myUInt)}") // myUInt = 100001
// 显示成字符(ASCⅡ码)
printf(p"myUInt = ${Character(myUInt)}") // myUInt = !

 对于自定义的Bundle类型,可以重写toPrintable方法来定制打印内容。当自定义的Bundle配合其他硬件类型例如Wire构成具体的硬件

class Message extends Bundle {
  val valid = Bool()
  val addr = UInt(32.W)
  val length = UInt(4.W)
  val data = UInt(64.W)
  override def toPrintable: Printable = {
      val char = Mux(valid, 'v'.U, '-'.U)
      p"Message:
" +
      p"  valid  : ${Character(char)}
" +
      p"  addr   : 0x${Hexadecimal(addr)}
" +
      p"  length : $length
" +
      p"  data   : 0x${Hexadecimal(data)}
"
  }
}

val myMessage = Wire(new Message)
myMessage.valid := true.B
myMessage.addr := "h1234".U
myMessage.length := 10.U
myMessage.data := "hdeadbeef".U

printf(p"$myMessage")

C风格

 val myUInt = 32.U
printf(“myUInt = %d”, myUInt) // myUInt = 32

Chisel的对数函数

chisel3.util包里有一个单例对象Log2,它的一个apply方法接收一个Bits类型的参数,计算并返回该参数值以2为底的幂次。返回类型是UInt类型,并且是向下截断的

chisel3.util包里还有四个单例对象:log2Ceil、log2Floor、log2Up和log2Down,它们的apply方法的参数都是Int和BigInt类型,返回结果都是Int类型。log2Ceil是把结果向上舍入,log2Floor则向下舍入。log2Up和log2Down不仅分别把结果向上、向下舍入,而且结果最小为1。

单例对象isPow2的apply方法接收Int和BigInt类型的参数,判断该整数是不是2的n次幂,返回Boolean类型的结果

与硬件相关的函数

Reverse("b1101".U)  // 等于"b1011".U

Reverse("b1101".U(8.W))  // 等于"b10110000".U

Reverse(myUIntWire)  // 动态旋转

单例对象Cat有两个apply方法,分别接收一个Bits类型的序列和Bits类型的重复参数,将它们拼接成一个UInt数。

Cat("b101".U, "b11".U)  // 等于"b10111".U

Cat(myUIntWire0, myUIntWire1)  // 动态拼接

Cat(Seq("b101".U, "b11".U))  // 等于"b10111".U

Cat(mySeqOfBits)  // 动态拼接 

1计数器

分别接收一个Bits类型的参数和Bool类型的序列,计算参数里“1”或“true.B”的个数,返回对应的UInt值

PopCount(Seq(true.B, false.B, true.B, true.B))  // 等于3.U

PopCount(Seq(false.B, false.B, true.B, false.B))  // 等于1.U

PopCount(“b1011”.U)  // 等于3.U

PopCount(“b0010”.U)  // 等于1.U

PopCount(myUIntWire)  // 动态计数
————————————————

独热码转换器

OHToUInt(“b1000”.U)  // 等于3.U

OHToUInt(“b1000_0000”.U)  // 等于7.U 

无关位

Verilog里可以用问号表示无关位,那么用case语句进行比较时就不会关心这些位。Chisel里有对应的BitPat类,可以指定无关位。

“b10101”.U === BitPat(“b101??”) // 等于true.B

“b10111”.U === BitPat(“b101??”) // 等于true.B

“b10001”.U === BitPat(“b101??”) // 等于false.B 

dontCare方法接收一个Int类型的参数,构造等值位宽的全部无关位。例如:

val myDontCare = BitPat.dontCare(4)  // 等于BitPat(“b????”) 

一种是单例对象Lookup,其apply方法定义

def apply[T <: Bits](addr: UInt, default: T, mapping: Seq[(BitPat, T)]): T 

第二种是单例对象ListLookup,它的apply方法与上面的类似,区别在于返回结果是一个T类型的列表

defapply[T <: Data](addr: UInt, default: List[T], mapping: Array[(BitPat, List[T])]): List[T] 

第二十四章 Chisel基础——其它议题

动态命名模块

转成Verilog时的模块名不使用定义的类名,而是使用重写的desiredName方法的返回字符串。模块和黑盒都适用

class Coffee extends BlackBox {
   val io = IO(new Bundle {
       val I = Input(UInt(32.W))
       val O = Output(UInt(32.W))
   })
   override def desiredName = "Tea"
}

class Salt extends Module {
   val io = IO(new Bundle {})
   val drink = Module(new Coffee)
   override def desiredName = "SodiumMonochloride"
}

动态修改端口

选值以及if语句可以创建出可选的端口,在例化该模块时可以通过控制Boolean入参来生成不同的端口

class ModuleWithOptionalIOs(flag: Boolean) extends Module {
   val io = IO(new Bundle {
       val in = Input(UInt(12.W))
       val out = Output(UInt(12.W))
       val out2 = if (flag) Some(Output(UInt(12.W))) else None
  })
  
   io.out := io.in
   if(flag) {
     io.out2.get := io.in
   }
} 

生成正确的块内信号名

在when、withClockAndReset等语句块里定义的信号(线网和寄存器),转换成Verilog时不会生成正确的变量名

package test
 
import chisel3._
 
class TestMod extends Module {
  val io = IO(new Bundle {
    val a = Input(Bool())
    val b = Output(UInt(4.W))
  })
  when (io.a) {
    val innerReg = RegInit(5.U(4.W))
    innerReg := innerReg + 1.U
    io.b := innerReg
  } .otherwise {
    io.b := 10.U
  }
}
 
object NameGen extends App {
  chisel3.Driver.execute(args, () => new TestMod)

如果想让名字正确,则需要在build.sbt文件里加上:

addCompilerPlugin(“org.scalamacros” % “paradise” % “2.1.0” cross CrossVersion.full) 

拆包一个值(给拼接变量赋值)

class MyBundle extends Bundle {
  val a = UInt(2.W)
  val b = UInt(4.W)
  val c = UInt(3.W)
}

val z = Wire(UInt(9.W))
z := ...
val unpacked = z.asTypeOf(new MyBundle)
unpacked.a
unpacked.b
unpacked.c

子字赋值

 在Verilog中,可以直接给向量的某几位赋值。同样,Chisel受限于Scala,不支持直接给Bits类型的某几位赋值

办法是先调用Bits类型的toBools方法。该方法根据调用对象的0、1排列返回一个相应的Seq[Bool]类型的结果,并且低位在序列里的下标更小,比如第0位的下标就是0、第n位的下标就是n。然后用这个Seq[Bool]对象配合VecInit构成一个向量,此时就可以给单个比特赋值。注意,必须都是Bool类型,要注意赋值前是否需要类型转换。子字赋值完成后,Bool向量再调用asUInt、asSInt方法转换回来

class TestModule extends Module {
   val io = IO(new Bundle {
       val in = Input(UInt(10.W))
       val bit = Input(Bool())
       val out = Output(UInt(10.W))
   })
   val bools = VecInit(io.in.toBools)
   bools(0) := io.bit
   io.out := bools.asUInt
}

参数化的Bundle

Chisel提供了一个内部的API函数cloneType,任何继承自Data的Chisel对象,要复制自身时,都是由cloneType负责返回该对象的复制对象当自定义的Bundle的主构造方法没有参数时,Chisel会自动推断出如何构造Bundle对象的复制,原因很简单,因为构造一个新的复制对象不需要任何参数,仅仅使用关键字new就行了。但是,如果自定义的Bundle带有参数列表,那么Chisel就无法推断了,因为传递进去的参数可以是任意的,并不一定就是完全地复制。此时需要用户自己重写Bundle类的cloneType方法override def cloneType = (new CustomBundle(arguments)).asInstanceOf[this.type]

class ExampleBundle(a: Int, b: Int) extends Bundle {

   val foo = UInt(a.W)

   val bar = UInt(b.W)

   override def cloneType = (new ExampleBundle(a, b)).asInstanceOf[this.type]

}

 

class ExampleBundleModule(btype: ExampleBundle) extends Module {

   val io = IO(new Bundle {

       val out = Output(UInt(32.W))

       val b = Input(chiselTypeOf(btype))

   })

   io.out := io.b.foo + io.b.bar

}

 

class Top extends Module {

   val io = IO(new Bundle {

       val out = Output(UInt(32.W))

       val in = Input(UInt(17.W))

   })

   val x = Wire(new ExampleBundle(31, 17))

   x := DontCare

   val m = Module(new ExampleBundleModule(x))

   m.io.b.foo := io.in

   m.io.b.bar := io.in

   io.out := m.io.out

}

Chisel泛型

无论是Chisel的函数还是模块,都可以用类型参数和上、下界来泛化。在例化模块时,传入不同类型的参数,就可能会产生不同的电路,而无需编写额外的代码,当然前提是逻辑、类型必须正确。

未驱动的线网

Chisel的Invalidate API支持检测未驱动的输出型IO以及定义不完整的Wire定义,在编译成firrtl时会产生“not fully initialized”错误。换句话说,就是组合逻辑的真值表不完整,不能综合出完整的电路。如果确实需要不被驱动的线网,则可以赋给一个DontCare对象,这会告诉Firrtl编译器,该线网故意不被驱动。转换成的Verilog会赋予该信号全0值

val io = IO(new Bundle {
    val outs = Output(Vec(10, Bool()))
})
io.outs <> DontCare 

第二十五章 Chisel进阶——隐式参数的应用

用Chisel编写的CPU,比如Rocket-Chip、RISCV-Mini等,都有一个特点,就是可以用一个配置文件来裁剪电路。这利用了Scala的模式匹配、样例类、偏函数、可选值、隐式定义等语法

相关定义

Chisel提供了一个内部的API函数cloneType,任何继承自Data的Chisel对象,要复制自身时,都是由cloneType负责返回该对象的复制对象

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注