[TOC]
环境安装
mac osx 10.15.6
1.brew install sbt
安装Chisel3
-
brew install verilator
安装verilator 好像是用来将Chisel转换为verilog以及输出仿真波形的工具 -
brew cask install gtkwave
安装用于查看仿真波形的gtkwave -
sudo cpan install Switch
不知道为啥总之命令行打开gtkwave它要求我装Switch -
现在可以在terminal直接打开gtkwave:
/Applications/gtkwave.app/Contents/Resources/bin/gtkwave
-
在
.bash_profile
中加入假名:alias gtkwave = /Applications/gtkwave.app/Contents/Resources/bin/gtkwave
,命令行输入gtkwave即可打开应用 -
直接用
gtkwave -f <文件名>
打开生成的.vcd格式文件,可以看到左侧有模块层次,点击module,把左下的port加入,就可以看到波形了 -
因为有一些testbench需要用到scala特性,装上scala:
brew install scala
。命令行输入scala
可以进解释器,scala <file name>
可以跑scala脚本。
Tutorial
Chisel tutorial wiki:使用上面的tutorial对chisel有个基础了解
sbt:sbt的tutorial
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是下一个周期的输出本周期的输入。
- Reg
val r = Reg(type, next, init)
最基本的类型,type指明种类,next指明数据输入端(这个值被延迟一拍输送到r端),init指明初始化值。
如果都没有生命,则默认为null。
- RegInit
使用resetData指明该reg初始化时的值:
val r = RegInit(resetData)
- 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类型