Posted by XnChen Blog on February 12, 2023

[TOC]

环境安装


mac osx 10.15.6

1.brew install sbt 安装Chisel3

  1. brew install verilator 安装verilator 好像是用来将Chisel转换为verilog以及输出仿真波形的工具

  2. brew cask install gtkwave 安装用于查看仿真波形的gtkwave

  3. sudo cpan install Switch 不知道为啥总之命令行打开gtkwave它要求我装Switch

  4. 现在可以在terminal直接打开gtkwave: /Applications/gtkwave.app/Contents/Resources/bin/gtkwave

  5. .bash_profile中加入假名:alias gtkwave = /Applications/gtkwave.app/Contents/Resources/bin/gtkwave,命令行输入gtkwave即可打开应用

  6. 直接用gtkwave -f <文件名>打开生成的.vcd格式文件,可以看到左侧有模块层次,点击module,把左下的port加入,就可以看到波形了

  7. 因为有一些testbench需要用到scala特性,装上scala:brew install scala。命令行输入scala可以进解释器,scala <file name>可以跑scala脚本。

Tutorial


Chisel tutorial wiki:使用上面的tutorial对chisel有个基础了解

sbt:sbt的tutorial

gtkwave安装

前辈教程

Chisel手册 翻译

这本比较全也比较新,但是翻译好像是机翻

一个可以直接用的chisel template

在线scala解释器scastie

Chisel代码目录结构


一个可以直接用的chisel templategit clone下来:

mkdir ~/ChiselProjects
cd ~/ChiselProjects

git clone https://github.com/ucb-bar/chisel-template.git MyChiselProject
cd MyChiselProject

rm -rf .git
git init
git add .gitignore *

二级目录结构如下所示:

.
├── project
│   └── target
├── src
│   ├── main
│   │   └── scala
│   └── test
│       └── scala
└── target

./build.sbt:

src文件夹中存放原代码

src/
  main/
    resources/
       <files to include in main jar here>
    scala/
       <main Scala sources>
    scala-2.12/
       <main Scala 2.12 specific sources>
    java/
       <main Java sources>
  test/
    resources
       <files to include in test jar here>
    scala/
       <test Scala sources>
    scala-2.12/
       <test Scala 2.12 specific sources>
    java/
       <test Java sources>

使用sbt run来运行项目;或者使用sbt进入Scala REPL,然后输入run指令。

sbt会自动找到classpath中的项目内容并且运行:

  • 项目根目录下的源文件
  • src/main/scala 或 src/main/java 中的源文件
  • src/test/scala 或 src/test/java 中的测试文件
  • src/main/resources 或 src/test/resources 中的数据文件
  • lib 中的 jar 文件

基础知识

赋值和重新赋值


=:: 用在电路正常赋值中 =: 用在电路生成的第一次赋值中

经验方法是将:=操作符用于已经使用=操作符赋值过的值。

位索引/切片


(类似python切片?)

高位先指定,index是零索引的。

eg:将y的15位到8位赋值给x

val x = y(15, 8)

只想要value的第x位:

val x_of_value = value(x)

可以用作赋值或者被赋值。

Chisel受限于Scala,不能给Bit类型的某几位直接复制。因此子字复制的方法是使用bool类型做中转:

val t_out_temp = Wire(Vec(n, Bool())) // 使用Bool类型的t_out_temp做中转
for(i <- 0 until n){
    t_out_temp(i) :=  PEs(i).t_out.asBool() // 将UInt类型的值强制转换为Bool值,赋给Bool类型中转
}
io.t_out_row := t_out_temp.asUInt // io.t_out_row是需要赋值的UInt对象

直接将宽度短的赋给宽度长的值,将会把短值塞在低位,和verilog一样。

位连接


import chisel3.util.Cat

or

import chisel3.util._

代码:

val A = UInt(32.W)
val B = UInt(32.W)
val bus = Cat(A, B) // concatenate A and B

其中,Cat的第一个参数在高位,第二个参数在低位。

类似verilog的if statement


在Chisel中使用when语句实现:

when(reset){
    r0 := 0.U
} .elsewhen(enable) {
    r0 := 1.U
} .otherwise {
    r0 := 2.U
}

这里的elsewhen和otherwise是不必须的。注意判断case中的reset和enable得是bool类型,如果不是bool类型,使用toBool方法进行类型转换:

when(reset.asBool){
    ...
}

在Chisel中,when语句常用来和

另外还有一个与when对应的unless语句,在util包内:

import chisel3.util._

unless(reset){
    ...
}

当reset条件为负,执行大括号内内容,否则不执行。

for循环


可以用来进行快速的重复赋值

for(i <- 0 until 5) {
    FAs(i).a := io.A(i) //把输入A的第i个bit赋给模块向量组FAs的第i个模块的输入a
}

新建wire的方法


val x = 1 //直接赋值
val x = Wire(type) //type代表了数据类型

新建reg的方法


对于reg类型,我这个新手是这么理解的:在赋值表达式右侧的reg类型是前一个周期的量,也就是reg的输出;在左侧的reg是下一个周期的输出本周期的输入。

  1. Reg
val r = Reg(type, next, init)

最基本的类型,type指明种类,next指明数据输入端(这个值被延迟一拍输送到r端),init指明初始化值。

如果都没有生命,则默认为null。

  1. RegInit

使用resetData指明该reg初始化时的值:

val r = RegInit(resetData)
  1. RegNext

把A的信号延迟一个周期送给B:

val B = RegNext(A)

类型


数据类型

能够表示具体值的数据类型

  • UInt -> 构成任意位宽的wire或者reg : 1.U, “b0101”.U
  • SInt -> Chisel按照补码解读,使用系统函数$signed: -8.S
  • Bool -> 1bit的wire或者reg: true.B

数据宽度

默认数据宽度按字面值取最小,Bool类型固定一位宽。

  1.U(32.W)       //值为1,位宽为32的UInt数据对象
  "hff".U         // 对于其他进制,常量被定义为字符串,开头h为16进制,o是8进制,b是2互不照顾。
  o377".U
  "b1111_1111".U  // 如果用下划线表示群组数字,下划线是被忽略的
  // (ps. scala里面没有单引号,请用双引号,不然会报错。)
Bool类型
val true_value = true.B
val false_value = false.B

强制转换的方法:

true_value.asUInt   // 强制转换为UInt
num_value.asBool    // 强制转换为Bool
Vec类型
val myVec = Vec(<number of elements>, <data type>)

<number of elements>:向量长度

<data type>:数据类型

例如,创建一个UInt类型向量reg:

val reg_vec32 = Reg(Vec(32, UInt(2.W))) 

val regOfVec = RegInit(VecInit(Seq.fill(4)(0.U(1.W)))) // 创建初始化为0的4个reg

创建UInt类wire:

val t_out_bool = Wire(Vec(n, Bool()))

如果实例化的数据类型是module,则语法不太一样,使用了关键字new,还有.io方法,array。eg:

val FullAdders =
    Array.fill(n) (Module(new FullAdder()).io)

注意,如果是实例化多个module,引用module的port的时候就不需要.io了(如果是直接实例化一个是需要的)

如果是将输入重复多次这种事情,就用Fill函数:

import chisel3.util._

val control_n = Fill(n, io.control_in) // 将io.control_in复制n遍

模块


定义模块

Chisel使用class来定义模块:

// A n-bit adder with carry in and carry out
class Adder(n: Int) extends Module {
  val io = IO(new Bundle {
    val A    = Input(UInt(n.W))
    val B    = Input(UInt(n.W))
    val Cin  = Input(UInt(1.W))
    val Sum  = Output(UInt(n.W))
    val Cout = Output(UInt(1.W))
  })
  // create a vector of FullAdders

  val FAs = Array.fill(n) (Module(new FullAdder()).io)

  // define carry and sum wires
  val carry = Wire(Vec(n+1, UInt(1.W)))
  val sum   = Wire(Vec(n, Bool()))

  // first carry is the top level carry in
  carry(0) := io.Cin

  // wire up the ports of the full adders
  for(i <- 0 until n) {
    FAs(i).a   := io.A(i)
    FAs(i).b   := io.B(i)
    FAs(i).cin := carry(i)
    carry(i+1) := FAs(i).cout
    sum(i)     := FAs(i).sum.asBool()
  }
  io.Sum  := sum.asUInt
  io.Cout := carry(n)
}

class Adder(n: Int)代表了参数化方法。如果不是参数化方法的class可以直接去掉(n: Int)

实例化模块

如果是上面的参数化模块:

val adder4 = Module(new Adder(4))

or

val adder4 = Module(new Adder(n=4))

另外的一个例子:

// A 4-bit adder with carry in and carry out
class Adder4 extends Module {
  val io = IO(new Bundle {
    val A    = Input(UInt(4.W))
    val B    = Input(UInt(4.W))
    val Cin  = Input(UInt(1.W))
    val Sum  = Output(UInt(4.W))
    val Cout = Output(UInt(1.W))
  })
  // Adder for bit 0
  val Adder0 = Module(new FullAdder())
  Adder0.io.a   := io.A(0)
  Adder0.io.b   := io.B(0)
  Adder0.io.cin := io.Cin
  val s0 = Adder0.io.sum
  // Adder for bit 1
  val Adder1 = Module(new FullAdder())
  Adder1.io.a   := io.A(1)
  Adder1.io.b   := io.B(1)
  Adder1.io.cin := Adder0.io.cout
  val s1 = Cat(Adder1.io.sum, s0)
  // Adder for bit 2
  val Adder2 = Module(new FullAdder())
  Adder2.io.a   := io.A(2)
  Adder2.io.b   := io.B(2)
  Adder2.io.cin := Adder1.io.cout
  val s2 = Cat(Adder2.io.sum, s1)
  // Adder for bit 3
  val Adder3 = Module(new FullAdder())
  Adder3.io.a   := io.A(3)
  Adder3.io.b   := io.B(3)
  Adder3.io.cin := Adder2.io.cout
  io.Sum  := Cat(Adder3.io.sum, s2).asUInt
  io.Cout := Adder3.io.cout
}

Testbench

Chisel的test方法:定义一个测试类,这个类会接收一个参数,参数类型是待测模块类名。

这个测试类继承自PeekPokeTester类,把接收的待测模块类名也传递给此超类。

tb可以使用scala的各种高级语法比如if、dowhile、until等等。

一个例子:

import chisel3.iotesters.PeekPokeTester

class Max2Tests(c: Max2) extends PeekPokeTester(c) {
  for (i <- 0 until 10) {

    val in0 = rnd.nextInt(256)
    val in1 = rnd.nextInt(256)
    poke(c.io.in0, in0)
    poke(c.io.in1, in1)
    step(1)
    expect(c.io.out, if(in0>in1) in0 else in1)
  }
}

tb的几个部分:

  • 设置测试的输入:使用poke(端口,激励值)
  • 让仿真前进n个时钟周期:step(n)
  • 检查输出值:使用except(端口,期望值),或者使用peek(端口)查看期望值
  • 重复这一切直到一切都看起来很完美

生成verilog


在主函数里实例化待编译的模块(如下例中的Adder模块),需要使用如下方法:

object Max2Tests extends App {
  chisel3.iotesters.Driver.execute(args, () => new Max2()) {
    c => new Max2Tests(c)
  }
}

要运行这个主函数,在命令行中执行命令:

sbt 'test:runMain <模块名>'

例如:

sbt 'test:runMain max.Max2Tests'

gcd是在./src/main/scala/test下的max文件夹名,tb文件的文件名为Max2Tests。在scala文件最开始要加上package max

后面可以加的一些参数:

  • --backend-name verilator

    • sbt 'test:runMain max.Max2Tests --backend-name verilator'使用verilator backend,会输出波形文件
  • --is-verbose

    • 测试的时候的peeks和pokes实例会在命令行输出
  • -td <target dir>

    • 指定输出文件在哪里

关于testbench我实在找不到更多材料了,想要试图使用读入文件的方法,但是好像无论哪里都没有这样的例子 TvT 摸了

硬件原语


状态机

util包里包括Enum方法,eg:

import chisel3.util._

val sIdle :: sStart :: sWait :: sFinish :: Nil = Enum(4)  // 注意:枚举状态名的首字母要小写,这样Scala编译器才能识别成变量模式匹配
// Nil在Scala里面好像是扩展List[Nothing]的对象,空列表
val state = RegInit(sIdle) // state首先被赋值为空闲状态sIdle

可以用switch语句来作状态机使用:

switch (state) {
    is (sStart) {
        ...
    }
    is (sWait) {
        ...
    }
    is (sFinish) {
        ...
    }
}
选择器
val a = Mux(sel, in1, in2)

sel是Bool类型