终于来到了敏捷型语言开发。chisel(Constructing Hardware in a Scala Embedded
Language)提供了更高层次的抽象,可以实现更快速的构建硬件,使得硬件更容易被理解和修改。

chisel’s env

要使用chisel首先要有java环境,每到这个环节我们一定要引入一个环境管理工具(因为依旧有多版本java的使用需求)。

这里引入的工具是SDKMAN!,他类似于nodejs的nvm和python的pyenv,可以使用SDKMAN!管理java环境的不同版本。

1
curl -s "https://get.sdkman.io" | bash

运行以上命令就可以下载SDKMAN!.

我们可以通过SDKMAN!来安装指定版本的OpenJDK.

1
sdk install java 17.0.1-open # install openjdk 17.0.1

安装完成之后的切换版本:

1
2
3
sdk list java
sdk use java 17.0.1-open
sdk current java

如果要查看所有通过sdk已安装的java版本,可以查看~/.sdkman/candidates/java

chisel的运行需要scala,这两者的安装方式如下:

1
2
sudo pacman -S sbt
sdk install scalacli

我们可以通过以下命令确认chisel环境准备就绪:

1
2
curl -O -L https://github.com/chipsalliance/chisel/releases/latest/download/chisel-example.scala
scala-cli chisel-example.scala

如果成功构建了chisel项目,则说明scala环境没有问题。

关于firtool,chisel6.0以后会自动配置,除非使用的是很老版本的OS;如果需要自己配置firtool,可以在~/.zshrc中加入:

1
export CHISEL_FIRTOOL_PATH=/path/to/firtool

scala-cli一般只适用于少数文件的组织,对于一个较大的项目,准备一个构建工具可以方便很多。sbt就是一个构建工具,印象中的chisel最开始就是用sbt来组织项目的,但是今天打开官方文档的时候发现现在更推荐使用mill,那我们也来尝尝新:

1
2
yay -S mill
# sudo pacman -S sbt

verilator

如果需要接入verilator,还需要至少支持C++14和make的C++编译器,不过一般PC随便下个gcc就可以解决问题。

verilator的下载:

1
sudo pacman -S verilator

neovim

chisel本质还是scala,所以可以通过:

1
:MasonInstall metals

为nvim添加智能补全,跳转等功能。

syntax

对于一个没有怎么认真学过java的人来说,chisel这个语法确实是抽象。不过好在我们已经写过了DFT系列3-4,这个java的语法还是和UVM有一点相像

首先是最重要的继承语法,和UVM很像:

1
2
3
4
5
6
7
8
//> using scala 2.13.18
//> using dep org.chipsalliance::chisel:7.11.0

import chisel3._

class MyModule extends Module{

}

在这里和UVM一样,用extends <base_class>完成类的继承。

最开头的//>用来引入依赖,分别指定了scala和chisel版本,比如此处使用了scala 2.13.18, chisel 7.11.0版本。不过chisel7已经被归档了,其实还是用chisel6更好一点。

class

同样是类似UVM,chisel也内置了很多class和method用来简化设计。有以下几个常用的class:

  1. Module

这是chisel中最基本的构建块,每个硬件设计模块都应该继承Module.

1
2
3
4
5
6
7
8
9
10
11
12

//> using scala 2.13.18
//> using dep org.chipsalliance::chisel:7.11.0

import chisel3._

class MyModule extends Module{
val io = IO(new Bundle{
val in = Input(UInt(8.W)
val out = output(UInt(8.W)))
})
}

在上面的代码中,创建了一个MyModule的新模块,在这个模块中,创建了一个IO模块(名为io),传入的参数是一个Bundle类型,包括一个输入和输出,输入和输出都是8.W.

val在设计中常用于定义硬件信号,寄存器,模块等,他本质是scala的关键字,定义一个不可变的变量(const),此处是不可变绑定,也就是说val x,x的值可以改变,但是不能把他变成另一种类型或者指向其他对象。

UInt(8.W)代表一种数据类型,也就是无符号整数,传入的参数用来定义无符号整数的格式,此处定义了8.W.W代表参数用于控制width,意味着8用于指定位宽。

  1. Bundle

用于组合多个信号端口,作为模块的输入输出接口。

  1. IO

用于声明模块的输入输出信号。

  1. UInt, SInt

代表无符号和有符号整数类型。

  1. Wire

用于定义组合逻辑中的信号,通常表示连线。

1
2
val sum = Wire(UInt(8.W))
sum := io.in1 + io.in2

:=用于重新赋值(也就是第二次及以后的赋值)。

  1. Reg

用于定义时序逻辑的存储器,用来存储数据。

1
2
val reg = Reg(UInt(8.W))
reg := io.in1 + io.in2
  1. RegInit

表示带有初始值的寄存器。

1
2
val reg = RegInit(0.U(8.W))
reg := io.in1 + io.in2

初始值被设定为0.U(8.W)0被指定为.U,就是代表数值为无符号数0,8.W就是8位宽。

  1. Clock

定义时钟信号。

1
val clk = Clock()
  1. Reset

定义复位信号。

1
val rst = Reset()
  1. Cat

可以拼接多个信号。

1
2
3
val a = UInt(4.W)
val b = UInt(4.W)
val c = Cat(a, b)
  1. Mux

实现多路选择器(其实只能二选一,多路需要嵌套或者用其他方式比如Vec+options实现)。

1
2
3
val sel = UInt(2.W)
val out = Mux(sel === 1.U, io.in1, io.in2)
// val out = Mux(sel === 1.U, io.in1, Mux(sel === 2.U, io.in2, io.in3))
  1. Decoupled

用于流水线设计的数据通道,是可以设计数据流和控制信号分离的类型。
定义带有valid和ready信号的数据流通道。

1
val Decoupled = Decoupled(UInt(8.W))
  1. Vec

一个向量类型。

1
val v = Vec(4, UInt(8.W))

此外还有控制语句when/otherwise:

1
2
3
4
5
when(io.in1 > 0.U){
reg := io.in1
}.otherwise{
reg := 0.U
}

for:

1
2
3
for(i <- 0 until 4){
regs(i) := io.in(i)
}

test

可以尝试写一个chisel版本的adder + tb.

adder.scala:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//> using scala "2.13.12"
//> using dep "org.chipsalliance::chisel:6.5.0"
//> using dep "edu.berkeley.cs::chiseltest:6.0.0"
//> using dep "org.scalatest::scalatest:3.2.20"
//> using plugin "org.chipsalliance:::chisel-plugin:6.5.0"
import chisel3._

class Adder(width: Int) extends Module{
val io = IO(new Bundle{
val a = Input(UInt(width.W))
val b = Input(UInt(width.W))
val sum = Output(UInt(width.W))
})
io.sum := io.a + io.b
}

adder_tb.scala:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec

class AdderTest extends AnyFlatSpec with ChiselScalatestTester{
"Adder" should "add two numbers correctly" in {
test(new Adder(8)){ c =>

c.io.a.poke(3.U)
c.io.b.poke(5.U)

c.clock.step(1)

c.io.sum.expect(8.U)

c.io.a.poke(10.U)
c.io.b.poke(20.U)
c.clock.step(1)
c.io.sum.expect(30.U)
}
}
}

此处test代码确实有点过于抽象了。

不过其实新的东西没有很多。首先是这个poke,看过DFT系列第四期就知道这个是写入的意思,当时UVM中frontdoor/backdoor,mirror/desired value概念很多容易混,但是这里的poke就是直接驱动信号,不绕过任何东西。

也就是说这里的poke就是UVM RAL里的write…

其次是新加的两个import,第一个import chiseltest._提供了poke expect clock.step等调试用方法和类,第二个import org.scalatest.flatspec.AnyFlatSpec,来自scalaTest,比如说很奇怪的一句:"Adder" should "add two numbers correctly",他是来自scala而不是chisel.

"Adder" should "add two numbers correctly"是用来描述测试的,"Adder" should "add two numbers correctly" in { ... }含义就是之后in之内的这个测试我要称他为:adder shoud add two numbers correctly,之后的in{}才表示具体怎么执行测试。最核心的一句是test(new Adder(8)) { c =>,在这里做了三件事,首先是实例化Adder用于之后的测试行为,其次是提供测试handle,c =>就是说Adder测试handle是c,类似于Adder c(.a(a), .b(b)),最后是自动生成电路并且启动仿真。

这个scala语法确实抽象,本质上是允许把should(“Adder”, “add two numbers correctly”)写成接近英语表述的形式。不过可以把"Adder" should "add two numbers correctly" in整体替换为test("adder should add two numbers correctly."),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import chisel3._
import chiseltest._
import org.scalatest.funsuite.AnyFunSuite

class AdderTest extends AnyFunSuite with ChiselScalatestTester {
test("adder basic test") {
test(new Adder(8)) { c =>
c.io.a.poke(3.U)
c.io.b.poke(5.U)
c.clock.step()
c.io.sum.expect(8.U)
}
}
}

scala可能会下一大堆没什么用的东西(尤其是依赖写错的时候),可以通过rm -rf ~/.cache/coursier/ ~/.cache/scalacli/ ~/.ivy2/cache/删除。(有时候有路径污染也需要这个命令来重新构建)

run proj

对于已经写好的chisel项目,一般考虑scala-cli, mill, sbt的方式来运行。

scala-cli

scala-cli的常用命令:

1
2
3
4
scala-cli compile . # compile files
scala-cli test . # compile + run test
scala-cli run . --main-class AdderMain # run + specify main
scala-cli repl . # interactive REPL

当然用.指定文件是很简陋的,之后文件架构组织起来还是需要对文件依次指定。

如果要生成verilog文件,需要在adder.scala添加一个main入口:

1
2
3
4
5
6
7
8
9
10
11
12
//> using dep "org.chipsalliance::chisel:6.5.0"
import circt.stage.ChiselStage

object AdderMain extends App {
ChiselStage.emitSystemVerilogFile(
new Adder(8), //要综合的模块实例
args = Array("--target-dir", "generated"),  //输出目录
firtoolOpts = Array(             //传给firtool的选项
"-disable-all-randomization",        //去掉寄存器初始化代码
"-strip-debug-info")             //去掉调试信息
)
}

再运行scala-cli run . --main-class AdderMain,就会在当前路径下生成generated/Adder.sv.

如果只是想输出到终端:

1
2
3
4
object AdderMain extends App {
val sv = ChiselStage.emitSystemVerilog(new Adder(8))
println(sv)
}

如果想要生成.v文件,可以在firtool加入:

1
2
"--lowering-options=disallowLocalVariables,disallowPackedArrays,noAlwaysFF",
"--format=verilog"

或者使用emitVerilog.

如果想要生成多个模块到多个文件,多个实例化模块分别调用函数即可:

1
2
3
4
5
6
7
8
object GenAll extends App {
val opts = Array("--target-dir", "generated")
val ftOpts = Array("-disable-all-randomization", "-strip-debug-info")

ChiselStage.emitSystemVerilogFile(new Adder(8), opts, ftOpts)
ChiselStage.emitSystemVerilogFile(new Adder(16), opts, ftOpts)
ChiselStage.emitSystemVerilogFile(new SomeOtherMod, opts, ftOpts)
}

ChiselStage.emitSystemVerilogFile接收的三个参数是实例化模块,输出路径(一般指定这个就够了,这里其实应该是ChiselStage自身的参数),输出参数(传给firtool的参数)。

sbt

sbt不会自动构建(全部)文件架构,得要自己写。

最简单的方法是:

1
2
3
mkdir myproject
cd myproject
mkdir -p src/main/scala src/test/scala project

然后手动写两个文件,一个是build.sbt,描述环境依赖:

1
2
3
4
5
6
7
scalaVersion := "2.13.12"
libraryDependencies ++= Seq(
"org.chipsalliance" %% "chisel" % "6.5.0",
"edu.berkeley.cs" %% "chiseltest" % "6.0.0" % Test,
"org.scalatest" %% "scalatest" % "3.2.20" % Test,
)
addCompilerPlugin("org.chipsalliance" %% "chisel-plugin" % "6.5.0" cross CrossVersion.full)

一个是project/build.properties:

1
sbt.version 1.12.10

然后把adder.scala写到src/main/scalaadder_tb.scala写到src/test/scala.

sbt其实可以构建模板结构,sbt new chipsalliance/chisel-template.g8,但是很老了。

常用命令:

1
2
3
4
5
sbt compile
sbt test
sbt run
sbt "runMain AdderMain"
sbt clean

mill

mill也要手动创建,不过可以稍微简单一点。

1
2
3
mkdir myproject
cd myproject
mkdir -p adder/src/ adder/test/src

设计文件放到adder/src,测试adder/test/src.

build.mill:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import mill._, scalalib._

object adder extends ScalaModule {
def scalaVersion = "2.13.12"
def ivyDeps = Agg(
ivy"org.chipsalliance::chisel:6.5.0"
)
def scalacPluginIvyDeps = Agg(
ivy"org.chipsalliance:::chisel-plugin:6.5.0"
)
object test extends ScalaTests {
def ivyDeps = Agg(
ivy"edu.berkeley.cs::chiseltest:6.0.0",
ivy"org.scalatest::scalatest:3.2.20"
)
def testFramework = "org.scalatest.tools.Framework"
}
}

常用命令:

1
2
3
4
mill adder.compile
mill adder.test
mill.run
mill adder.clean

verilator

chisel6+chiseltest默认后端是纯JVM仿真treadle2,如果想要切换到verilator,需要在测试文件中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
import chiseltest._
import chiseltest.simulator.VerilatorBackendAnnotation
import org.scalatest.flatspec.AnyFlatSpec

class AdderTest extends AnyFlatSpec with ChiselScalatestTester {
"Adder" should "work" in {
test(new Adder(8)).withAnnotations(Seq(VerilatorBackendAnnotation)) { dut =>
dut.io.a.poke(3.U)
dut.io.b.poke(5.U)
dut.io.out.expect(8.U)
}
}
}

这样verilator可以把chisel编译成C++仿真,速度会比treadle2快很多。