前言
本章将继续讲解Chisel硬件设计内容,前期已经讲解了Chisel的基本语言和数据类型,以及操作符号,对基本知识不了解的读者可以查看前序文章。
本章讲述Chisel定义模块相关内容。
端口
定义端口
在Chisel中,可以使用IO
对象来定义端口。
IO
对象是一个包含输入和输出信号的容器,它可以在模块中被引用和连接。
如何定义一个带有输入和输出端口的模块,如下示例:
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val input = Input(UInt(8.W))
val output = Output(UInt(8.W))
})
// 模块的逻辑部分
// ...
io.output := io.input
}
在上面的示例中,定义了一个名为MyModule
的模块,它有一个8位的输入端口input
和一个8位的输出端口output
。
通过创建一个IO
对象,并指定其内部的信号类型和方向,我们可以定义模块的接口。
注意,UInt(8.W)
表示一个8位无符号整数类型,Input
和Output
指定了信号的方向。
在Chisel中,可以使用Bundle
来定义自定义的端口对象,它可以包含多个信号,并以结构化的方式组织。
import chisel3._
class MyIO extends Bundle {
val in_1 = Input(UInt(32.W))
val in_2 = Input(UInt(32.W))
... ...
val out_1 = Output(UInt(32.W))
}
class MyModule extends Moudle {
val io = IO(new MyIO) //端口列表
}
使用Bundle
可以更好地组织和管理多个信号,使得端口对象更加结构化和易于理解。
端口翻转
在Chisel中,Flipped
关键字用于翻转端口的方向。它可以应用于Bundle或Data类型,将其所有的输入信号转换为输出信号,将所有的输出信号转换为输入信号。
对于需要相连的模块,很大可能存在同名但传输方向相反的端口,故此可以使用Flipped
将端口翻转使用。
下面是一个示例,展示如何使用Flipped
来翻转端口方向:
import chisel3._
class MyModule_1 extends Module {
val io = IO(new MyIO)
// 其他逻辑部分
// ...
}
class MyModule_2 extends Module {
val io = IO(Flipped(new MyIO))
// 其他逻辑部分
// ...
}
使用Flipped
可以方便地翻转端口的方向,使得输入信号变为输出信号,输出信号变为输入信号,从而符合特定的设计需求。
端口连接
在 Chisel 中,可以使用 <>
运算符来进行端口连接。这个运算符允许将一个模块的输出端口连接到另一个模块的输入端口。
下面是一个简单的示例,展示了如何使用 <>
运算符连接两个模块的端口:
import chisel3._
class Source extends Module {
val io = IO(new Bundle {
val dataOut = Output(UInt(8.W))
})
// 模拟数据产生
val data = RegInit(0.U(8.W))
data := data + 1.U
io.dataOut := data
}
class Sink extends Module {
val io = IO(new Bundle {
val dataIn = Input(UInt(8.W))
})
// 在控制台打印接收到的数据
printf("Received data: %d\n", io.dataIn)
}
class Top extends Module {
val io = IO(new Bundle {})
val source = Module(new Source)
val sink = Module(new Sink)
// 使用 <> 运算符连接模块的端口
sink.io.dataIn <> source.io.dataOut
}
在上面的例子中,有三个模块:Source、Sink 和 Top。Source 模块产生一个递增的数据,并将其输出到 dataOut
端口。Sink 模块接收数据并在控制台上打印。Top 模块将 Source 和 Sink 模块连接在一起。
在 Top 模块中,使用 <>
运算符将 Source 模块的 dataOut
端口连接到 Sink 模块的 dataIn
端口。这样,Source 模块产生的数据将通过连接传递给 Sink 模块。
使用 <>
运算符可以简化端口连接的代码,并提高代码的可读性。它是 Chisel 中常用的一种连接方式。
端口修改
可选字段法:
在 Chisel 中,可以使用 Some
来表示一个可选字段的值。Some
是 Scala 中的一个类,用于封装一个值,并表示该值存在。
下面是一个简单的示例,演示如何使用 Some
创建一个可选字段:
import chisel3._
class MyModule(myFlag:Boolean) extends Module {
val io = IO(new Bundle {
val input = Input(UInt(8.W))
val output_1 = Output(UInt(8.W))
val output_2 = if (myFlag) Some(Output(UInt(8.W))) else None
})
if (myFlag) {
// 使用get方法获取
io.output_2.get := io.in
}
}
通过使用 Some
和 get
,您可以根据需要创建可选字段,并根据字段的存在与否进行端口连接。这样可以提供更大的灵活性和配置性。
Zero-Width:
在 Chisel 中,Zero-Width 表示宽度为零的信号。它可以在定义端口、信号和信号连接等场景中使用。
下面是一些示例,展示了 Zero-Width 的使用:
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val input = Input(Bool()) // 单位宽度输入信号
val output = Output(UInt(0.W)) // 零宽度输出信号
})
}
在上面的示例中,input
是一个单位宽度的输入信号,即宽度为1。output
是一个零宽度的输出信号,宽度为0。零宽度的信号在某些情况下是有用的,比如用作控制信号、状态信号等。
使用 Zero-Width 的信号时,不能对其进行赋值或进行位操作,因为它没有具体的位数。但是,可以使用 Zero-Width 的信号进行模块之间的连接,用于表示某些特定的信号语义。
模块
模块分类
在 Chisel 中,有三种常见的模块类型:Module
、MultiIOModule
和 RawModule
。
-
Module
:Module
是最常用的模块类型,用于定义具有输入和输出端口的硬件模块。它是 Chisel 的标准模块类型,提供了一种简洁的方式来描述模块的功能和连接。Module
继承自MultiIOModule
。
-
MultiIOModule
:MultiIOModule
是Module
的超类,它允许在模块中定义多个输入和输出端口。与Module
类型相比,它提供了更多的灵活性,可以定义更多的端口信号。大多数情况下,使用Module
就足够了,只有在需要定义多个端口时才需要使用MultiIOModule
。
-
RawModule
:RawModule
是一个更底层的模块类型,它提供了对原始的 Verilog 原语的直接访问和使用。与Module
和MultiIOModule
不同,RawModule
并不提供高级抽象,而是允许直接编写和使用 Verilog 代码。这种模块类型适用于需要直接使用 Verilog 原语或与其他 Verilog 代码进行交互的情况。
大多数情况下,使用 Module
类型就可以满足设计需求,它提供了高级抽象和便捷的功能。MultiIOModule
在需要定义多个端口时很有用,而 RawModule
则适用于需要直接访问和使用 Verilog 原语的场景。选择适当的模块类型取决于设计的需求和复杂性。
模块定义
- Module 类是 Chisel 中用于定义硬件模块的基类,它具有以下特点:
-
- 继承关系:Module 类是所有 Chisel 模块的基类,其他模块类需要继承自 Module 类。
-
- 端口定义:在 Module 类中,可以通过
IO
方法定义输入和输出端口,使用Input
和Output
方法指定端口的方向和数据类型。
- 端口定义:在 Module 类中,可以通过
-
- 层次结构:Module 类支持嵌套和层次结构,即可以在一个模块内部实例化其他模块,并将其作为子模块。这种层次结构的组织可以方便地构建复杂的硬件设计。
-
- 时钟和复位:Module 类提供了
clock
和reset
两个默认的时钟和复位信号,它们是模块中的常用信号,可以直接在模块中使用。
- 时钟和复位:Module 类提供了
-
- 状态和逻辑:在 Module 类中,可以定义状态变量和逻辑操作来实现模块的功能。使用 Chisel 提供的各种操作符和语法,可以编写具有复杂逻辑的硬件描述。
-
- 仿真和生成:Module 类不仅可以用于在仿真环境中验证设计的正确性,还可以生成对应的硬件描述,用于实际的 FPGA 或 ASIC 实现。
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val input = Input(UInt(4.W))
val output = Output(UInt(8.W))
})
// 模块的功能逻辑
io.output := io.input + 1.U
}
Module 类是 Chisel 中用于定义硬件模块的核心类,它提供了端口定义、层次结构、时钟和复位支持以及状态和逻辑操作的能力,帮助开发者构建复杂的硬件设计。
MultiIOModule
用于定义具有多个输入输出端口的模块。相比于普通的Module
类,MultiIOModule
允许在一个模块中定义多个IO
对象,每个IO
对象可以包含多个输入输出端口。以下是MultiIOModule
的特点:
-
- 多个
IO
对象:使用IO
方法可以在MultiIOModule
中定义多个IO
对象,每个IO
对象代表一个输入输出接口。
- 多个
-
- 简化的端口定义:使用
IO
方法时,可以直接定义输入输出端口的类型和方向,而无需再使用Bundle
进行封装。
- 简化的端口定义:使用
-
- 无需
Bundle
类型:与普通的Module
不同,MultiIOModule
中的输入输出端口可以直接使用基本数据类型(如UInt
、Bool
)作为其类型,而无需使用Bundle
类型。
- 无需
-
- 简化的端口连接:由于每个
IO
对象代表一个接口,因此可以直接通过点运算符将模块的输入输出端口与相应的IO
对象的端口进行连接。
- 简化的端口连接:由于每个
import chisel3._
class MyModule extends MultiIOModule {
val input = IO(Input(UInt(4.W)))
val output = IO(Output(UInt(8.W)))
// 模块的功能逻辑
output := input + 1.U
}
RawModule
提供了更底层的硬件描述能力。与普通的Module
不同,RawModule
不提供自动生成的io
对象,而是要求开发者手动定义所有的输入和输出端口。RawModule
的主要特点包括:
-
- 自定义输入和输出端口:使用
Input
、Output
或Flipped
等方法手动定义输入和输出端口,而不使用自动生成的io
对象。
- 自定义输入和输出端口:使用
-
- 自定义信号线和寄存器:可以直接创建
Wire
、Reg
等信号线和寄存器对象,并在硬件描述中进行连接和操作。
- 自定义信号线和寄存器:可以直接创建
-
- 更灵活的信号处理能力:可以使用原始的硬件原语和底层操作,对信号进行更细粒度的控制和处理。
import chisel3._
import chisel3.util._
class MyRawModule extends RawModule {
// 定义输入和输出端口
val in = IO(Input(UInt(8.W)))
val out = IO(Output(UInt(8.W)))
// 定义内部信号线
val wire1 = Wire(UInt(8.W))
val wire2 = Wire(UInt(8.W))
// 定义寄存器
val reg = RegInit(0.U(8.W))
// 连接信号线和寄存器
wire1 := in + 1.U
wire2 := reg + 2.U
reg := wire1 + wire2
// 连接输出端口
out := reg
}
由于 RawModule
提供了更底层的硬件描述能力,它在某些特定场景下非常有用,例如需要与外部接口进行底层硬件通信的模块设计。然而,由于需要手动定义所有的端口和信号线,相对于使用 Module
进行高级抽象的描述方式,编写和维护代码可能会更加繁琐和复杂。
模块例化
在 Chisel 中,模块的实例化可以通过使用 Module
类的构造函数来完成。
class MyModule extends Module {
val io = IO(new Bundle {
val input = Input(UInt(8.W))
val output = Output(UInt(8.W))
})
// 模块的实现
// 例如,将输入信号直接赋值给输出信号
io.output := io.input
}
// 模块实例化
val myModule = Module(new MyModule())
// 使用实例的端口进行连接
myModule.io.input := inputSignal
outputSignal := myModule.io.output
在上面的示例中,首先定义了一个继承自 Module
的自定义模块 MyModule
。在模块内部,通过 IO
函数定义了输入和输出端口。然后,在模块实例化阶段,通过调用 Module
构造函数来创建 MyModule
的一个实例 myModule
。最后,可以使用实例的端口与其他信号进行连接。
需要注意的是,Chisel 的模块实例化是通过调用构造函数创建对象的方式进行的,而不是传统的 Verilog 中的实例化语法。此外,Chisel 中的模块实例化是静态的,即模块在运行时实例化一次,而不支持动态的实例化。
同时也可以使用 VecInit
来实例化多个模块。VecInit
函数接受一个包含多个元素的序列,并将其转换为 Chisel 的 Vec
类型。
以下是一个使用 VecInit
实例化多个模块的示例:
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val input = Input(UInt(8.W))
val output = Output(UInt(8.W))
})
// 模块的实现
// 例如,将输入信号直接赋值给输出信号
io.output := io.input
}
// 定义模块数量
val numModules = 4
// 实例化多个模块并连接
val myModules = VecInit(Seq.fill(numModules)(Module(new MyModule()).io))
// 使用实例的端口进行连接
for (i <- 0 until numModules) {
myModules(i).input := inputSignals(i)
outputSignals(i) := myModules(i).output
}
使用 VecInit
和 Seq.fill
创建了一个包含多个 MyModule
实例的 Vec
,其中 numModules
定义了模块的数量。接下来,通过循环遍历模块实例,使用实例的端口进行连接。
通过使用 VecInit
和 Seq.fill
,可以方便地一次性实例化多个模块,并进行模块之间的信号连接。
线网
Wire
在 Chisel 中,wire
是用于声明信号连接的关键字。它用于声明一个连接两个或多个电路元件的信号线。
在 Chisel 中,可以使用 wire
关键字来声明一个信号,并在后续的逻辑中对其进行操作和连接。
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val input = Input(UInt(8.W))
val output = Output(UInt(8.W))
})
// 声明一个 wire
val myWire = Wire(UInt(8.W))
// 对 wire 进行操作
myWire := io.input + 1.U
// 将 wire 连接到输出端口
io.output := myWire
}
使用 wire
关键字可以方便地声明信号,并在逻辑中对其进行操作和连接,使得电路设计更加清晰和可读。
WireDefault
在 Chisel 中,可以为 Wire 类型的信号提供默认值。为 Wire 提供默认值可以确保在电路中的所有位置,如果信号未被显式地赋值,那么它将具有预定义的默认值。
为了为 Wire 类型的信号提供默认值,可以使用 WireDefault
类。WireDefault
类包装了 Wire 类型的信号,并为其提供默认值。
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val input = Input(UInt(8.W))
val output = Output(UInt(8.W))
})
// 声明一个带有默认值的 wire
val myWire = WireDefault(0.U(8.W))
// 对 wire 进行操作
myWire := io.input + 1.U
// 将 wire 连接到输出端口
io.output := myWire
}
使用 WireDefault
类为 myWire
提供了默认值为 0 的 8 位无符号整数。如果 myWire
未在电路中的任何位置被显式地赋值,它将保持默认值。
通过为 Wire 类型的信号提供默认值,可以确保在电路中的所有位置,信号都具有预定义的默认值,从而增加了电路的可靠性和可维护性。
线网驱动
在 Chisel 中,可以使用 Wire
或 Reg
定义线网(wire)或寄存器(register),然后使用 :=
运算符将信号驱动到线网或寄存器上。
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val input = Input(UInt(8.W))
val output = Output(UInt(8.W))
})
val myWire = Wire(UInt(8.W)) // 定义一个线网
myWire := io.input // 驱动线网
io.output := myWire // 连接线网到输出端口
}
使用 Wire
定义了一个 8 位无符号整数的线网 myWire
。然后,使用 :=
运算符将 io.input
信号驱动到 myWire
上。最后将 myWire
连接到输出端口 io.output
。
如果一个线网没有被显式地驱动,那么它将被认为是未驱动的(unconnected)。
未驱动的线网可能会导致逻辑错误或仿真问题,因为它们没有定义明确的值。为了避免未驱动的线网,通常需要确保每个线网都有合适的驱动。
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val input = Input(UInt(8.W))
val output = Output(UInt(8.W))
})
val myWire = Wire(UInt(8.W))
// 驱动线网之前进行默认值赋值
myWire := 0.U
myWire := io.input
io.output := myWire
}
驱动线网之前,对 myWire
进行了默认值赋值,确保它在被驱动之前具有明确的值。
请注意
,在某些情况下,未驱动的线网可能是有意为之,例如在设计中使用了高阻抗(Z)状态。在这种情况下,可以使用 DontCare
表示线网的值不重要。
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val input = Input(UInt(8.W))
val output = Output(UInt(8.W))
})
val myWire = Wire(UInt(8.W))
myWire := DontCare // 表示线网的值不重要
io.output := myWire
}
使用 DontCare
将 myWire
的值设置为不重要,表示我们不关心它的具体值。
此外还可以通过一些设置,CompileOptions
用于设置编译选项,它允许你为 Chisel 编译器提供一些特定的配置和指令。
CompileOptions
是一个 ChiselExecutionOptions
对象,它可以用于配置编译过程中的各种选项,例如设置生成的 Verilog 文件的输出路径、指定需要引用的外部库等,同时可以设置检查机制。
class MyModule extends Module {
// 严格检测
override val compileOptions = chisel3.ExplicitCompileOptions.NotStrict.copy(explicitInvalidate = true)
// 不严格检查
override val compileOptions = chisel3.ExplicitCompileOptions.NotStrict.copy(explicitInvalidate = false)
}
开启检查时,编译过程中会产生not fully initialized
错误,组合逻辑真值表不完整,不能综合出完整电路。
寄存器
在 Chisel 中,可以使用 Reg
类型来定义寄存器。Reg
类型是 Chisel 提供的一种特殊类型,用于表示寄存器。
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val dataIn = Input(UInt(8.W))
val dataOut = Output(UInt(8.W))
})
val myReg = RegInit(0.U(8.W)) // 定义一个8位宽的寄存器,并初始化为0
myReg := io.dataIn // 将输入数据写入寄存器
io.dataOut := myReg // 将寄存器数据输出
}
使用 RegInit
函数定义了一个8位宽的寄存器 myReg
,并将其初始化为0。然后,使用 :=
运算符将输入信号 io.dataIn
的值赋给寄存器。最后,将寄存器的值赋给输出信号 io.dataOut
。
需要注意的是,Chisel 中的寄存器是同步元素,其更新操作通常在时钟的上升沿或下降沿发生。在时序电路中,需要将寄存器的更新操作放置在适当的时钟域和时钟触发条件下,以确保正确的时序行为。
除了使用 RegInit
初始化寄存器外,还可以使用其他函数和语法来定义和操作寄存器,如 RegNext
、RegEnable
等。这些函数提供了更多的灵活性和功能,可以根据具体的设计需求进行选择和使用。
电路赋值
在硬件设计中,"when"是Chisel中用于条件赋值的关键字。它类似于其他编程语言中的"if-else"语句,用于在特定条件下对信号进行赋值操作。
when(condition) {
// 赋值语句
}
其中,"condition"是一个布尔表达式,用于指定条件。当条件为真时,会执行大括号中的赋值语句。
下面演示如何在Chisel中使用"when"进行条件赋值:
val a = Wire(UInt(4.W))
val b = Wire(UInt(4.W))
val result = Wire(UInt(4.W))
when(a > b) {
result := a
}.otherwise {
result := b
}
在上面的示例中,根据条件"a > b"的真假,将变量"result"赋值为变量"a"或变量"b"。
“when"语句还可以与其他控制流语句结合使用,如"else"和"elsewhen”,以实现更复杂的条件逻辑。
总结
o(▽)o