【Chisel文档】 从一个硬件模块的例子中了解Chisel

1. 从第一个例子中学习

  本节将介绍新手使用者的第一个硬件模块、一个测试用例以及如何运行它。它会包含很多我们不理解的东西,这没关系。让我们忘记大致的轮廓,这样就可以不断地返回到这个完整的、可工作的示例来巩固我们所学到的内容。
  像Verilog一样,我们可以在Chisel中声明模块定义。下面的例子是一个Chisel ModulePassthrough,它有一个4位输入in和一个4位输出out。模块通过组合方式连接inout,因此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),它包含一些命名信号inout,分别指向InputOutput

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特性来参数化。


2. 测试生成的硬件

  Chisel中有内置的测试功能,下面的例子是一个Chisel测试工具,它将值传递给Passthrough的输入端口in的实例,并检查输出端口out是否有相同的值。

  这里有一些高级的Scala。然而,除了pokeexpect命令,不需要理解其他任何命令,我们可以将其余代码视为编写这些简单测试的样板代码。

// 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代替来检查输出。

注意,pokeexpect这两个操作都需要正确类型的字面值。 如果poke是一个UInt(),你必须提供一个UInt字面量(例如:c.io.in.poke(10.U),同样地,如果输入是Bool()poke会期望true.Bfalse.B

3. 查看生成的Verilog/FIRRTL

如果在理解生成的硬件方面遇到困难时,并且能够轻松地读取结构化的Verilog和/或FIRRTL (Chisel的IR,相当于Verilog的一个仅用于合成的子集),那么我们可以尝试查看生成的Verilog,以查看Chisel执行的结果。下面是一个生成Verilog和FIRRTL的示例。

// 查看Verilog
println(getVerilog(new Passthrough))
// 查看firrtl
println(getFirrtl(new Passthrough))

你可能感兴趣的:(学习,scala,fpga开发,硬件工程,dsp开发)