组合逻辑电路从数学的角度来讲,就是用布尔代数的操作符来描述的数字逻辑电路,也就是一系列布尔代数运算符的组合。Chisel中,这些布尔代数的操作符跟C、Java、Scala以及其他编程语言中定义的是类似的,比如,&
是按位与操作符,|
是按位或操作符。这一部分就详细介绍Chisel中基本的位运算符、算术运算符、逻辑运算符、比较运算符等,以及Chisel中的一个高阶组合电路运算符——多路选择器。
下面这一行代码,定义了一个组合电路,它用一个与门连接信号a
和b
,然后把这个与门的输出和信号c
用或门连接在一起:
val logic = (a & b) | c
这个表达式对应的电路示意图如下:
可以看到基础语法是很简单的,需要注意的是,这个电路中与门和或门的输入信号可以是单个比特,也可以是比特向量。
下面的例子分别演示了四种基本的位运算,用的是Scala中的标准运算符,他们的操作数可以是UInt
、SInt
和Bool
:
val and = a & b // 按位与
val or = a | b // 按位或
val xor = a ^ b // 按位异或
val not = ~a // 按位取反
这几种基本的位运算都很基础,还有两个移位操作,他们的操作数可以是UInt
或SInt
:
val shiftleft = a << b
val shiftright = a >> b
需要注意的是,对于SInt
类型的操作数,右移或会进行符号拓展,即算术右移。
总结Chisel中的位运算符如下:
操作符 | 描述 | 数据类型 |
---|---|---|
& |
按位与 | UInt 、SInt 、Bool |
` | ` | 按位或 |
^ |
按位异或 | UInt 、SInt 、Bool |
~ |
按位取反 | UInt 、SInt 、Bool |
<< |
左移 | UInt 、SInt |
>> |
对于UInt 是逻辑右移,对于SInt 是算术右移 |
UInt 、SInt |
下面是Chisel中使用Scala标准运算符进行的算术运算,他们的操作数可以是UInt
或SInt
:
val add = a + b // 加法
val sub = a - b // 减法
val neg = -a // 取相反数
val mul = a * b // 乘法
val div = a / b // 除法
val mod = a % b // 取余
需要注意这里的位宽推断:
另外,对于加法和减法,还可以指定是否进行位宽拓展保留进位,在+
或-
后加上%
就是不进行位宽拓展,加上&
就是不保留进位,默认是不进行位宽拓展。
总结Chisel中的算术运算符如下:
操作符 | 描述 | 数据类型 |
---|---|---|
+ 或+% |
加(不保留进位) | UInt 、SInt |
+& |
加(保留进位) | UInt 、SInt |
- 或-% |
减(不保留进位) | UInt 、SInt |
-& |
减(保留进位) | UInt 、SInt |
* |
乘 | UInt 、SInt |
/ |
除 | UInt 、SInt |
% |
取余 | UInt 、SInt |
逻辑运算符是针对Bool
类型的值进行运算的,有逻辑与、逻辑或和逻辑非这三种,和Scala以及其他编程语言是类似的:
操作符 | 描述 | 数据类型 |
---|---|---|
&& |
逻辑与 | Bool |
` | ` | |
! |
逻辑非 | Bool |
对于小于、小于等于、大于和大于等于,Chisel和Scala是一致的,但在等于和不等于上表示不一样。比较运算符的操作数为UInt
或SInt
,总结如下:
操作符 | 描述 | 数据类型 |
---|---|---|
> |
大于 | UInt 、SInt ,返回Bool |
>= |
大于等于 | UInt 、SInt ,返回Bool |
< |
小于 | UInt 、SInt ,返回Bool |
<= |
小于等于 | UInt 、SInt ,返回Bool |
=== |
等于 | UInt 、SInt ,返回Bool |
=/= |
不等于 | UInt 、SInt ,返回Bool |
虽然这里的===
和=/=
看起来很奇怪,但千万不能弄错,设计者表示这么做是为了让Scala中原有的==
和!=
仍然可用。
这是Chisel中比较好用的运算符,操作数为SInt
或UInt
,对操作数的每一位进行规约运算,返回值为Bool
类型,三个规约运算符如下:
操作符 | 描述 | 数据类型 |
---|---|---|
.andR |
与规约 | UInt 、SInt ,返回Bool |
.orR |
或规约 | UInt 、SInt ,返回Bool |
.xorR |
异或规约 | UInt 、SInt ,返回Bool |
用法如下:
val allSet = x.andR // 与规约
val anySet = x.orR // 或规约
val parity = x.xorR // 异或规约
我们前面提到UInt
和SInt
都是位向量,因此应该有一些对向量的位字段进行操作的操作符,上一部分的规约操作符就属于这类。Chisel中还有其他的位字段操作符:
比如从位向量中提取单个比特,操作符为(n)
,表示提取第n位,最低有效位LSB索引为0:
val xLSB = x(0) // 提取x的最低位
也可以提取一个位段,操作符为(end, start)
,表示提取第start
位到第end
位之间的字段,这个start
和end
是包括在内的,返回值是个UInt
:
val xTopNibble = x(15, 12) // 假设x是16位的,提取x的高4位
还可以把一个位向量复制多次,操作符为Fill(n, x)
,n
为复制次数,x
为被复制的位向量,只可以是或UInt
,返回值也是UInt
:
val usDebt = Fill(3, "hA".U) // "hAAA".U
最后是可以拼接多个位向量的操作,操作符为##
或Cat
,和Verilog中的{}
类似,示例如下:
val float = Cat(sign, exponent, mantissa) // 拼接三个向量,或者
val float = sign ## exponent ## mantissa
不过需要注意的是,拼接操作的操作数两边类型必须一样,而返回值为UInt
,因此,如果在多个操作数上用##
进行拼接的时候需要注意,比如对三个SInt
进行拼接就会报错,而对两个SInt
和一个UInt
进行拼接就不会报错,而使用Cat
就不会有这个问题。
另外还有个需要注意的是,虽然##
和Cat
功能是类似的,但是生成的Verilog会有所不同,比如:
a := -1.S ## -2.S
会生成:
assign a = {1'sh1,2'sh2};
而:
a := Cat(-1.S, -2.S)
会生成:
assign a = 3'h6;
一般来说使用Cat
进行拼接是更好的。
总结如下:
操作符 | 描述 | 数据类型 |
---|---|---|
x(n) |
提取第n 位 |
UInt 、SInt ,返回Bool |
x(end, start) |
提取第start 到第end 位 |
UInt 、SInt ,返回UInt |
Fill(n, x) |
位向量x 复制n 次 |
UInt ,返回UInt |
a ## b |
位向量拼接 | UInt 、SInt ,返回UInt |
Cat(a, b, ...) |
位向量拼接 | UInt 、SInt ,返回UInt |
Chisel操作符的优先级并没有作为Chisel语言的一部分直接定义出来,而是取决于电路的赋值顺序,自然地遵循Scala的运算符优先级。如果实在拿不准的话,那就使用括号来表达运算优先级。
题外话,Chisel和Scala的运算符优先级和Java/C相似但不同,而Verilog和C是一样的,但是VHDL直接就是没有这个特性的。在VHDL里面,所有的运算符优先级相同,按照从左到右的顺序进行计算。
多路选择器(multiplexer)是在多个输入中选择一个作为输出的组合电路,其最基本的形式就是2-1多路选择器,即二选一。下图就是一个2-1多路选择器,或者简称为Mux:
根据选择信号sel
的值,输出y
会表示输入信号a
或b
。当然,我们用逻辑门也是可以实现这个Mux的,但是Chisel标准库里面就提供了Mux
作为标准的操作符,示例如下:
val y = Mux(sel, a, b)
当sel
是个Chisel中的Bool
类型值,为true
的时候选择输出a
,否则选择输出b
。这里a
和b
可以是任意的Chisel基本类型或聚合类(比如bundle或vector,后面会详细讲),只要它俩的类型是一样的就行。
有了上面的基本算术、逻辑操作和这里的多路选择器,那就可以描述所有的组合电路了。但是,硬用这些来描述显然不够优雅,比如我要实现一个3-8译码器,我总不能使用8个Mux吧?那代码可读性也太差了!而Chisel里面还提供了更多的组件和控制抽象,能让我们在描述一个组合电路的时候更优雅,相关内容后面详细说!