第2章:基础组件
数字设计的核心在于组合逻辑电路和触发器,这些基本组件是构建更大、更复杂电路的基础。数字系统通常使用二进制信号,即单个位或信号只能表示两个可能值:0 和 1。这些值也常用以下术语描述:低/高(low/high)、假/真(false/true)、未断言/断言(deasserted/asserted)。无论采用哪种术语,均表示二进制信号的两个可能状态。
2.1 Chisel 类型与常量
Chisel 提供了三种基本数据类型,用于描述硬件连接、组合逻辑和寄存器:
- Bits:表示位向量。
- UInt:无符号整数,扩展自 Bits。
- SInt:有符号整数,扩展自 Bits,使用二进制补码表示。
以下是这些类型的定义示例:
Bits(8.W) // 8 位的 Bits 类型
UInt(8.W) // 8 位无符号整数
SInt(10.W) // 10 位有符号整数
宽度定义
位向量的宽度通过 Chisel 的宽度类型(Width
)定义。可以使用 n.W
将整数 n
转换为 Chisel 宽度类型。例如:
Bits(8.W) // 定义一个宽度为 8 的 Bits 向量
常量定义
Chisel 提供了简洁的语法定义常量,可以通过将 Scala 整数转换为 Chisel 类型:
0.U // 定义一个值为 0 的 UInt 常量
-3.S // 定义一个值为 -3 的 SInt 常量
3.U(4.W) // 定义一个宽度为 4 的 UInt 常量,值为 3
这些语法类似于 C、Java 或 Scala 中的长整数常量(如 3L
表示 long 类型整数)。
常见陷阱
定义常量宽度时,可能会遗漏 .W
后缀。例如:
1.U(32) // 错误
上例中,(32)
被解释为从位 32 提取单个位的操作,结果是一个值为 0 的单个位常量,而非宽度为 32 的常量。正确写法为:
1.U(32.W)
类型推导
Chisel 借助 Scala 的类型推导,在许多情况下可以省略显式的类型和宽度定义。例如,Chisel 可以自动推导常量的最小宽度,使硬件描述更加简洁易读。
进制表示
Chisel 支持通过字符串定义其他进制(如十六进制、八进制和二进制)的常量,前缀分别为 h
、o
和 b
:
"hff".U // 十六进制表示 255
"o377".U // 八进制表示 255
"b1111_1111".U // 二进制表示 255,支持使用下划线分组以提高可读性
字符常量
可以直接使用 ASCII 编码的字符作为常量:
val aChar = 'A'.U // 定义字符 'A' 对应的常量
布尔类型
Chisel 定义了类型 Bool
,用于表示逻辑值 true
和 false
:
Bool() // 定义 Bool 类型
true.B // 布尔值 true
false.B // 布尔值 false
2.2 组合逻辑电路
Chisel 使用布尔代数运算符来描述组合逻辑电路,这些运算符在 C、Java 和 Scala 等编程语言中广泛使用。以下是典型的逻辑操作示例:
val logic = (a & b) | c
该代码描述了一个简单的组合逻辑电路:
a & b
表示两个信号通过 与门(AND gate)结合。(a & b) | c
表示与门输出与信号c
通过 或门(OR gate)结合。
图 2.1 展示了这一逻辑表达式的电路图。需要注意的是,这些信号可以是单个位,也可以是多位向量。
运算符与类型推导
在上述示例中,logic
的类型和宽度是从表达式中自动推导的,无需显式声明。这种类型推导简化了硬件描述。
标准逻辑运算
Chisel 提供了以下标准逻辑运算符:
按位与:
scalaval and = a & b
按位或:
scalaval or = a | b
按位异或:
scalaval xor = a ^ b
按位非:
scalaval not = ~a
算术运算
Chisel 同样支持常见的算术运算:
加法:
scalaval add = a + b
减法:
scalaval sub = a - b
取反:
scalaval neg = -a
乘法:
scalaval mul = a * b
除法:
scalaval div = a / b
取模:
scalaval mod = a % b
运算结果的宽度依赖于操作数:
- 加法和减法:结果宽度为操作数的最大宽度。
- 乘法:结果宽度为两个操作数宽度之和。
- 除法和取模:结果宽度通常与被除数(分子)的宽度一致。
信号定义与操作
定义与赋值
Chisel 中可以通过 Wire
定义一个信号,随后使用 :=
运算符对信号赋值:
val w = Wire(UInt())
w := a & b
位提取
可以从一个信号中提取特定位或位字段:
提取单个位:
scalaval sign = x(31) // 提取第 31 位
提取子字段:
scalaval lowByte = largeWord(7, 0) // 提取第 0 到第 7 位
位拼接
位字段可以通过 ##
运算符拼接:
val word = highByte ## lowByte
或者使用等效的 Cat
函数:
val word = Cat(highByte, lowByte)
运算符优先级
Chisel 的运算符优先级遵循 Scala 的规则,与 Java/C 相似,但与 Verilog 和 VHDL 存在差异:
- Verilog 的运算符优先级与 C 相同。
- VHDL 的逻辑运算符没有优先级,按从左到右的顺序计算。
建议在复杂表达式中使用括号确保运算顺序清晰。例如:
val result = ((a & b) | c) ^ d
运算符与硬件函数
Chisel 提供了丰富的运算符和硬件函数来描述硬件行为。以下是两张表格总结的内容,帮助更好地理解和使用这些功能。
表 2.1:Chisel 定义的硬件运算符
运算符 | 描述 | 支持的数据类型 |
---|---|---|
* / % | 乘法、除法、取模 | UInt , SInt |
+ - | 加法、减法 | UInt , SInt |
=== =/= | 判断相等、不等,返回 Bool | UInt , SInt , 返回 Bool |
> >= < <= | 比较运算符,返回 Bool | UInt , SInt , 返回 Bool |
<< >> | 左移、右移(SInt 类型右移时带符号扩展) | UInt , SInt |
~ | 按位非(NOT) | UInt , SInt , Bool |
& | ^ | 按位与(AND)、或(OR)、异或(XOR) | UInt , SInt , Bool |
! | 逻辑非(NOT) | Bool |
&& || | 逻辑与(AND)、或(OR) | Bool |
这些运算符与软件语言(如 C、Java 或 Scala)中定义的运算符相似,可以直接用于描述硬件行为。
表 2.2:Chisel 定义的硬件函数
函数 | 描述 | 支持的数据类型 |
---|---|---|
v.andR v.orR v.xorR | 按位与、或、异或的归约操作,返回单个布尔值 | UInt , SInt , 返回 Bool |
v(n) | 提取信号的第 n 位 | UInt , SInt |
v(end, start) | 提取从 start 位到 end 位的子字段 | UInt , SInt |
Fill(n, v) | 将位字段 v 复制 n 次形成新的位向量 | UInt , SInt |
a ## b | 按位拼接两个信号 | UInt , SInt |
Cat(a, b, ...) | 按位拼接多个信号,功能等同于 ## | UInt , SInt |
示例与说明
按位归约操作:
andR
:对所有位取按位与,若所有位均为 1 则返回true
。示例:
scalaval result = v.andR
位提取:
提取单个位:
scalaval bit = v(3) // 提取第 3 位
提取子字段:
scalaval subField = v(7, 4) // 提取第 4 到第 7 位
位拼接与填充:
拼接两个信号:
scalaval combined = highByte ## lowByte
使用
Fill
进行位复制:scalaval replicated = Fill(4, 1.U) // 将 1 复制 4 次,生成 4'b1111
运算符优先级
Chisel 运算符的优先级继承自 Scala,与 Java/C 相似,但与 Verilog 和 VHDL 存在差异。
建议:在表达式中使用括号来确保逻辑的清晰性,例如:
scalaval result = ((a & b) | c) ^ d
多路复用器(Multiplexer)
多路复用器是一种在多个输入中选择一个输出的电路。在最基本的形式中,它在两种输入间进行选择,如图 2.2 所示,这是一个 2:1 多路复用器(简称 MUX)。
工作原理:
- 根据选择信号
sel
的值,输出y
将对应输入信号a
或b
: - 当
sel
为 1 时,y
输出a
; - 当
sel
为 0 时,y
输出b
。
在 Chisel 中使用多路复用器
虽然多路复用器可以用基本的逻辑操作(如 AND
和 OR
)实现,但由于其广泛使用,Chisel 提供了内置的 Mux
函数来简化设计。以下是多路复用器的定义:
val result = Mux(sel, a, b)
参数说明
sel
:选择信号,为Bool
类型。如果sel
为true.B
,选择输入a
,否则选择输入b
。a
和b
:两个输入信号,可以是任何 Chisel 数据类型(如UInt
、SInt
)或聚合类型(如Bundle
或Vec
)。两者必须具有相同的类型。
示例
val a = Wire(UInt(8.W))
val b = Wire(UInt(8.W))
val sel = Wire(Bool())
val y = Mux(sel, a, b)
- 如果
sel
为true.B
,y
的值为a
; - 如果
sel
为false.B
,y
的值为b
。
功能与应用
结合逻辑运算、算术运算和多路复用器,几乎可以描述所有的组合逻辑电路。多路复用器特别适合以下场景:
- 选择路径:在多个数据路径中选择一条作为输出。
- 实现条件判断:替代传统的条件语句(如
if-else
或switch
)。
虽然 Mux
功能强大,但 Chisel 还提供了更高级的控制抽象,使得描述复杂的组合逻辑更加简洁优雅。这些高级功能将在第 5 章详细介绍。
2.3 寄存器
2.3.1 寄存器的定义与使用
Chisel 提供了寄存器的抽象,它是由多个 D 触发器(D flip-flops)组成的集合。寄存器可以存储信号的状态,并在时钟上升沿更新。寄存器的初始化值可以通过定义时指定,并与全局复位信号同步复位。
寄存器的基本定义
以下代码定义了一个 8 位宽的寄存器,初始值为 0:
val reg = RegInit(0.U(8.W))
RegInit
:初始化寄存器,并在复位时将其值设为指定的初始值。0.U(8.W)
:表示宽度为 8 位的无符号整数常量,值为 0。
连接输入与使用输出
寄存器的输入通过 :=
运算符连接,输出可以直接通过寄存器名称使用:
reg := d // 将输入信号 d 写入寄存器
val q = reg // 读取寄存器的输出信号
简化定义方式
Chisel 提供了 RegNext
方法,简化了寄存器与输入信号的连接:
val nextReg = RegNext(d)
上述代码创建了一个寄存器,并将其输入设为 d
,默认的复位值为 0。
初始化与连接
寄存器还可以通过 RegNext
同时指定输入信号和复位值:
val bothReg = RegNext(d, 0.U)
上述代码定义了一个寄存器,输入信号为 d
,复位时的初始值为 0。
2.3.2 图示分析
图 2.3 展示了寄存器的基本结构:
- 时钟(clock):控制寄存器在上升沿更新。
- 复位(reset):同步复位寄存器的值为 0。
- 输入信号(d):寄存器的数据输入。
- 输出信号(q):寄存器的当前存储值。
复位信号和时钟信号是全局的,Chisel 会为寄存器自动连接这些信号。
2.3.3 命名惯例
为增强代码可读性,推荐以下命名规则:
- 寄存器后缀:在寄存器名称后添加
Reg
,例如cntReg
。 - 驼峰命名法:采用小驼峰式命名变量和函数,例如
nextReg
。类名(如模块名)以大写字母开头。
虽然 Chisel 的命名规则较为灵活,但建议使用简洁、描述性强的名称。同时注意避免使用保留字(保留字列表详见附录 A)。
2.3.4 计数器的实现
计数器是数字系统中常见的操作,通常用于事件计数或时间间隔定义。通过计数时钟周期,可以触发特定的行为或操作。
基本循环计数器
以下代码实现了一个从 0 计数到 9 的计数器,到达上限后重置为 0:
val cntReg = RegInit(0.U(8.W)) // 定义一个 8 位宽的寄存器,复位值为 0
cntReg := Mux(cntReg === 9.U, 0.U, cntReg + 1.U)
计数逻辑:
- 当
cntReg
的值达到9
时,Mux
选择0.U
,将计数器重置为 0。 - 否则,选择
cntReg + 1.U
,计数器递增 1。
- 当
代码解析
RegInit
:- 定义一个初始值为 0 的寄存器。
条件判断:
- 使用
cntReg === 9.U
判断是否达到上限值。
- 使用
多路选择:
Mux(condition, trueValue, falseValue)
是一个条件选择器:
- 若条件为
true
,选择trueValue
; - 若条件为
false
,选择falseValue
。
- 若条件为
计数器的应用场景
- 事件计数:记录特定事件的发生次数。
- 时间控制:通过计数器生成定时信号或触发周期性动作。
- 状态机:用于状态计数和状态转换。
2.4 使用 Bundle
和 Vec
组织结构化信号
Chisel 提供了两种工具来组织相关信号:
Bundle
:将不同类型的信号组合为命名字段。Vec
:表示相同类型信号的可索引集合(类似于数组)。
这两种构造工具允许用户定义新的 Chisel 类型,并支持嵌套使用。
2.4.1 Bundle
定义 Bundle
Bundle
将多个信号组合成一个整体,类似于 C 和 SystemVerilog 中的 struct
,或 VHDL 中的 record
。 通过扩展 Bundle
类,可以定义一个信号集合:
class Channel extends Bundle {
val data = UInt(32.W) // 32 位无符号整数信号
val valid = Bool() // 布尔信号,表示有效位
}
使用 Bundle
创建一个 Bundle
的实例并将其包装成 Wire
:
val ch = Wire(new Channel())
ch.data := 123.U // 为字段 data 赋值
ch.valid := true.B // 为字段 valid 赋值
val b = ch.valid // 读取字段 valid 的值
- 点号表示法:通过
.
访问Bundle
中的字段。这种语法类似于面向对象编程中的字段访问。
整体引用 Bundle
整个 Bundle
也可以作为一个整体被引用或传递:
val channel = ch
适用场景
- 端口组织:将模块的输入输出信号组织成
Bundle
,简化接口设计。 - 结构化信号:将逻辑相关的信号组合在一起,增强代码可读性。
2.4.2 Vec
定义 Vec
Vec
表示相同类型信号的集合,支持按索引访问元素。它类似于其他编程语言中的数组数据结构。例如:
val vec = Wire(Vec(4, UInt(8.W))) // 定义一个包含 4 个 8 位信号的向量
vec(0) := 1.U // 为第一个元素赋值
vec(1) := 2.U // 为第二个元素赋值
Vec
的用途
Vec
在硬件设计中有多种用途:
- 动态寻址:用于硬件中的多路选择器(Multiplexer)。
- 寄存器文件:实现读写的多路复用和写入使能控制。
- 模块端口参数化:根据端口数量动态定义模块接口。
使用建议
对于其他集合类型(如生成器数据),如果不涉及硬件信号的生成,推荐使用 Scala 的 Seq
来替代 Vec
,以便更好地利用 Scala 的集合工具。
Bundle
和 Vec
的嵌套
Bundle
和 Vec
可以相互嵌套,实现复杂结构的信号组织。例如:
class NestedExample extends Bundle {
val data = Vec(4, UInt(8.W)) // 包含 4 个 8 位信号的向量
val meta = new Channel // 嵌套的 Bundle 类型
}
上述代码创建了一个 Bundle
:
- 包含一个名为
data
的Vec
。 - 包含一个嵌套的
Channel
类型字段。
使用 Vec
实现组合逻辑
Vec
是 Chisel 提供的工具之一,用于组织同类型信号的集合。在组合逻辑中,Vec
常被用来实现多路复用器(multiplexer)或其他索引操作。以下是 Vec
的更多细节与应用。
定义 Vec
Vec
通过构造函数创建,指定两个参数:
- 元素的数量。
- 元素的类型。
例如,定义一个包含三个 4 位无符号整数的向量:
val v = Wire(Vec(3, UInt(4.W)))
索引访问
可以通过索引访问 Vec
的单个元素,并为其赋值:
v(0) := 1.U // 第一个元素赋值为 1
v(1) := 3.U // 第二个元素赋值为 3
v(2) := 5.U // 第三个元素赋值为 5
使用索引值动态选择一个元素:
val index = 1.U(2.W) // 定义一个索引值,宽度为 2 位
val a = v(index) // 根据索引选择向量中的一个元素
使用 Vec
实现多路复用器
Vec
是实现多路复用器的常见工具,如图 2.4 所示。以下代码展示了一个简单的多路复用器设计:
val vec = Wire(Vec(3, UInt(8.W))) // 定义一个包含三个 8 位信号的向量
vec(0) := x // 第一个输入
vec(1) := y // 第二个输入
vec(2) := z // 第三个输入
val muxOut = vec(select) // 根据 `select` 选择输出
select
是一个索引信号,用于选择输出。muxOut
是根据select
选择的向量中的一个元素。
使用 VecInit
设置默认值
VecInit
提供了更简洁的语法,用于初始化 Vec
的默认值或连接信号:
定义带默认值的 Vec
以下代码实现了一个带初始值的 3:1 多路复用器:
val defVec = VecInit(1.U(3.W), 2.U, 3.U) // 定义默认值为 1, 2, 3 的向量
when (cond) { // 根据条件覆盖默认值
defVec(0) := 4.U
defVec(1) := 5.U
defVec(2) := 6.U
}
val vecOut = defVec(sel) // 根据 `sel` 输出向量中的一个值
- 默认值初始化:
VecInit
中的常量直接赋值给向量的每个元素。 - 动态覆盖:通过
when
语句,可以动态修改Vec
中的值。
连接信号到 Vec
除了初始化常量,VecInit
也可以直接将信号连接到 Vec
:
val defVecSig = VecInit(d, e, f) // 将信号 d, e, f 连接到 Vec
val vecOutSig = defVecSig(sel) // 根据 sel 选择输出信号
使用 Vec
创建寄存器文件
Vec
可以和 Reg
结合使用,用于创建一组寄存器(即寄存器文件)。寄存器文件是数字系统中常见的结构,广泛应用于处理器设计中。
创建寄存器向量
以下代码创建了一个包含三个 8 位宽寄存器的向量:
val regVec = Reg(Vec(3, UInt(8.W))) // 定义一个包含 3 个 8 位寄存器的向量
val dout = regVec(rdIdx) // 通过索引 rdIdx 读取寄存器
regVec(wrIdx) := din // 通过索引 wrIdx 写入寄存器
工作原理
写操作:
- 使用
wrIdx
(写索引)选择要写入的寄存器,并将din
的值写入对应寄存器。
- 使用
读操作:
- 使用
rdIdx
(读索引)选择要读取的寄存器,输出值为dout
。
- 使用
电路图说明
图 2.5 展示了寄存器向量的电路结构:
多路复用器:
- 通过
rdIdx
选择从哪个寄存器输出数据。
- 通过
解码器:
- 根据
wrIdx
激活对应寄存器的写入使能信号(en
)。
- 根据
创建完整的寄存器文件
寄存器文件是寄存器的更大集合,通常用于存储处理器的操作数。例如,以下代码定义了一个包含 32 个寄存器的寄存器文件,每个寄存器 32 位宽:
val registerFile = Reg(Vec(32, UInt(32.W)))
寄存器文件的读写操作
写操作:
scalaregisterFile(index) := dIn
- 使用
index
选择要写入的寄存器,将数据dIn
写入其中。
- 使用
读操作:
scalaval dOut = registerFile(index)
- 使用
index
选择要读取的寄存器,返回其存储的值。
- 使用
初始化寄存器文件
通过 VecInit
初始化寄存器
寄存器文件可以通过 VecInit
设置复位值:
val initReg = RegInit(VecInit(0.U(3.W), 1.U, 2.U)) // 初始化寄存器的复位值
val resetVal = initReg(sel) // 读取选中的寄存器值
initReg(0) := d // 动态修改第一个寄存器的值
initReg(1) := e // 动态修改第二个寄存器的值
initReg(2) := f // 动态修改第三个寄存器的值
RegInit
:初始化寄存器文件的复位值。VecInit
:通过传入值列表初始化寄存器。
统一初始化为相同值
如果所有寄存器的复位值相同,可以使用 Scala 的 Seq.fill
:
val resetRegFile = RegInit(VecInit(Seq.fill(32)(0.U(32.W)))) // 所有寄存器复位值为 0
val rdRegFile = resetRegFile(sel) // 通过 sel 选择输出寄存器值
Seq.fill
:- 用于创建一个指定长度的序列,所有元素初始化为相同值。
- 在上例中,生成了一个长度为 32 的序列,每个元素初始化为
0.U(32.W)
。
应用场景
处理器寄存器文件:
- RISC-V 等架构中使用寄存器文件存储操作数。
- 典型寄存器文件包含 32 个 32 位宽寄存器。
缓冲器设计:
- 用于数据存储和传输的缓冲区。
多路复用器控制:
- 利用
Vec
结合索引实现动态选择逻辑。
- 利用
使用 Bundle
和 Vec
的组合
Chisel 提供了强大的抽象能力,可以将 Bundle
和 Vec
组合使用,构建复杂的数据结构。这种方式不仅可以提高代码的模块化和可读性,还能适应灵活多变的硬件设计需求。
在 Vec
中使用 Bundle
可以在 Vec
中存储 Bundle
类型的元素,创建一个包含多个 Bundle
的向量。例如:
val vecBundle = Wire(Vec(8, new Channel())) // 包含 8 个 Channel 的向量
这里:
Channel
是一个Bundle
类型。Vec(8, new Channel())
定义了一个包含 8 个Channel
的向量。
在 Bundle
中使用 Vec
同样,可以在 Bundle
中嵌套一个 Vec
。例如:
class BundleVec extends Bundle {
val field = UInt(8.W) // 一个普通字段
val vector = Vec(4, UInt(8.W)) // 一个包含 4 个 8 位整数的向量
}
这种组合方式非常适合描述包含数组或向量的复杂信号结构。
寄存器初始化带有 Bundle
的 Vec
如果需要为 Bundle
类型的寄存器设置复位值,可以使用如下方式:
- 先创建一个
Bundle
类型的Wire
,初始化其字段。 - 使用
RegInit
将初始化值赋给寄存器。
示例
val initVal = Wire(new Channel()) // 创建一个 Channel 类型的 Wire
initVal.data := 0.U // 初始化字段 data
initVal.valid := false.B // 初始化字段 valid
val channelReg = RegInit(initVal) // 定义寄存器并初始化为 initVal
注意事项:禁止部分赋值
在 Chisel 3 中,不允许对 UInt
类型的部分位进行赋值(Verilog 和 Chisel 2 支持此功能)。例如:
val assignWord = Wire(UInt(16.W))
assignWord(7, 0) := lowByte // 会报错
assignWord(15, 8) := highByte // 会报错
解决方法:使用 Bundle
可以通过定义一个本地的 Bundle
类型,将数据拆分为字段,然后将 Bundle
转换为 UInt
。例如:
val assignWord = Wire(UInt(16.W))
class Split extends Bundle {
val high = UInt(8.W)
val low = UInt(8.W)
}
val split = Wire(new Split())
split.low := lowByte
split.high := highByte
assignWord := split.asUInt() // 将 Bundle 转换为 UInt
这种方法要求知道字段的拼接顺序(如高位字段在前,低位字段在后)。
使用 Vec
进行逐位赋值
另一种解决方案是使用 Vec
来表示每个位的值,然后将其转换为 UInt
。例如:
val vecResult = Wire(Vec(4, Bool())) // 定义一个 Bool 类型的向量
// 为向量的每个位赋值
vecResult(0) := data(0)
vecResult(1) := data(1)
vecResult(2) := data(2)
vecResult(3) := data(3)
val uintResult = vecResult.asUInt // 将 Vec 转换为 UInt
这种方法可以避免使用自定义 Bundle
,直接将位向量组合成整数。
2.5 Wire
, Reg
和 IO
在 Chisel 中,基本类型 UInt
、SInt
和 Bits
本身并不直接代表硬件。只有将这些类型封装到 Wire
、Reg
或 IO
中时,才会生成实际的硬件组件:
Wire
:表示组合逻辑(如一个连线或中间信号)。Reg
:表示寄存器(由一组 D 触发器组成)。IO
:表示模块输入输出端口(如硬件上的引脚)。
2.5.1 定义和使用
通过将 Wire
或 Reg
赋值给 Scala 的不可变变量(val
),即可定义一个硬件信号。例如:
val number = Wire(UInt()) // 定义一个无符号整数的 Wire
val reg = Reg(SInt()) // 定义一个有符号整数的寄存器
赋值
使用 :=
运算符为 Wire
或 Reg
赋值:
number := 10.U // 给 Wire 赋值为 10
reg := value - 3.U // 给 Reg 赋值为 (value - 3)
注意:Chisel 中的
:=
和 Scala 的=
不同。=
用于创建硬件对象时初始化信号。:=
用于重新赋值给已存在的硬件对象。
2.5.2 条件赋值与默认值
条件赋值的规则
组合逻辑信号(Wire
)可以使用条件赋值,但每个分支必须进行赋值,否则会引入潜在的锁存器(latch)。例如:
when(condition) {
number := 5.U
}.otherwise {
number := 0.U
}
最佳实践:设置默认值
为了避免忘记赋值或引入锁存器,推荐在定义 Wire
时设置默认值:
val number = WireDefault(10.U(4.W)) // 设置默认值为 10,位宽为 4
2.5.3 定义寄存器的初始值
对于寄存器,建议在复位时为其设置一个初始值。这样可以简化测试和验证,并避免硬件在复位后进入未定义状态:
val reg = RegInit(0.S(8.W)) // 定义一个 8 位宽的有符号寄存器,复位值为 0
RegInit
:- 初始化寄存器的复位值。
- 如果省略复位值,寄存器的值在复位后可能不确定,这可能会导致硬件行为不可预测。
2.5.4 小结与建议
Wire
和Reg
的区别:Wire
:表示组合逻辑信号,信号值可以随时更新。Reg
:表示时序逻辑信号(寄存器),值仅在时钟上升沿更新。
:=
和=
的区别:=
:用于创建硬件对象。:=
:用于为硬件对象赋值或重新赋值。
- 组合逻辑的默认值:
- 使用
WireDefault
设置组合信号的默认值,避免因漏掉条件赋值导致锁存器。
- 使用
- 寄存器初始化:
- 使用
RegInit
明确寄存器的复位值,提高硬件设计的可预测性和测试的可靠性。
- 使用
通过掌握 Wire
、Reg
和 IO
的使用,可以在 Chisel 中准确描述组合逻辑、时序逻辑和模块接口,构建清晰且易维护的硬件设计。
2.6 Chisel 是如何生成硬件的?
尽管 Chisel 的代码风格类似于 Java 或 C 等经典编程语言,但它的本质完全不同。Chisel 是一种 硬件描述语言,其代码生成的是硬件电路,而非传统的软件程序。
硬件的并行执行
在软件程序中,代码是一行接一行顺序执行的。而在硬件设计中,所有逻辑是并行执行的:
- 每个赋值语句会生成硬件组件(如门电路或触发器)。
- 所有硬件节点彼此独立,在时钟驱动下并行工作。
在构建 Chisel 模块时,可以将其想象为绘制电路图中的模块和连线。例如:
val logic = (a & b) | c
这段代码描述了一个组合逻辑电路,由 AND 和 OR 门组成。这不是顺序执行的,而是直接生成一组互连的硬件节点。
硬件生成的过程
- Scala 程序运行:
- Chisel 的代码运行在 Scala 的执行环境中。
- 程序中的每个语句描述并生成硬件节点。
- 硬件描述的收集:
- Chisel 代码通过运行后收集所有硬件组件及其连接关系。
- 这些硬件节点构成了一个电路图。
- 输出 Verilog:
- 收集到的硬件节点网络会被转换为 Verilog 文件,供 ASIC 或 FPGA 工具进一步综合。
对于软件工程师而言,这种硬件设计方式提供了极大的并行性,无需像多线程编程那样复杂地管理任务分配和通信同步。
2.7 练习:从简单硬件设计开始
在这一节中,您将扩展之前实现的 Blinking LED 设计,为硬件添加输入开关,并在 FPGA 上测试基础组合逻辑。
步骤 1:扩展 IO 接口
创建新的 Chisel 模块,扩展 IO 接口,使其包含输入开关和输出 LED。例如:
val io = IO(new Bundle {
val sw = Input(UInt(2.W)) // 2 位输入开关
val led = Output(UInt(1.W)) // 1 位输出 LED
})
步骤 2:简单的硬件功能
将设计简化为直接将输入连接到输出。例如:
io.led := io.sw(0) // 第一个开关控制 LED 的状态
这是一种简单的直通逻辑,验证开关和 LED 是否正常连接。
步骤 3:引脚分配
要将硬件映射到 FPGA 开发板上,需要为开关和 LED 分配物理引脚:
- 查看 FPGA 的手册,找到输入和输出的引脚位置。
- 在 Quartus 或 Vivado 工具中完成引脚分配。
步骤 4:加载到 FPGA
- 编译 Chisel 项目,生成 Verilog 文件。
- 使用 Quartus 或 Vivado,将 Verilog 转换为比特流文件。
- 加载比特流文件到 FPGA,并观察 LED 是否跟随开关的状态变化。
进阶练习:实现逻辑功能
实现 AND 门功能:
- 使用两个开关作为输入,计算逻辑与的结果并显示在 LED 上。
scalaio.led := io.sw(0) & io.sw(1) // 两个开关的逻辑与
实现多路复用器:
- 增加一个输入开关作为选择信号,另外两个开关作为数据输入,输出选择结果:
scalaio.led := Mux(io.sw(0), io.sw(1), io.sw(2)) // 实现 2:1 复用器
观察与调试:
- 检查 FPGA 上的 LED 和开关是否正确工作。
- 如果 LED 不工作,验证引脚分配是否正确。
下一步
完成以上基础实验后,您已经掌握了在 FPGA 上实现简单逻辑的技能。接下来的重点是:
- 深入学习:了解如何使用 Chisel 描述更复杂的逻辑功能。
- 测试框架:探索 Chisel 提供的测试工具(如
ChiselTest
),无需直接加载到 FPGA 上即可验证设计逻辑。
通过这些实践,您将逐步从简单设计过渡到复杂的数字系统开发。