1第一个Chisel模块

本文介绍了如何使用Chisel,Scala的领域专用语言,实现硬件模块,包括基本模块定义、参数化模块、测试用例及Verilog/FIRRTL转换。通过一个名为Passthrough的模块实例,逐步学习模块的创建和验证过程。
摘要由CSDN通过智能技术生成

本节的目的

通过上一章的学习,您应该对Scala已经有所了解,我们可以开始来看看怎么来实现硬件。Chisel的全称是嵌入在Scala中的硬件构造语言(Constructing Hardware In a Scala Embedded Language)。这意味着它是Scala中的领域专用语言(DSL),允许您在同一代码中同时利用Scala和Chisel编程。理解哪些代码是“Scala”,哪些代码是“Chisel”非常重要,我们将在稍后讨论这一点。在这一节中我们只需要将Chisel看作是实现Verilog的更好的方法就可以。这一节中我们会看到一个完整的Chisel模块和它的测试,我们现在只需要大致掌握一些要点,之后将会看到更多的例子。

val path = System.getProperty("user.dir") + "/source/load-ivy.sc"
interp.load.module(ammonite.ops.Path(java.nio.file.FileSystems.getDefault().getPath(path)))

如上一章所述,下面的语句在Scala中导入Chisel:

import chisel3._
import chisel3.util._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}



import chisel3._

import chisel3.util._

import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}

第一个Chisel模块

本节将介绍一个简单的硬件模块的实现,测试用例以及如何运行它。它可能会包含许多您还不理解的东西,不需要太纠结于细节,我们还会不断地回到这个例子来强化您所学到的东西。

**例子:一个模块**
类似于Verilog,我们可以在Chisel中定义模块(module)。下面的例子是一个名字叫做Passthrough的Chisel Module,它有一个4比特的输入,名字叫做in,还有一个4比特的输出out。这个模块的组合电路中将输入in连接到输出out,所以outin驱动。

// 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
}



defined class Passthrough

上面的代码包含了许多东西,以下我们会详细解释代码的每一行:

class Passthrough extends Module {

我们定义了一个模块,名字叫做Passthrough。 Module是一个Chisel内建的类,所有的硬件模块都必须从它继承。

val io = IO(...)

我们在一个叫做io的常量中声明所有的输入输出端口。这个常量的名称必须叫做io,并且它必须是类IO的对象或实例。这个类要求一个实例化的Bundle:IO(_instantiated_bundle_)

new Bundle {
    val in = Input(...)
    val out = Output(...)
}

我们声明了一个新的硬件结构类型(Bundle),它包含了信号inout,这两个信号分别带有方向Input和Output。

UInt(4.W)

我们声明了一个信号的硬件类型,它是宽度为4的无符号整数。

io.out := io.in

我们将输入端口和输出端口连接,这里io.in 会驱动 io.out。注意:=操作符是一个Chisel操作符,它表示右边的信号驱动左边的信号,它具有方向性。

硬件构造语言(HCL)的巧妙之处在于我们可以使用底层编程语言Scala作为脚本语言。例如,在定义了上面的Chisel模块之后,我们可以使用Scala调用Chisel编译器来将Chisel的Passthrough描述转换成为Verilog的Passthrough描述。这一步叫做展开(elaboration)

// Scala代码:将Chisel的设计展开成为Verilog设计
//下面生成的代码比较复杂,看不懂的话不需要太担心
println(getVerilog(new Passthrough))
[info] [0.002] Elaborating design...
[info] [0.605] Done elaborating.
Total FIRRTL Compile Time: 476.9 ms
module cmd2HelperPassthrough(
  input        clock,
  input        reset,
  input  [3:0] io_in,
  output [3:0] io_out
);
  assign io_out = io_in; // @[cmd2.sc 6:10]
endmodule

注意上面生成的Verilog模块叫做cmd<#>HelperPassthrough,这是因为在jupyter里面运行的关系。在正常的环境中,名字应该就叫做Passthrough。不过这是重要的一课:虽然Chisel会尽可能的保留硬件模块或者其他硬件组件的名称,但是并不能保证总是这样。

**例子:一个模块生成器(Generator)**
如果我们将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)))



[info] [0.000] Elaborating design...
[info] [0.077] Done elaborating.
Total FIRRTL Compile Time: 29.1 ms
module cmd4HelperPassthroughGenerator(
  input        clock,
  input        reset,
  input  [9:0] io_in,
  output [9:0] io_out
);
  assign io_out = io_in; // @[cmd4.sc 6:10]
endmodule

[info] [0.000] Elaborating design...
[info] [0.005] Done elaborating.
Total FIRRTL Compile Time: 27.8 ms
module cmd4HelperPassthroughGenerator(
  input         clock,
  input         reset,
  input  [19:0] io_in,
  output [19:0] io_out
);
  assign io_out = io_in; // @[cmd4.sc 6:10]
endmodule

defined class PassthroughGenerator

注意上面生成的Verilog会根据width参数的值使用不同的输入/输出位宽。我们可以深入理解一下这是如何工作的,因为Chisel模块就是一个普通的Scala类,所以我们可以使用Scala类的构造函数来参数化我们的设计。

这里的可以这样参数化的原因是因为我们使用了Scala,而不是Chisel。Chisel并没有别的API来使得我们可以参数化一个设计。然而我们却可以依赖Scala来使得参数化成为可能。

在这里,因为PassthroughGenerator并不只是用来描述一个设计,它其实描述了以width为参数的一系列设计,所以我们将Passthrough称为一个生成器(generator)

测试您的硬件

没有测试,任何的硬件模块或生成器都不能称为一个完整的设计。本教程也会介绍Chisel内置的测试功能。下面的例子是一个Chisel测试,它将值传递给模块Passthrough的输入端口in,并检查输出端口out上是否能看到相同的值。

**例子:一个测试**
这里有一些高级的Scala语法。但是,除了pokeexpect语句之外,您不需要理解其他任何东西。您可以将其余代码视为简单的样板来编写这些简单的测试。

// Scala代码:调用驱动(Driver)来实例化 Passthrough 和 PeekPokeTester,并且运行测试。
// 如果不理解下面代码的话不要担心;这是非常复杂的Scala代码。
// 可以简单地把它想象成运行Chisel测试的样板
val testResult = Driver(() => new Passthrough()) {
  c => new PeekPokeTester(c) {
    poke(c.io.in, 0)     // 将输入设置成 0
    expect(c.io.out, 0)  // 检查输出是否为 0
    poke(c.io.in, 1)     // 将输入设置成 1
    expect(c.io.out, 1)  // 检查输出是否为 1
    poke(c.io.in, 2)     // 将输入设置成 2
    expect(c.io.out, 2)  // 检查输出是否为 2
  }
}
assert(testResult)   // Scala代码:如果testResult等于false的话,,这里会抛出异常
println("SUCCESS!!") // Scala代码:到这里,我们的测试已经通过了



[info] [0.000] Elaborating design...
[info] [0.068] Done elaborating.
Total FIRRTL Compile Time: 19.0 ms
Total FIRRTL Compile Time: 10.5 ms
End of dependency graph
Circuit state created
[info] [0.001] SEED 1561161438550
test cmd2HelperPassthrough Success: 3 tests passed in 5 cycles taking 0.017532 seconds
[info] [0.007] RAN 0 CYCLES PASSED
SUCCESS!!
testResult: Boolean = true

上面发生了什么?这个测试接受一个类型为Passthrough的模块,驱动它的输入,并且检查它的输出。我们调用poke来驱动输入,调用expect来检查输出。如果不使用expect来比较期望值的话(没有断言的话),也可以使用peek来读出输出值。

如果所有的expect语句都为真,上面的代码会返回真(见testResult)。

**练习:写一个您自己的测试**
写两个测试,一个用来测试位宽为10的PassthroughGenerator,另一个测试位宽为20的PassthroughGenerator。至少检查两个值:0和最大值。注意这里三个问号???在Scala中有特殊含义。直接运行下面的代码会有未被实现NotImplementedError的错误。将下面的???替换成您自己的代码:

val test10result = ???

val test20result = ???

assert((test10result == true) && (test20result == true))
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!

答案 

val test10result = Driver(() => new PassthroughGenerator(10)) {
  c => new PeekPokeTester(c) {
    poke(c.io.in, 0)
    expect(c.io.out, 0)
    poke(c.io.in, 1023)
    expect(c.io.out, 1023)
  }
}

val test20result = Driver(() => new PassthroughGenerator(20)) {
  c => new PeekPokeTester(c) {
    poke(c.io.in, 0)
    expect(c.io.out, 0)
    poke(c.io.in, 1048575)
    expect(c.io.out, 1048575)
  }
}

看一看生成的Verilog/FIRRTL

如果您无法理解Chisel描述的硬件并且习惯于阅读Verilog或FIRRTL(Chisel的中间语言相当于Verilog的可综合子集),那么您可以尝试看看所生成的Verilog。

以下是生成Verilog(之前已经看过),以及FIRRTL的例子:

// 查看生成的 Verilog 代码
println(getVerilog(new Passthrough))


[info] [0.000] Elaborating design...
[info] [0.004] Done elaborating.
Total FIRRTL Compile Time: 20.5 ms
module cmd2HelperPassthrough(
  input        clock,
  input        reset,
  input  [3:0] io_in,
  output [3:0] io_out
);
  assign io_out = io_in; // @[cmd2.sc 6:10]
endmodule
// 查看生成的 FIRRTL 代码
println(getFirrtl(new Passthrough))

[info] [0.000] Elaborating design...
[info] [0.070] Done elaborating.
;buildInfoPackage: chisel3, version: 3.2-SNAPSHOT, scalaVersion: 2.12.6, sbtVersion: 1.2.7
circuit cmd2HelperPassthrough : 
  module cmd2HelperPassthrough : 
    input clock : Clock
    input reset : UInt<1>
    output io : {flip in : UInt<4>, out : UInt<4>}
    
    io.out <= io.in @[cmd2.sc 6:10]

解释一下用“printf”来调试

并不总是最好的办法,但是通常可以作为调试的第一步。

因为Chisel生成器是用来生成硬件的程序,需要好好区分是在生成电路的时候打印还是在运行电路的时候打印。

下面是想要打印的三种常见的场景:

  • Chisel生成器在生成电路的时候打印
  • 在运行电路仿真的时候打印
  • 在测试的时候打印

println是Scala内嵌的打印函数。它不可以用于电路仿真的时候打印,因为生成的电路是FIRRTL或者Verilog,而不是Scala。

下面的代码展示了这三种场景:

class PrintingModule extends Module {
    val io = IO(new Bundle {
        val in = Input(UInt(4.W))
        val out = Output(UInt(4.W))
    })
    io.out := io.in

    printf("Print during simulation: Input is %d\n", io.in)
    // chisel printf 的被解析的字符串有自己的语法
    printf(p"Print during simulation: IO is $io\n")

    println(s"Print during generation: Input is ${io.in}")
}


class PrintingModuleTester(c: PrintingModule) extends PeekPokeTester(c) {
    poke(c.io.in, 3)
    step(5) // 每次运行Chisel电路会打印
    
    println(s"Print during testing: Input is ${peek(c.io.in)}")
}
chisel3.iotesters.Driver( () => new PrintingModule ) { c => new PrintingModuleTester(c) }


[info] [0.000] Elaborating design...
Print during generation: Input is UInt<4>(IO in unelaborated cmd9$Helper$PrintingModule)
[info] [0.027] Done elaborating.
Total FIRRTL Compile Time: 30.7 ms
Total FIRRTL Compile Time: 18.8 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1561161468619
Print during simulation: Input is 3
Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3)
Print during simulation: Input is 3
Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3)
Print during simulation: Input is 3
Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3)
Print during simulation: Input is 3
Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3)
Print during simulation: Input is 3
Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3)
[info] [0.013] Print during testing: Input is 3
test cmd9HelperPrintingModule Success: 0 tests passed in 10 cycles taking 0.022628 seconds
[info] [0.014] RAN 5 CYCLES PASSED



defined class PrintingModule
defined class PrintingModuleTester
res9_2: Boolean = true

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝是天的蓝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值