基本组件
chisel 类型和常数
Chisel提供了三种数据类型来描述连接、组合逻辑和寄存器:Bits,UInt,和SInt。UInt和SInt扩展了Bits,并且所有三种类型都表示位的向量。UInt为这个位向量赋予了无符号整数的含义,SInt为有符号整数的含义。Chisel使用2的补码作为有符号整数表示。以下是不同类型的定义,8位位,8位无符号整数和10位有符号整数:
Bits(8.W)
UInt(8.W)
SInt(10.W)
位向量的宽度由chisel宽度类型(Width)定义。下面的表达式将Scala整数n转换为Chisel宽度,我们将其用于Bits向量的定义:
n.W
Bits(n.W)
常量可以通过使用Scala整数并将其转换为Chisel类型来定义:
0.U //定义一个UInt类型的常数0
-3.S//定义了一个SInt类型的常数-3
常量也可以通过宽度来定义,使用Chisel width
3.U(4.W)//一个4bit的UInt常数3
Chisel受益于Scala的类型推断,在许多地方可以省略类型信息。这对于位宽也是有效的。在许多情况下,Chisel会自动推断正确的宽度。因此,硬件的Chisel描述比VHDL或Verilog更简洁,可读性更好。
对于以十进制以外的其他进制定义的常数,该常数定义在字符串中,前面的h表示十六进制(基数为16),o表示八进制(基数为8),b表示二进制(基数为2)。下面的例子显示了常数255在不同基中的定义。在这个例子中,我们省略了位宽,Chisel推断出适合常数的最小宽度,在这个例子中是8位。
"hff".U //16进制的255
"o377".U //8进制的255
"b1111_1111".U //2进制的255
上面的代码演示了如何使用下划线对表示常量的字符串中的数字进行分组。下划线将被忽略。
表示文本的字符(ASCII编码)也可以用作Chisel中的常量:
val aChar='A'.U
为了表示逻辑值,Chisel定义了Bool类型。Bool可以表示true或false值。下面的代码通过将Scala布尔常量true和false转换为Chisel布尔常量来显示类型Bool的定义和Bool常量的定义。
Bool()
true.B
false.B
组合电路
Chisel使用布尔代数运算符,因为它们在C,Java,Scala和其他几种编程语言中定义,以描述组合电路:&是AND运算符,|是OR运算符。下面的代码行定义了一个电路,该电路将信号a和b与与门组合,并将结果与信号c与或门组合,并将其命名为逻辑。
val logic=( a & b ) | c
在这个例子中,我们不定义信号逻辑的类型和宽度。两者都是从表达式的类型和宽度推断出来的。Chisel中的标准逻辑操作是:
val and = a & b
val or = a | b
val xor = a ^ b
val not = ~a
算术运算使用标准运算符:
val add = a + b
val sub = a - b
val neg = -a
val mul = a * b
val div = a / b
val mod = a % b
运算的结果宽度是加法和减法运算符的最大宽度,乘法是两个宽度之和,除法和模运算的宽度通常是分子的宽度。
信号也可以首先定义为某种类型的Wire。之后,我们可以使用:=来升级操作符。
val w = Wire(UInt())
w:=a&b
单个比特可以如下提取:
val sign = x(31)
可以从结束到开始位置提取子字段:
val lowByte = largeWord(7,0)
位字段用##运算符连接
val word=highByte ## lowByte
多路复用器
多路复用器是在备选方案之间进行选择的电路。在最基本的形式中,它在两个选项之间进行选择。图2.2显示了这样一个2:1多路复用器,简称为mux。取决于选择信号(sel)的值,信号y将表示信号a或信号B。
多路复用器可以由逻辑构建。然而,由于多路复用是这样的标准操作,Chisel提供多路复用器:
val result = Mux (sel, a, b)
其中,当sel为true.B时选择a,否则选择b。sel的类型是Chisel Bool;输入a和b可以是任何Chisel基类型或聚合(束或向量),只要它们是相同的类型。
用逻辑运算、算术运算和多路复用器,每一个组合电路都可以被描述。然而,Chisel提供了进一步的组件和控制抽象,以便更优雅地描述组合电路,这些将在后面的章节中描述。
描述数字电路所需的第二个基本组件是状态元素,也称为寄存器,下面将对其进行描述。
寄存器
Chisel提供了一个寄存器,它是D触发器的集合。该寄存器隐式连接到全局时钟,并在上升沿更新。当在寄存器的声明处提供初始化值时,它使用连接到全局复位信号的同步复位。寄存器可以是可以表示为位集合的任何Chisel类型。以下代码定义了一个8位寄存器,复位时初始化为0:
val reg = RegInt(0.U(8.W))
输入通过:= 升级操作符连接到寄存器,寄存器的输出可以仅与表达式中的名称一起使用:
reg := d
val q = reg
一个寄存器也可以连接到它的输入在定义:
val nextReg = RegNext(d)
图2.3显示了我们定义的寄存器电路,它有一个时钟、一个同步复位到0.U、输入d和输出q。全局信号clock和reset隐式地连接到每个定义的寄存器。
一个寄存器也可以连接到它的输入和一个常数作为初始值在定义:
val bothReg = RegNext (d,0.U)
为了区分表示组合逻辑的信号和寄存器,一个常见的做法是在寄存器名后面加上Reg
计数
计数是数字系统中的基本操作。人们可以计算事件。然而,更常见的是使用计数来定义时间间隔。对时钟周期进行计数并在时间间隔到期时触发动作。
一个简单的方法是计数到一个值。然而,在计算机科学和数字设计中,计数从0开始。因此,如果我们想数到10,我们从0数到9。下面的代码显示了这样一个计数器,它计数到9,当达到9时返回到0。
val cntReg = RegInt(0.U(8.W))
cntReg := Mux(cntReg == 9.U, 0.U, cntReg + 1.U)
Structure with Bundle and Vec
Chisel提供两种结构来分组相关信号:(1)Bundle和(2)Vec。Bundle将不同类型的信号分组为命名字段。Vec表示相同类型的信号(元素)的可索引集合。Bundle和Vec创建新的用户定义的Chisel类型,并且可以任意嵌套。
Bundle
一个Chisel Bundle 组织了一系列信号。整个Bundle可以作为一个整体引用,也可以通过其名称访问单个字段。Bundle类似于C和SystemVerilog中的struct或VHDL中的record。我们可以通过定义一个扩展Bundle的类来定义一个Bundle(信号的集合),并在构造函数块中将字段列为vals。
class Channel() extends Bundle {
val data = UInt(32.W)
val valid = Bool()
}
要使用bundle,我们使用new创建它并将其包装到Wire中。使用点表示法访问这些字段:
val ch =Wire (new Channel())
ch.data := 123.U
ch.valid : true.B
val b =ch.valid
点表示法在面向对象语言中很常见,其中x.y表示x是对对象的引用,y是该对象的字段。由于Chisel是面向对象的,我们使用点符号来访问bundle中的字段。一个bundle也可以作为一个整体引用:
val ch = Wire (new Channel())
val channel = ch
Vec
Chisel Vec (a vector) 表示相同类型的Chisel类型的集合。每个元素都可以通过索引访问。Chisel Vec类似于其他编程语言中的数组数据结构。
Vec用于三个不同的目的:
- 硬件中的动态寻址,其是多路复用器;
- 寄存器堆,其包括多路复用读取并产生用于写入的使能信号;
- 如果模块的端口的数量,则参数化。
对于其他事物的集合,无论是硬件元素还是其他生成器数据,最好使用Scala集合Seq
组合向量 Combinational Vec
Vec是通过使用两个参数调用构造函数来创建的:元件的数量和元件的类型。需要将组合Vec包装到Wire中:
val v = Wire(Vec(3,UInt(4.W))
使用(index)访问单个元素。包装到Wire中的向量只是一个多路复用器。
v(0) := 1.U
v(1) := 3.U
v(2) := 5.U
val index = 1.U(2.W)
val a = v (index)
下面是使用Vec作为多路复用器的另一个示例。三个输入连接到三根导线x、y和z。选择线选择使用哪个输入并将其连接到muxOut。
val vec =Wire(Vec(3,UInt(8.W)))
vec(0) := x
vec(1) := y
vec(2) := z
val muxOut = vec (select)
图2.4显示了上述代码片段的结果示意图。
与使用WireDefault类似,我们可以使用VecInit设置Vec的默认值。以下代码表示具有三个常量默认值的3:1多路复用器。注意,我们用第一个常量指定了UInt数据类型的大小(3位)。使用条件(cond),我们可以覆盖这些默认值。该重写硬件本身由三个2:1多路复用器组成。最后一行选择defVec多路复用器三个输入中的一个。注意,VecInit已经返回了Chisel硬件,我们不需要将其包装到Wire中。
val defVec = VecInit(1.U(3.W),2.U,3.U)
when (cond){
defVec(0) := 4.U
defVec(1) := 5.U
defVec(2) := 6.U
}
val vecOut =defVec(sel)
不仅可以为Vec输入设置初始常量(如WireDefault),还可以使用VecInit将信号(线)连接到Vec的输入。以下示例将导线d、e和f连接到Vec的三个输入
val defVecSig = VecInit(d,e,f)
val vecOutSig = defVecSig(sel)
寄存器向量 Register Vec
我们还可以将Vec包装到寄存器中以定义寄存器数组。以下代码显示了一个包含三个寄存器的向量。
val regVec =Reg(Vec(3,UInt(8.W)))
val dout = regVec(rdIdx)
regVec(wrIdx) := din
图2.5显示了该电路的原理图。它包含三个寄存器。读索引(rdIdx)选择连接到三个寄存器的输出的多路复用器。输出信号为dout。写索引(wrIdx)选择将来自din的数据写入哪个寄存器。wrIdx驱动选择寄存器的三个使能信号之一的解码器。
以下示例定义了用于处理器的寄存器文件; 32个寄存器,每个32位宽,如经典的32位RISC处理器,如32位版本的RISC-V。
val registerFile =Reg(Vec(32,UInt(32.W))
该寄存器堆的元素用索引访问并用作正常寄存器.
registerFile(index):= dIn
val dOut =registerFile (index)
向量的寄存器也可以被初始化。这就是寄存器复位到的值。为了初始化寄存器文件,我们使用A VecInit和用于重置的常量,包装到RegInit中。三个寄存器的输入然后连接到导线a、b和c。
val initReg =RegInit(VecInit(0.U(3.W),1.U,2.U))
val resetVal = initReg(sel)
initReg(0) :=a
initReg(1) :=b
initReg(2) :=c
如果我们想将一个大型寄存器文件的所有元素重置为相同的值(可能是0),我们可以使用Sala序列Seq。VecInit可以用包含Chisel类型的序列构造。Seq包含一个创建函数fill,用于初始化具有相同值的序列。以下代码构造了一个包含32个寄存器的寄存器文件,每个寄存器的宽度为32位,并且复位为0:
val resetRegFile = RegInit(VecInit(Seq.fill(32)(0.U(32.W))))
val rdRegFile = resetRegFile(sel)
组合Bundle和Vec
我们可以自由地混合bundle和vector。当创建一个bundle类型的vector时,我们需要为vector字段传递一个prototype。使用上面定义的Channel,我们可以创建一个通道向量:
class Channel() extends Bundle {
val data = UInt(32.W)
val valid = Bool()
}
val vecBundle = Wire(Vec(8,new Channel())
一个Bundle也可以包含一个向量:
class BundleVec extends Bundle{
val field = UInt(8.W)
val vector = Vec(4,UInt(8.W))
}
当我们想要一个需要重置值的bundle类型的寄存器时,我们首先创建该bundle的Wire,根据需要设置各个字段,然后将此bundle传递给RegInit
val initVal =Wire(new Channel())
initVal.data := 0.U
initVal.valid := flase.B
val channelReg = RegInit(initVal)
通过Bundle和Vecs的组合,我们可以定义自己的数据结构。
易错点
Chisel中不允许部分赋值,尽管Chisel 2中允许部分赋值,并且在Verilog和VHDL中是可能的。接下来的代码可能在电路细化期间生成错误:
val assignWord = Wire(UInt(16.W))
assignWord(7,0) := lowByte
assignWord(15,8) := highByte
争论的焦点是,在这个用例中使用bundle会更好。解决此问题的一个可能的方法是创建一个(本地)bundle,从该bundle创建一个Wire,分配各个字段,使用asUInt()将该bundle转换为UInt,并将此值分配给目标UInt。请注意,我们在这里将Bundle定义为本地数据结构,因为我们只需要它。
val assignWord =Wire(UInt(16.W))
class Split extends Bundle {
val high = UInt(8.W)
val low = UInt(8.W)
}
val split = Wire(new Spilt())
spilt.low := lowByte
spilt.high := highByte
assignWord := spilt.asUInt()
这种解决方案的小缺点是,需要知道Bundle字段以哪些顺序合并为单个位向量。另一种选择是使用Bool向量来单独赋值,然后将其转换为UInt。
val vecResult = Wire(Vec(4,Bool()))
vecResult(0) := data(0)
vecResult(1) := data(1)
vecResult(2) := data(2)
vecResult(3) := data(3)
val uintResult = vecResult.asUInt
Wire, Reg 和IO
UInt、SInt和Bits是Chisel类型,它们本身并不代表硬件。只有将它们包装到Wire、Reg或IO中才能生成硬件。Wire表示组合逻辑,Reg表示寄存器(D触发器的集合),并且IO表示模块的连接(如具体集成电路(IC)的引脚)。Wire、Reg或IO可以缠绕在任何Chisel类型上,也可以缠绕在Bundle或Vec上。
你给予通过将一个硬件组件赋给一个Scala不可变变量来给它命名:
val number = Wire(UInt())
val reg = Reg(SInt())
您可以稍后使用Chisel运算符为Wire、Reg或IO指定(或重新指定)值或表达式:=
number := 10.U
reg := value - 3.U
请注意Scala赋值运算符“=”和Chisel运算符“:="之间的微小差异。当创建一个硬件对象(并给它一个名字)时,使用Scala的“=”操作符,但当为现有硬件对象赋值或重新赋值时,使用Chisel的“:=”操作符。
最佳做法是在创建导线时已经定义默认值。因此,前一个代码最好重写如下。
val number = WireDefault(10.U(4.W))
尽管Chisel推断信号和寄存器所需的位宽,但在创建硬件对象时指定预期的位宽也是一个很好的做法。在大多数情况下,在复位时将寄存器设置为已知的初始值也是一种好的做法:
val reg =RegInit(0.S(8.W))
Chisel 生成硬件
看过一些初始Chisel代码后,它可能看起来类似于Java或C等经典编程语言。然而,Chisel(或任何其他硬件描述语言)确实定义了硬件组件。在软件程序中,一行代码接一行代码执行,而在硬件中,所有代码行并行执行。
必须记住,Chisel代码确实会生成硬件。试着想象,或在一张纸上画出由Chisel电路描述生成的各个块。每创建一个组件都会增加硬件;每个赋值语句生成门和/或触发器。
从技术上讲,当Chisel执行代码时,它会作为Scala程序运行,通过执行Chisel语句,它收集硬件组件并连接这些节点。这个硬件节点网络是硬件,Chisel可以将其作为用于ASIC或FPGA综合的Verilog代码溢出,或者可以用Chisel测试仪进行测试。硬件节点的网络是完全并行执行的。
对于软件工程师来说,想象一下这种巨大的并行性,您可以在硬件中创建这种并行性,而无需将应用程序划分为线程并为通信正确锁定。