chisel学习笔记
Basic Components
Chisel Types and Constants
Chisel提供三种数据类型来描述连接、组合逻辑和寄存器:
- Bits (8.W)
- UInt (8.W)
- SInt (10.W)
下面的表达式将Scala整数n转换为Chisel宽度:n.W
,然后可以使用他来定义bit向量Bits(n.W)
常量也可以通过使用Chisel宽度类型来定义宽度:3.U(4.W)
,An 4-bit constant of 3
如果要使用其他进制的常数,可以按照下面的格式定义
1 | "hff".U // hexadecimal representation of 255 |
chisel中也可以使用字符数据,不过是他们的ascii码
1 | val aChar = ’A’.U |
同时还可以使用逻辑值,但是和scala中的类型不同,scala的类型为Boolean,chisel的为Bool
1 | Bool() // 类型 |
chisel运算结果的宽度:加减法中操作数最大的宽度;乘法两个操作数宽度之和;除法和模运算则是分子宽度
1 | val and = a & b // bitwise and |
UInt、SInt和Bits是Chisel类型,它们本身并不代表硬件。只有将它们包装成Wire、Reg或IO才会产生硬件。
Wire、Reg、IO能够包装任何chisel类型,包括Bundle和Vec。在创建硬件对象(并给它一个名字)时使用Scala的”=”操作符,但在给现有硬件对象赋值或重新赋值时使用Chisel的”:=”操作符。
1 | val number = Wire(UInt ()) |
Wire代表组合逻辑、Reg代表寄存器、IO代表模块的端口
同时还可以使用WireDefault给Wire一个默认值
1 | val number = WireDefault (10.U(4.W)) |
Bundle
一个Chisel Bundle将几个信号分组,要使用Bundle,需要将其用Wire或者Reg包起来
1 | class Channel () extends Bundle { |
由于在chisel中部分赋值是不允许的(但在verilog中可以),如下面的例子是非法的
1 | val assignWord = Wire(UInt (16.W)) |
但是可以使用Bundle来解决,最后将bundle转换成对应类型即可
1 | val assignWord = Wire(UInt (16.W)) |
Vec
一个Chisel Vec(一个向量)代表相同类型的Chisel类型的集合,每个元素都可以通过一个索引来访问。Chisel Vec类似于其他编程语言中的数组,Vec是个类型!并不是个硬件模块!
使用vec的三个目的:
- 硬件中的动态寻址,这是一个多路选择器
- 寄存器文件
- 参数
要使用Vec需要用chisel类型将其包裹起来,下面的例子是一个三路的多路选择器,通过索引来选择
1 | val v = Wire(Vec(3, UInt (4.W))) |
也可以给Vec设置初始值,VecInit本身就是个硬件模块
1 | val defVec = VecInit (1.U(3.W), 2.U, 3.U) // 根据第一个数据类型来推断 |
下面的例子是一个寄存器文件,使用Reg类型来包裹
1 | val registerFile = Reg(Vec(32, UInt (32.W))) |
Combining Bundle and Vec
可以结合Bundle和Vec一起使用
1 | class BundleVec extends Bundle { |
Module 1: Introduction to Scala
scala使用var和val来创建变量或者常量,但是尽可能使用val来创建,scala无需分号分隔语句,其会根据换行符来推断,唯一需要使用分号的情况是需要将多个语句放在一行上。
1 | var numberOfKittens = 6 |
scala的条件语句和其他语言一样
1 | if (done) { |
但是“if”块会返回一个值,返回值是条件块所选择分支的最后一行
1 | val likelyCharactersSet = if (alphabet.length == 26) |
使用def来声明函数(方法),没有任何参数的 Scala 函数不需要空括号。按照惯例,没有副作用的无参数函数(即调用它们不会改变任何东西,它们只是返回一个值)不使用括号,而具有副作用的函数(可能它们改变类变量或打印出东西)应该使用括号
scala不建议使用return语句返回函数值,因为是函数式编程。scala会自动将最后一个表达式的结果作为返回值。因此可以将要返回的值写到函数末即可,无需return语句
1 | // Simple scaling function with an input argument, e.g., times2(3) returns 6 |
scala支持函数重载,但是不建议使用重载
1 | // Overloaded function |
scala支持嵌套函数,但是嵌套函数只能使用外层函数作用域里的变量
1 | def asciiTriangle(rows: Int) { |
scala的list和python的类似,是个动态数据类型,但是数据初始化的方式不太一样,用::来初始化列表需要在末尾添加个Nil
1 | val x = 7 |
for循环和python的更像一点,需要指明范围迭代的范围,可以使用by来指明步长。for循环能直接迭代集合
1 | // 0-7 |
scala是个面向对象的语言,有类的概念,一个类的例子如下所示
1 | // WrapCounter counts up to a max value based on a bit size |
- (counterBits: Int):创建这个类需要一个整数参数counterBits,就是构造函数
- 0L:说明为长整数
- println(s”counter created with max value $max”)
- s:表示这是个内插字符串
- $max:运行时会被替换从变量max的值
- ${}:可以用来插入表达式来计算,如${max * 3}
创建类的实例的方法和java差不多
1 | val x = new WrapCounter(2) |
代码块由括号分割,代码块的返回值为最后一行,如果代码块为空会返回一个Unit对象
We should use this when we do not want our function to return any value.
匿名函数,和java的lambda表达式类似
1 | val intList = List(1, 2, 3) |
函数还可以指令参数默认值
1 | def myMethod(count: Int, wrap: Boolean, wrapValue: Int = 24): Unit = { ... } |
调用函数时可以指明参数赋值,顺序可以不和定义的时候一样
1 | myMethod(count = 10, wrap = false, wrapValue = 23) |
scala中有元组的概念,使用括号来创建元组,和python一样,但是引用元组中的元素要使用_i
,注意要有下划线。元组可以用在函数返回中,用于返回多个值。
1 | val city = (2000 , " Frederiksberg ") |
Module 2.1: Your First Chisel Module
使用chisel前需要导入包
1 | import chisel3._ |
下面是一个简单的chisel模块代码,该电路模块有一个四位的输入端口in和一个4位的输出端口out
1 | // Chisel Code: Declare a new module definition |
- class Passthrough extends Module:所有的硬件模块都要extends Module类
- val io = IO(…):一个模块的输入输出端口是一个IO对象的实例
- 声明了一个新的硬件结构类型(Bundle) ,它分别包含一些输入和输出方向的命名信号
- 要在Bundle中声明输入输出信号的类型和位宽,如UInt(4.W)为四位的无符号整数
- :=表示右边信号驱动左边信号
要获得verilog代码,需要使用scala来调用chisel编译器来将chisel代码转换为verilog代码,这个过程叫elaboration
1 | // Scala Code: Elaborate our Chisel design by translating it to Verilog |
生成的verilog代码
1 | module Passthrough( |
由于chisel的模块是用scala的类实现的,因此可以使用scala类的语法特性,可以给chisel模块设置构造参数
1 | // Chisel Code, but pass in a parameter to set widths of ports |
chisel测试代码的例子,用来测试chisel硬件模块,poke和expect传入的数据类型要和端口类型相匹配
1 | // Scala Code: `test` runs the unit test. |
注意chisel中使用printf
和println
的规则:
printf
会在每个时钟周期都打印信息,println
只会在第一次生成模块的时候打印printf
只能写在用户定义的Module中;并且必须要有时钟和复位,否则生成verilog报错
1 | class PrintingModule extends Module { |
Module 2.2: Combinational Logic
scala和chisel的运算符看起来是相同的,但是操作数不一样会导致结果类型不一样
1 | class MyModule extends Module { |
- 第一个函数add两个 Scala Ints,所以 println 输出整数2
- 第二个 val 将两个 Chisel UInt 相加,因此 println 将其视为一个硬件节点,并打印出类型名称和指针
如果两个操作数的类型不是同一个,如一个是scala,另一个是chisel,编译器会报错类型不匹配。Scala 是一种强类型语言,因此任何类型转换都必须是显式的
1 | class MyModuleTwo extends Module { |
chisel支持的操作符还有减、乘、连接、mux
1 | class MyOperatorsTwo extends Module { |
利用scala特性,创建参数化模块。下面这个例子是一个参数化的加法器,因为加法最后结果的位宽是操作数的最大位宽,最后的操作结果可能会超出这个位宽,可以使用+&,具体作用见上面的图。饱和加法器会让最后操作数大于位宽的时候赋给结果当前位宽的最大值
1 | // Boolean为scala的类型 |
最后生成的verilog代码
1 | module ParameterizedAdder( |
Module 2.3: Control Flow
chisel可以向同一组件发出多个 connect 语句。当这种情况发生时,最后的声明获胜。
下面这个例子中最后的输出是由最后一次声明决定的,所以输出是4.U
1 | class LastConnect extends Module { |
chisel的控制流语句和verilog的不太一样,不是使用if、else if和else,而是使用when、elsewhen和otherwise。可以对scala对象使用if else,但是对于chisel对象只能使用when,下面是例子
1 | // Max3 returns the max of its 3 arguments |
生成的verilog,可以看到生成了条件表达式
1 | module Max3( |
要注意,**chisel中的相等判断是使用三个等于号===,不等判断是=/=**,和其他的有区别
scala的列表模式匹配的另类用法,常用于有限状态机中的固定参数parameter设置,枚举状态名的首字母要小写,这样Scala的编译器才能识别成变量模式匹配
1 | val sNone :: sOne1 :: sTwo1s :: Nil = Enum(3) |
chisel中也有switch语句,但不是switch case,而是switch is
Module 2.4: Sequential Logic
Reg
chisel中的寄存器为Reg,寄存器会在每个时钟上升沿更新状态。默认情况下,所有的chisel模块都有一个隐藏的时钟信号,会连接到模块中的所有寄存器上,所以默认情况下一个模块中所有的寄存器是共用一个时钟信号的,都在时钟上升沿更新状态。下面是一个使用寄存器的例子
1 | class RegisterModule extends Module { |
寄存器使用Reg(tpe)
来创建,tpe
指明寄存器的编码类型,上面的例子是12位的UInt类型
注意使用chisel测试时序逻辑的方法,每次都要让时钟信号前进,使用c.clock.step(1)来前进一个时钟周期
下面是生成的verilog代码
1 | module RegisterModule( |
注意:chisel区分类型(UInt)和硬件结点(2.U或者myReg)!
1 | val myReg = Reg(UInt(2.W)) // ok,because a Reg needs a data type as a model, |
RegNext
RegNext不需要指定寄存器位宽。它是从寄存器的输出连接推断出来的。下面用RegNext替换了上面的Reg,此时是根据输出端口io.out来推断寄存器类型和位宽
1 | class RegNextModule extends Module { |
RegInit
前面两种寄存器模块的初始化都是随机数,要想指定明确初始化数据的寄存器,需要使用RegInit
模块,指定reset值,有两种使用方法:
1 | val myReg = RegInit(UInt(12.W), 0.U) |
第二种方法还可以使用VecInit来初始化寄存器文件
1 | val initReg = RegInit(VecInit(0.U(3.W), 1.U, 2.U)) |
生成的verilog,可以看到采用的是高电平同步复位,在使用reset复位之前,寄存器的值还是随机数
1 | module RegInitModule( |
RegEnable
RegEnable
是带有使能信号的寄存器,其中使能信号是高有效,采用如下的方式定义,第一个参数是输入端口,第二个参数是复位初始值,第三个参数是使能信号
1 | val resetEnableReg2 = RegEnable(inVal , 0.U(4.W), enable) |
控制流
寄存器的控制流,仍然有最后连接的语义,最后连接的生效
1 | class FindMax extends Module { |
生成的verilog代码
1 | module FindMax( |
可以指定寄存器输出的类型,便于后续的操作(虽然我感觉没有甚么不同)
1 | class Comb extends Module { |
生成的verilog
1 | module Comb( |
案列:移位寄存器
下面是一个移位寄存器的例子,注意:在chisel中不允许给子字赋值,如out(0) := in
是非法的!
1 | // n is the output width (number of delays - 1) |
显式时钟和复位
chisel提供了显式时钟和复位,使用withClock() {}
, withReset() {}
, and withClockAndReset() {}
来构造显式时钟和复位
- 复位总是同步复位,类型为Bool
- 时钟的类型为Clock
- chisel的tester并未提供多时钟设计
下面是一个多时钟模块的例子,需要导入with模块
1 | // we need to import multi-clock features |
生成的verilog,可以看到复位信号仍是两个都在生效,有点懵逼。。。。
1 | module ClockExamples( |
Module 2.5: Putting it all Together: An FIR Filter
滤波器
$y[n] = b_0 x[n] + b_1 x[n-1] + b_2 x[n-2] + …$
- $y[n]$ is the output signal at time $n$
- $x[n]$ is the input signal
- $b_i$ are the filter coefficients or impulse response
- $n-1$, $n-2$, … are time $n$ delayed by 1, 2, … cycles
1 | class My4ElementFir(b0: Int, b1: Int, b2: Int, b3: Int) extends Module { |
注意:b0、b1、b2、b3都是scala的整数类型,不能直接和chisel类型进行相加减!
Module 3.1: Generators: Parameters
可以使用require语句来限制传给chisel对象的参数
1 | class ParameterizedWidthAdder(in0Width: Int, in1Width: Int, sumWidth: Int) extends Module { |
由于chisel对象也是个类,因此可以在chisel模块中定义函数
1 | /** Sort4 sorts its 4 inputs to its 4 outputs */ |
可以使用Option来传递参数,这样在创建模块的时候可以不提供参数或者提供参数。下面的例子是如果提供了参数就用参数值作为reset值
1 | class DelayBy1(resetValue: Option[UInt] = None) extends Module { |
Match/Case Statements
第一个案例,value matching。下面的例子中x的值由y的值来定
1 | // y is an integer variable defined somewhere else in the code |
- case会按照顺序向下搜索,知道遇到末尾的大括号
- 使用下划线来匹配其他项,类似于default
- 如果匹配到一项就不会再去匹配了
第二个案例是多值匹配,case可以同时匹配多个值,详见下面的代码
1 | def animalType(biggerThanBreadBox: Boolean, meanAsCanBe: Boolean): String = { |
第三个案例是类型匹配,不仅可以匹配值,还可以匹配参数的类型
1 | val sequence = Seq("a", 1, 0.0) |
还可以匹配多个类型,但是在使用的时候要使用下划线
1 | val sequence = Seq("a", 1, 0.0) |
但是类型匹配有一些局限性,无法匹配多态类型,多态在运行时都被消除了。下面的例子中所有的类型都是String
1 | val sequence = Seq(Seq("a"), Seq(1), Seq(0.0)) |
可以在chisel模块中运用match,下面的例子使用match/case代替了之前的if/else
1 | class DelayBy1(resetValue: Option[UInt] = None) extends Module { |
What is Some and Option?
Option
is a data structure that represents optionality, as the name suggests. Whenever a computation may not return a value, you can return anOption
.Option
has two cases (represented as two subclasses):Some
orNone
.
对option使用match,对应的值是Some或者None,如果有值就是Some,否则为None
IOs with Optional Fields
可以提供可以隐藏的io端口,便于调试。下面提供了两种方式。第一种方式是Some和None,第二种方式是使用位宽为0的信号来代替None。注意第一种方式,为什么要用Some来构造,因为None是option的子类,要保持类型一致(Some也是option的子类)
1 | class HalfFullAdder(val hasCarry: Boolean) extends Module { |
1 | class HalfFullAdder(val hasCarry: Boolean) extends Module { |
Module 3.2: interlude
Decoupled
是一个带有握手信号的接口,可以将chisel的类型包裹起来
1 | val tmp = UInt(8.W) |
Queue
是一个fifo队列,其两端的接口使用的是Decoupled类型,都有valid和ready信号,下面是一个使用的例子。当队列不为空时,队列出口处valid信号有效;当队列未满时,入口处的ready信号有效;当队列的入口处的valid信号有效且队列未满时,会在时钟上升沿将in.bits中的数据入队;当队列出口的ready信号有效且队列未空时,队列会在时钟上升沿弹出队首元素
1 | class Test extends Module { |
Arbiter
是个n-1仲裁器,优先级基于索引,低索引的优先级高。同样n+1个接口都是DecoupledIO。下面是一个使用的例子,一个2-1仲裁器。
1 | class Test extends Module { |
- 还有另一个基于轮询机制的仲裁器
RRArbiter
PopCount
可用于统计输入数据中1的个数,Reverse
可以将输入数据进行位反转
1 | val io = IO(new Bundle{ |
UIntToOH
可以将二进制转为独热码,OHToUInt
可以将独热码转为二进制
1 | io.out := UIntToOH(io.in) // 4'd8 -> 16'b10000000 |
PriorityMux
是一个优先级多路选择器,会选择sel信号中最低有效信号处所选择的数据。此处需要每个输入数据对应一个选择信号,如有8个输入数据则需要选择信号为8位,每位选择信号选择一路数据
1 | val io = IO(new Bundle { |
Mux1H
是一个多路选择器,前提是只有一个选择信号有效,否则行为是未定义的。即选择信号必须是独热编码,选择对应路的数据
1 | io.out := Mux1H(io.in_sels, io.in_bits) |
Types
chisel和scala类型的区别
1 | val bool = Wire(Bool()) // chisel |
scala的强制类型转换,使用x.asInstanceOf[T]
来将x强制转换成T
1 | val x: UInt = 3.U |
chisel中使用asTypeOf()
来进行强制类型转换,例如把UInt
转换为SInt
是非法的,但是可以使用强制类型转换,例如下面的例子
1 | class TypeConvertDemo extends Module { |
switch is
chisel使用switch/is来代替switch/case,下面的例子是个译码器的代码
1 | switch (a) { |
Connections
chisel的模块互联是使用如下形式,就是new一个对象
1 | class CompA extends Module { |
一个一个端口的连接很麻烦,chisel提供了一种bulk connection,使用符号<>
来直接连接两个模块的io即可,前提是端口的名字要相同,名字不同的端口不会被连接,会被忽略
1 | val fetch = Module (new Fetch ()) |
Memory
chisel中有两种存储器类型可以使用:
- 同步写,异步读(组合逻辑):
Mem
- 同步读写:
SyncReadMem
使用如下方法定义存储器:
1 | // 其实就是16个32位的寄存器 |
存储器的使用,来自官网同步读写存储器的示例,异步的方法也是一样的,只是使用Mem
来替代SyncReadMem
1 | import chisel3._ |
chisel还支持读取文件内容进存储器的方法loadMemoryFromFile
,转换成verilog会生成$readmemh
系统函数
1 | // 需要使用这两个包 |
Hardware Generators
使用scala的函数来构建Hardware Generators。下面的代码在执行期间不执行任何加法操作,而是创建两个加法器(硬件实例)。或者换句话说,它返回一条连接到加法器输出端的线路。
1 | def adder(x: UInt , y: UInt) = { |
还可以进行嵌套
1 | def delay(x: UInt) = RegNext(x) |
使用元组来返回多个输出
1 | def compare (a: UInt , b: UInt) = { |
参数化配置也是Hardware Generators,下面是一种新的定义方式。[T <: Data]
定义了参数T
的类型是Data或者Data的子类,其中Data是所有chisel类型的根类型,也是Bundle类型的根类型,因此也可以传入Bundle
1 | // 定义函数 |
使用fPath.cloneType
来获得参数的类型
1 | def myMuxAlt [T <: Data](sel: Bool , tPath: T, fPath: T): T = { |
但是在克隆Bundle类型时要注意,如果被克隆的Bundle类型也使用了参数化的配置,可能会产生问题,因此要将Bundle中的配置设为私有
1 | class Port[T <: Data](private val dt: T) extends Bundle { |
todo
BlackBox
todo
一些例子
编码器
可以使用switch is
来进行编码,但是当编码是数目多的时候使用for
循环更方便
下面是一个编码器的代码,其中v(15)
是最终的编码值(假设输入为独热编码),使用(... | v(i-1))
来将最先为1的位所编码出的值传播
1 | val v = Wire(Vec(16, UInt (4.W))) |
仲裁器
仲裁器电路如下,每次只grant一个请求,最低位优先
可以使用两种方式来实现仲裁器,第一种是switch is,第二种是for循环,这里采用for循环
1 | val grant = VecInit.fill(n)(false.B) |
优先编码器
由于chisel不允许casez和casex这一类型的语句,因此不能使用verilog中的优先编码方式,但是可以采用仲裁器和编码器来实现优先编码器!
具体实现如下图所示