Chisel 手册(part1)
作者:Jonathan Bachrach, Huy Vo, Krste Asanović; EECS Department, UC Berkeley
译者:智能物联(CSDN)
1 简介
本文为Chisel手册 (Constructing Hardware In a Scala Embedded Language)。Chisel是一种内置于scala高级编程语言之上的硬件构建语言。我们还提供了一个单独的Chisel Tutorial作为简单的入门课程文档,用于早期阅读。本手册对Chisel语言给出了全面的概述和定义,Chisel实际上仅仅是包括特殊定义的class类,预定义的对象以及scala的使用习惯的一个集合。当你在写Chisel程序的时候,你实际上写的是Scala程序。本手册假定你已经基本掌握Scala语言。如果你尚不熟悉如果使用Scala,我们推荐你查阅优秀的Scala书籍 ([3], [2]).
2 节点(Nodes)
Chisel编写的任何硬件设计最终能用一个节点(node)对象的图表(graph)来表示。Chisel用户代码生成节点的图表,然后传给Chisel后端,翻译成Verilog或者C++ 代码。节点定义如下所示:
class Node {
// name assigned by user or from introspection
var name: String = ""
// incoming graph edges
def inputs: ArrayBuffer[Node]
// outgoing graph edges
def consumers: ArrayBuffer[Node]
// node specific width inference
def inferWidth: Int
// get width immediately inferrable
def getWidth: Int
// get first raw node
def getRawNode: Node
// convert to raw bits
def toBits: Bits
// convert to raw bits
def fromBits(x: Bits): this.type
// return lit value if inferrable else null
def litOf: Lit
// return value of lit if litOf is non null
def litValue(default: BigInt = BigInt(-1)): BigInt
}
节点的最上面层级参见图1。节点的基本类别如下:
Lit
– 常数或文字
Op
– 逻辑或者算数运算符
Updateable
– 条件更新节点
Data
– typed wires 或者 ports,
Reg
– 正沿触发寄存器
Mem
– 存储器
图1: 节点层次结构
3 Lits
Raw literals 作为 Lit nodes 定义如下:
class Lit extends Node {
// original value
val inputVal: BigInt
}
Raw literals为bits的集合。用户并不直接创建raw literals,而是使用type constructors,参见Section 5.
4 Ops
Raw operations 作为Op nodes 定义如下:
class Op extends Node {
// op name used during emission
val op: String
}
Ops运算操作符运算输入的组合函数。
5 Types
一个能够表示硬件设计的Chisel图表包括raw和type节点。Chisel type系统独立于Scala type系统,单独维护,type节点与raw节点杂处,以便Chisel检查和对应到Chisel types。硬件设计被转换成 C++ 或者 Verilog代码以后,Chisel type节点被全部删除。Node基类定义的getRawNode运算符,跳过type nodes,返回第一个raw node。图 2 展示了内建Chisel type层次结构,Data为最顶层的节点(node)
图2:Chisel type层次结构
内建scalar类型包括Bool,SInt,UInt;内建集合(aggregate)类型Bundle和Vec方便用户扩展其他的Chisel数据类型。
Data本身是一个节点:
abstract class Data extends Node {
override def cloneType(): this.type =
this.getClass.newInstance.
asInstanceOf[this.type]
// simple conversions
def toSInt: SInt
def toUInt: UInt
def toBool: Bool
def toBits: Bits
// flatten out to leaves of tree
def flatten: Array[(String, Data)]
// port direction if leaf
def dir: PortDir
// change dir to OUTPUT
def asOutput: this.type
// change dir to INPUT
def asInput: this.type
// change polarity of dir
def flip: this.type
// assign to input
def :=[T <: Data](t: T)
// bulk assign to input
def <>(t: Data)
}
Data类拥有用于type转换的method,代理port method到输入。我们将在第10节讨论port。最后,为了反映克隆所必要的构造参数,用户可以在自己的type节点(比如bundle)里面重载cloneType方法。
Data节点有四种用法:
- types – UInt(width = 8) – 记录图表的中间态types,使用最小的比特宽度(本节所描述),
- wires – UInt(width = 8) – 数据的前向声明,可以条件更新 (第6节描述),
- ports – UInt(dir = OUTPUT, width = 8) – module接口专用wire,另加direction属性(参见第十节),
- literals – UInt(1) or UInt(1, 8) – 使用type对象构造器生成,指定值和可选的位宽。
5.1 Bits
在Chisel中,Bits的原始集合可以如下定义:
object Bits {
def apply(dir: PortDir = null,
width: Int = -1): Bits
// create literal from BigInt or Int
def apply(value: BigInt, width: Int = -1): Bits
// create literal from String using
// base_char digit+ string format
def apply(value: String, width: Int = -1): Bits
}
class Bits extends Data with Updateable {
// bitwise-not
def unary_~(): Bits
// bitwise-and
def & (b: Bits): Bits
// bitwise-or
def | (b: Bits): Bits
// bitwise-xor
def ^ (b: Bits): Bits
// and-reduction
def andR(): Bool
// or-reduction
def orR(): Bool
// xor-reduction
def xorR(): Bool
// logical NOT
def unary_!(): Bool
// logical AND
def && (b: Bool): Bool
// logical OR
def || (b: Bool): Bool
// equality
def ===(b: Bits): Bool
// inequality
def != (b: Bits): Bool
// logical left shift
def << (b: UInt): Bits
// logical right shift
def >> (b: UInt): Bits
// concatenate
def ## (b: Bits): Bits
// extract single bit, LSB is 0
def apply(x: Int): Bits
// extract bit field from end to start bit pos
def apply(hi: Int, lo: Int): Bits
}
def Cat[T <: Data](elt: T, elts: T*): Bits
Bits拥有简单比特操作的method。请注意## 为二进制串接操作,而Cat为任意进制串接操作。为了避免与Scala的内建双联等于号==起冲突,Chisel的按比特比较使用三联等于号===。
宽度为n的比特可以使用单比特和Fill函数生成:
def Fill(n: Int, field: Bits): Bits
两个输入使用Mux来选择:
def Mux[T <: Data](sel: Bits, cons: T, alt: T): T
Constant或者literal值,使用Scala整数或者字符串传递给类型构造器来表示:
UInt(1) // decimal 1-bit lit from Scala Int.
UInt("ha") // hex 4-bit lit from string.
UInt("o12") // octal 4-bit lit from string.
UInt("b1010") // binary 4-bit lit from string.
生成Lit,参见 图3 中最左边的子图
运算符操作返回一个实际的运算符节点,包括一个组合了所有输入type节点的type节点。参见图3,查看连续的更复杂例子。
|
|
|
a = UInt(1) |
b = a & UInt(2) |
b | UInt(3) |
|
图 3: Chisel Op/Lit 图表
5.2 Bools
布尔值可以定义为 Bools:
object Bool {
def apply(dir: PortDir = null): Bool
// create literal
def apply(value: Boolean): Bool
}
class Bool extends UInt
Bool 等同于 UInt(width = 1).
5.3 Nums
Num是定义算数运算的type节点:
class Num extends Bits {
// Negation
def unary_-(): Bits
// Addition
def +(b: Num): Num
// Subtraction
def -(b: Num): Num
// Multiplication
def *(b: Num): Num
// Greater than
def >(b: Num): Bool
// Less than
def <(b: Num): Bool
// Less than or equal
def <=(b: Num): Bool
// Greater than or equal
def >=(b: Num): Bool
}
有符号和无符号整数是定点数的子集,分别用SInt和UInt来表示:
object SInt {
def apply (dir: PortDir = null,
width: Int = -1): SInt
// create literal
def apply (value: BigInt, width: Int = -1): SInt
def apply (value: String, width: Int = -1): SInt
}
class SInt extends Num
object UInt {
def apply(dir: PortDir = null,
width: Int = -1): UInt
// create literal
def apply(value: BigInt, width: Int = -1): UInt
def apply(value: String, width: Int = -1): UInt
}
class UInt extends Num {
// arithmetic right shift
override def >> (b: UInt): SInt
}
有符号定点数,包括整数,使用补码表示。
5.4 Bundles
Bundles可以把若干可能不同的数据类型组合在一起,很类似C中的struct:
class Bundle extends Data {
// shallow named bundle elements
def elements: ArrayBuffer[(String, Data)]
}
Bundle中每个元素的名称和类型通过element方法获取,flatten method返回嵌套aggregate末端的元素。用户可以通过扩展子类来定义新的bundle:
class MyFloat extends Bundle {
val sign = Bool()
val exponent = UInt(width = 8)
val significand = UInt(width = 23)
}
Bundle元素访问使用Scala的域field:
val x = new MyFloat()
val xs = x.sign
C++ 或者Verilog后端内对bundle元素的命名,取自其bundle的域名,使用Scala introspection。
5.5 Vecs
Vecs 生成可索引的元素矢量:
object Vec {
def apply[T <: Data](elts: Seq[T]): Vec[T]
def apply[T <: Data](elt0: T, elts: T*): Vec[T]
def fill[T <: Data](n: Int)(gen: => T): Vec[T]
def tabulate[T <: Data](n: Int)
(gen: (Int) => T): Vec[T]
def tabulate[T <: Data](n1: Int, n2: Int)
(gen: (Int, Int) => T): Vec[Vec[T]]
}
class Vec[T <: Data](n: Int, val gen: () => T)
extends Data {
def apply(idx: UInt): T
def apply(idx: Int): T
def forall(p: T => Bool): Bool
def exists(p: T => Bool): Bool
def contains[T <: Bits](x: T): Bool
def count(p: T => Bool): UInt
def indexWhere(p: T => Bool): UInt
def lastIndexWhere(p: T => Bool): UInt
}
with n elements of type defined with the gen thunk. Users can access elements statically with an Int index or dynamically using a UInt index, where dynamic access creates a virtual type node (representing a read “port”) that records the read using the given address. In either case, users can wire to the result of a read as follows:
v(a) := d
只读存储可以表示成literal的矢量:
val rom = Vec(UInt(3), UInt(7), UInt(4), UInt(0)) { UInt(width=3) }
val dout = rom(addr)
5.6 位宽推定
用户必须设置端口和寄存器的位宽,但是,节点的位宽是自动推定的,除非由用户手动设置(使用Extract 或者 Cat)。位宽推定引擎从图表的输入端口开始计算节点的输出位宽,按照下列表格所列的规则:
operation |
bit width |
z = x + y |
wz = max(wx, wy) |
z = x - y |
wz = max(wx, wy) |
z = x & y |
wz = max(wx, wy) |
z = Mux(c, x, y) |
wz = max(wx, wy) |
z = w * y |
wz = wx + wy |
z = x << n |
wz = wx + maxNum(n) |
z = x >> n |
wz = wx - minNum(n) |
z = Cat(x, y) |
wz = wx + wy |
z = Fill(n, x) |
wz = wx * maxNum(n) |
|
wz为wire z的位宽,规则适用于所有按比特逻辑操作。
位宽推定持续,直到位宽不再变化。除了向右移位常数个位移,位宽推定规则导致的输出位宽不可能小于输入位宽,因此,输出位宽要么变大,要么保持不变。更进一步,寄存器的宽度必须由用户显式指定,或者通过reset值的位宽指定。通过这两个要求,我们能够确保位宽推定过程将收敛导一个固定的位置。
& 操作不应该返回输入位宽的min()?