本节将介绍新手使用者的第一个硬件模块、一个测试用例以及如何运行它。它会包含很多我们不理解的东西,这没关系。让我们忘记大致的轮廓,这样就可以不断地返回到这个完整的、可工作的示例来巩固我们所学到的内容。
像Verilog一样,我们可以在Chisel中声明模块定义。下面的例子是一个Chisel Module, Passthrough,它有一个4位输入in和一个4位输出out。模块通过组合方式连接in和out,因此in驱动out。
import chisel3._
import chisel3.util._
// Chisel代码:声明一个新的模块定义
class Passthrough extends Module{
val io = IO(new Bundle{
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
io.out := io.in
}
上面代码很多知识点,下面来解释如何从所描述的硬件的角度来考虑每一行代码。
首先,我们声明了一个名为Passthrough的新模块。Module是一个内置的Chisel类,所有硬件模块都必须扩展它。
class Passthrough extends Module {
我们在一个特殊的val类型的io 中声明所有的输入和输出端口,它必须被称为io,并且是一个io对象或实例,这需要**io (instantiated_bundle)**形式的东西。
val io = IO(...)
我们声明了一个新的硬件结构类型(Bundle),它包含一些命名信号in和out,分别指向Input和Output。
new Bundle {
val in = Input(...)
val out = Output(...)
}
我们声明信号的硬件类型。在本例中,它是宽度为4的无符号整数。
UInt(4.W)
我们将输入端口连接到输出端口,例如 io.in 驱动 io.out。注意,:= 操作符是一个Chisel操作符,它表示右信号驱动左信号,是一个有向算子。
io.out := io.in
关于硬件构造语言(HCLs)的巧妙之处在于,我们可以将底层编程语言用作脚本语言。例如,在声明了我们的Chisel模块之后,我们使用Scala调用Chisel编译器,将Chisel Passthrough 翻译成Verilog Passthrough。这个过程被称为精化。
// Scala代码:把我们的Chisel设计翻译成Verilog
//不要担心理解这段代码;这是非常复杂的Scala
println(getVerilog(new Passthrough))
如果我们将Scala的知识应用到这个例子中,我们可以看到Chisel模块被实现为一个Scala类。就像其他Scala类一样,我们可以让Chisel模块接受一些构造参数。在本例中,我们创建了一个新类PassthroughGenerator,它将接受一个整数width,用于指定输入和输出端口的宽度:
//Chisel代码:但传入一个参数来设置端口的宽度
class PassthroughGenerator(width :Int) extends Module{
val io = IO(new Bundle{
val in = Input(UInt(width.W))
val out = Output(UInt(width.W))
})
io.out := io.in
}
// 生成不同宽度的模块
println(getVerilog(new PassthroughGenerator(10)))
println(getVerilog(new PassthroughGenerator(20)))
因为PassthroughGenerator不再描述单个模块,而是描述由width参数化的一组模块,所以我们将这个Passthrough称为generator。
注意,生成的Verilog对输入/输出使用不同的位宽,这取决于分配给width参数的值。让我们来深入了解一下这是如何工作的。因为Chisel模块是普通的Scala类,所以我们可以使用Scala的类构造函数来参数化我们的设计。你可能注意到这个参数是由Scala启用的,而不是Chisel;Chisel没有额外的用于参数化的API,但是我们可以简单地利用Scala特性来参数化。
Chisel中有内置的测试功能,下面的例子是一个Chisel测试工具,它将值传递给Passthrough的输入端口in的实例,并检查输出端口out是否有相同的值。
这里有一些高级的Scala。然而,除了poke和expect命令,不需要理解其他任何命令,我们可以将其余代码视为编写这些简单测试的样板代码。
// Scala代码:test运行单元测试
// test接受一个用户模块,并有一个代码块,将pokes和expect应用到测试电路(c)
test(new Passthrough()){c =>
c.io.in.poke(0.U) // 将输入设置为0
c.io.out.expect(0.U) // 期待的正确输出为0
c.io.in.poke(1.U)
c.io.out.expect(1.U)
c.io.in.poke(2.U)
c.io.out.expect(2.U)
}
println("SUCCESS!") // Scala代码:如果能运行至此,说明测试通过
该测试接受一个Passthrough模块,将值赋给模块的输入,并检查其输出。设置一个输入,我们称之为poke。为了检查输出,我们调用expect。如果我们不想将输出与预期值进行比较(没有断言),我们可以peek代替来检查输出。
注意,poke和expect这两个操作都需要正确类型的字面值。 如果poke是一个UInt(),你必须提供一个UInt字面量(例如:c.io.in.poke(10.U),同样地,如果输入是Bool(), poke会期望true.B或false.B。
如果在理解生成的硬件方面遇到困难时,并且能够轻松地读取结构化的Verilog和/或FIRRTL (Chisel的IR,相当于Verilog的一个仅用于合成的子集),那么我们可以尝试查看生成的Verilog,以查看Chisel执行的结果。下面是一个生成Verilog和FIRRTL的示例。
// 查看Verilog
println(getVerilog(new Passthrough))
// 查看firrtl
println(getFirrtl(new Passthrough))