一、概述
Chisel(Constructing Hardware In a Scala Embedded Language)是一种嵌入在高级编程语言Scala的硬件构建语言。Chisel实际上只是一些特殊的类定义,预定义对象的集合,使用Scala的用法,所以在写Chisel程序时实际上是在写Scala程序。本文假设对Scala程序无任何了解,会通过一些Chisel的例子来说明某些重要的Scala特征,只使用本文介绍的东西也能完成一些的硬件设计。 当希望自己的代码能够更加简化或提高复用性,你会发现有必要了解Scala语言的潜力。作者也在学习中,后续文章陆续介绍。
二、Chisel硬件表达
Chisel只支持二进制逻辑,不支持三态信号。
由于只有芯片对外的IO处才能出现三态门,所以内部设计几乎用不到x和z。而且x和z在设计中会带来危害,忽略掉它们也不影响大多数设计,还简化了模型。当然,如果确实需要,可以通过黑盒语法与外部的Verilog代码互动,也可以在下游工具链里添加四态逻辑。
三、Chisel数据类型
Chisel数据类型用于指定状态元素中保存的值或wire上传输的值。
虽然硬件设计最终操作的是二进制数值向量,但对于值的其他抽象表示具有更清晰的规范,并且能够帮助工具生成更优化的电路。
在Chisel中,原始比特集合可以用Bits类型来表示。带符号和无符号整数被认为是定点数的子集,可以用SInt和UInt来表示。带符号定点整数(包括整数)使用二进制补码格式来表示。布尔值可以用Bool类型表示。注意,这些类型与Scala的内建类型不同,例如Int或Boolean。另外,Chisel定义了Bundle用来将值进行集合(类似于其他语言中的struct),还定义了Vec用来对值的集合进行索引。
常量或字面值使用Scala整数或传递给构造函数的字符串表示:
UInt(1) // decimal 1-bit lit from Scala Int.
UInt("ha") // hexadecimal 4-bit lit from string.
UInt("o12") // octal 4-bit lit from string.
UInt("b1010") // binary 4-bit lit from string.
SInt(5) // signed decimal 4-bit lit from Scala Int.
SInt(-8) // negative decimal 4-bit lit from Scala Int.
UInt(5) // unsigned decimal 3-bit lit from Scala Int.
Bool(true) // Bool lits from Scala lits.
Bool(false)
下划线可以用作长字符串文字中的分隔符,以帮助可读性,但在创建值时会被忽略,例如:
UInt("h_dead_beef") // 32-bit lit of type UInt
默认情况下,Chisel编译器将每个常量的大小设置为保存常量所需的最小位数,包括带符号类型的符号位。位宽也可以在字面上明确指定,如下所示:
UInt("ha", 8) // hexadecimal 8-bit lit of type UInt
UInt("o12", 6) // octal 6-bit lit of type UInt
UInt("b1010", 12) // binary 12-bit lit of type UInt
SInt(5, 7) // signed decimal 7-bit lit of type SInt
UInt(5, 8) // unsigned decimal 8-bit lit of type UInt
对于UInt类型值,值被零扩展到所需的位宽。对于类型为SInt的文字,该值被符号扩展以填充所需的位宽度。如果给定的位宽太小而不能容纳参数值,则会生成Chisel错误。
四、组合电路
在Chisel中,电路会被表示为一张节点图。每个节点是具有零个或多个输入并驱动一个输出的硬件运算符。
上面介绍的Uint是一种退化类型的节点,它没有输入,并且在其输出上驱动一个恒定的值。
创建和连接节点的一种方法是使用字面表达式。例如,我们可以使用以下表达式来表示简单的组合逻辑电路:
(a & b) | (~c & d)
语法应该看起来很熟悉,用&和|分别表示按位与和按位或,~表示按位非。a到d表示某些(未指定)宽度的命名导线。
任何简单的表达式都可以直接转换成电路树,在叶子处使用命名的导线和操作符形成内部节点。表达式的电路输出取自树根处的运算符,在本示例中是按位或运算。
简单表达式可以以树的形式构建电路,但是如果想以任意有向非循环图(DAG)的形式构建电路,我们需要描述扇出。在Chisel中,我们通过命名一根wire来表示一个子表达式,这样我们就可以在后续表达式中多次引用。我们通过声明变量来命名Chisel中的wire。例如,考虑如下示例的select表达式,它在后续的多选器描述中可以多次使用:
val sel = a | b
val out = (sel & in1) | (~sel & in0)
关键字val是Scala的一部分,用于命名具有不会再更改的值的变量。 在上面的例子中它命名了wire类型的sel,保存了第一个按位或运算符的输出,以便输出可在第二个表达式中多次使用。
五、内建操作符
Chisel定义了一组硬件操作符,如下表所示:
1、位宽接口
用户需要设置端口和寄存器的位宽,除非用户手动设置,否则编译器会自动推测wire上的位宽。位宽推测引擎会从节点图的输入端口开始,并根据以下规则集从它们各自的输入位宽度计算节点输出位宽度:
位宽推测过程会持续到没有位宽改变。 除了通过已知固定数量的右移之外,位宽推测规定了输出位宽度不能小于输入位宽度,因此输出位宽度增长或保持相同。 此外,寄存器的宽度必须由用户明确地或根据复位值或下一个参数的位宽指定。根据这两个要求,我们可以将位宽推测过程将收敛到一个固定点。
我们选择的运算符名称受到Scala语言的限制。所以我们必须使用===表示等于判断逻辑和=/=表示不等判断逻辑,这样可以保持原生Scala相关运算符可用。
六、功能抽象
我们可以定义函数来分解一个重复的逻辑,这样可以在后续设计中重复使用。例如,我们可以包装一个简单的组合逻辑块:
def clb(a: UInt, b: UInt, c: UInt, d: UInt): UInt = (a & b) | (~c & d)
其中clb是表示以a,b,c,d为参数的函数,并返回一个布尔电路的输出。 def关键字是Scala的一部分,表示引入了一个函数定义,每个语句后面跟一个冒号,然后是它的类型,函数返回类型在参数列表之后的冒号之后。(=)符号将函数参数列表与函数定义分隔开。
然后我们就可以在其他的电路中使用了:
val out = clb(a,b,c,d)
我们将在后面介绍许多酷炫的函数使用方法来构造硬件。
七、Bundles & Vecs
Bundle和Vec是可以允许用户使用其他数据类型来扩展Chisel数据类型集合的类。
1、Bundles
Bundle可以将一些不同类型的命名字段组合成一个单元,类似于C语言中的struct。用户可以通过将一个类定义为Bundle的子类来定义自己的bundle:
class MyFloat extends Bundle {val sign = Bool()val exponent = UInt(width = 8) val significand = UInt(width = 23)
}
val x = new MyFloat()
val xs = x.sign
scala约定将新类的名称的首字母大写,所以我们建议在Chisel中也遵循这个约定。 UInt构造函数的width命名参数指定类型中的位数。
2、Vecs
Vecs用来创建一个可索引的元素向量,其构造如下所示:
// Vector of 5 23-bit signed integers.
val myVec = Vec.fill(5){ SInt(width = 23) }
// Connect to one element of vector.
val reg3 = myVec(3)
注意,我们必须在花括号内指定Vec元素的类型,因为我们必须将位宽参数传递给SInt构造器。
原始类(SInt,UInt和Bool)加上聚合类(Bundles和Vecs)都继承自一个公共的超类Data。在电路中,每个最终继承自Data的对象都可以表示为一个位向量。
Bundle和Vec可以任意嵌套,从而构建复杂的数据结构:
class BigBundle extends Bundle {// Vector of 5 23-bit signed integers.val myVec = Vec.fill(5) { SInt(width = 23) } val flag = Bool()// Previously defined bundle.val f = new MyFloat()
}
八、端口
端口用作硬件组件的接口。一个端口可以是任意的Data对象,但它是具有方向的。
Chisel提供端口构造函数,以允许在构建时给对象添加(输入或输出)。原始的端口构造函数需要将方向作为第一个参数(方向为INPUT或OUTPUT),将位数作为第二个参数(除了始终为1位的布尔值)。
端口的声明如下所示:
class Decoupled extends Bundle { val ready = Bool(OUTPUT)val data = UInt(INPUT, 32) val valid = Bool(INPUT)
}
Decoupled被定义后,它就会变成一个新的类型,可以根据需要用于模块接口或命名的wire集合。
对象的方向也可以实例化时确定:
class ScaleIO extends Bundle {val in = new MyFloat().asInput val scale = new MyFloat().asInput val out = new MyFloat().asOutput
}
asInput和asOutput方法可以强制数据对象的所有模块设置成对应的方向。
通过将方向折叠到对象声明中,Chisel能够提供强大的布线能力,稍后会详细介绍。
九、Modules
Chisel 模块与 Verilog 模块非常相似,在生成的电路中定义层次结构。 层次化的命名空间可在下游工具中访问,以帮助调试和物理布局。
用户定义的模块被定义为一个类,它:
- 继承自 Module,
- 包含一个存储在名为 io 的端口字段中的接口
- 在其构造函数中将子电路连接在一起。
例如,将双输入多路复用器定义为一个模块:
class Mux2 extends Module {
val io = new Bundle{
val sel = UInt(INPUT, 1)
val in0 = UInt(INPUT, 1)
val in1 = UInt(INPUT, 1)
val out = UInt(OUTPUT, 1)
}
io.out := (io.sel & io.in1) | (~io.sel & io.in0)
}
模块的接线接口是Bundle的端口集合。 模块的接口是通过名为 io 的字段定义的。 对于 Mux2,io 被定义为具有四个字段的包,每个多路复用器端口一个。
:= 赋值运算符,在定义的主体中使用,是 Chisel 中的一个特殊运算符,它将左侧的输入连接到右侧的输出。
我们现在可以构建电路层次,我们可以从较小的子模块开开始构建更大的模块。例如,我们可以通过将三个2输入多路选择器连接在一起,构建一个4输入多路选择器模块:
class Mux4 extends Module { val io = new Bundle {val in0 = UInt(INPUT, 1) val in1 = UInt(INPUT, 1) val in2 = UInt(INPUT, 1) val in3 = UInt(INPUT, 1) val sel = UInt(INPUT, 2) val out = UInt(OUTPUT, 1)}val m0 = Module(new Mux2())m0.io.sel := io.sel(0)m0.io.in0 := io.in0; m0.io.in1 := io.in1val m1 = Module(new Mux2())m1.io.sel := io.sel(0)m1.io.in0 := io.in2; m1.io.in1 := io.in3val m3 = Module(new Mux2())m3.io.sel := io.sel(1)m3.io.in0 := m0.io.out; m3.io.in1 := m1.io.outio.out := m3.io.out
}