Chisel

Chisel 学习资料:

  • chisel-cheatsheet3.pdf
    • chisel基本语法、数据类型的讲解和示例,阅读之后可以熟悉基础的语法知识点。chisel变量命名严格按照面向对象语言设计:类名使用驼峰命名法、变量和函数使用小写字母和下划线方式命名。

    • 不同点是,chisel使用:=进行物理连线的描述,用=来进行变量的定义(和verilog的=、<=表示方式不同),目前没有看到区分组合逻辑部分和时序逻辑部分的表示。

    • 有一些奇怪的语法,不知道有什么特殊的含义没有(cheat sheet上没有明示),例如下面这几个加法,减法同理也有。

      Chisel Explanation Width
      x + y Addition max(w(x),w(y))
      x +% y Addition max(w(x),w(y))
      x +& y Addition max(w(x),w(y))+1
    • 同时chisel里面也定义了一些较为高级的函数、接口(例如,带有握手信号的IO、仲裁器等)

  • Digital Design with Chisel
    • 这本是丹麦大学Martin Schoeberl教授撰写的Cshisel入门书籍
    • 目前有中文版(第几版不太清楚),且目录有点小问题,可以先看中文版,过了一遍之后,再去读英文原版(第四版)Digital Design with Chisel
    • 话不多说直接开看

Chisel book

chisel book是一本简明扼要的书,书中的代码都在github仓库里面有源码,开始不知道,跑不通手敲的demo,可能就是只看到了源码的一部分导致的。源码仓库:schoeberl/chisel-examples: Chisel examples and code snippets

明天记得写一下gitignore,今天有点晚了。

上午还没有写gitignore,把10月总结和11月计划倒是先写了,然后装了sbt,又遇到了上次的那个bug。

参考了一下官方的教程去安装sbt Reference Manual — Installing sbt on Linux,但是在运行这一行命令的时候还是会出现报错:

1
2
3
4
curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823" | sudo -H gpg --no-default-keyring --keyring gnupg-ring:/etc/apt/trusted.gpg.d/scalasbt-release.gpg --import

gpg: no valid OpenPGP data found.
gpg: Total number processed: 0

直接打开这个网址是可以看到有public key,但是加不进去,头大。

最后解决方案:手动复制下来放在一个临时的文件里面再运行后面的那句话就可以了。

1
2
3
4
5
6
7
cat scalasbt-release.gpg | sudo -H gpg --no-default-keyring --keyring gnupg-ring:/etc/apt/trusted.gpg.d/scalasbt-release.gpg --import

gpg: directory '/root/.gnupg' created
gpg: /root/.gnupg/trustdb.gpg: trustdb created
gpg: key 99E82A75642AC823: public key "sbt build tool <scalasbt@gmail.com>" imported
gpg: Total number processed: 1
gpg: imported: 1

上面这个当前路径下的gpg就是自己创建的那个文件,把网站上的内容复制下来。最后成功运行,安装好了sbt。

Ch2 Basic Component

Data Type and Contant

Chisel 中的有符号数是用补码表示,数据表示的时候,通过用(n.W)的方式规定数据的位宽为n。如下实例:

1
2
3
4
5
6
7
8
// variable 
val b_data = Bits(3.W) // binary, datawidth is 3 bits
val s_data = SInt(2.W) // signed integer, datawidth is 2 bits
val u_data = UInt(2.W) // unsigned integer, datawidth is 2 bits

// constant
3.U(3.W) // constant unsigned int 3, with datawidth of 3
-3.S(3.W) // constant signed int -3, with datawidth of 3

注意:规定位宽的时候,一定要带上.W,否则会被判断为是抓取操作。例如:

1
val a = 1.U(32) // 结果a会是0,因为对这个常量来说,第32位是0(没有规定位宽时,**高位会补零**)

数据默认时十进制,但是可以规定需要需要的进制,例如:o、h、b

1
"b1111_1011".U

Chisel Bool类型的定义:

1
2
3
val a = Bool()
a = true.B
b = false.B

Component

Update operator

Chisel 同样规定了线网类型的变量,以及用来赋值的update operator (:=)。

1
2
val w = Wire(UInt()) 
w := a & b

Mux

Chisel 自带了多路选择器模块,调用方法:

1
val r = Mux(sel, a, b)

Reg

Chisel的寄存器有两种定义方法,一种是不带输入端口的,另一种是带的:

1
2
3
4
5
6
7
8
// first case:
val reg = RegInit(0.U(8.W))
reg := d
val q = reg

// second case:
val reg = RegNext(d, 0.U(8.W))
val q = reg

Counter

计数器可以用Reg + Mux的方式实现:

1
2
val cntReg = RegInit(0.U(8.W)) 
cntReg := Mux(cntReg === 9.U, 0.U, cntReg + 1.U)

Bundle

Bundle 是一个类似于c语言中的结构体的数据类型,可以在其中自定义不同类型的数据。

自定义Bundle需要通过使用类继承的方式进行,下面是一个实例:

1
2
3
4
class Channel() extends Bundle{
val sig = UInt(8.W)
val ena = Bool()
}

需要创建一个变量时:

1
2
3
4
5
6
val ch = Wire(new Channel())
ch.sig := 123.U
ch.ena := true.B

val mySig = ch.sig
val channel = ch

这里有一个小问题就是定义Channel的时候加()和不加有什么区别么
后面会有一个是没有加括号的情况

Vec

Wire Vec

Vec是一个数组变量类型

组合逻辑的数组类型需要打包在Wire中(例如,当定义模块的接口的时候),下面是一个示例:

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

上面这个示例说的是,创建一个有3个8位无符号整形变量的数组(用于组合逻辑)。下面是赋值的实例,可以使用常量,也可以使用变量。

1
2
3
4
5
6
v(0) = 1.U
v(1) = 2.U
v(2) = 5.U

val index = 2.U(2.W)
val a = vec(index)

接下来通过数组实现一个多路选择器:

1
2
3
4
5
6
7
val v = Wire(Vec(3, UInt(8.W)))

v(0) := x
v(1) := y
v(2) := z

val muxOut = v(select)

还可以使用VecInit的方式进行初始化,VecInit继承了WireDefault,因此不必再将其定义到Wire中(这不是推荐的CodingStyle)。下面代码中的when结构对Vec的值进行了重写,其生成了3个二选一多路选择器件(注意理解)。

1
2
3
4
5
6
7
8
9
10
val vecDef = VecInit(1.U(3.W), 2.U, 3.U)

when (cond) {
vecDef(0) = 3.U
vecDef(1) = 4.U
vecDef(2) = 5.U
}

val vecOut = vecDef(sel)

同时,也可以通过VecInit直接线路的连接:

1
2
val vecDef = VecInit(x, y, z)
val vecOut = vecDef(sel)
Reg Vec

寄存器也可以定义为数组(注意,在Chisel中,Reg就是寄存器,和verilog中不同)。

实现上面这个电路:

1
2
3
4
val regVec = Reg(Vec(3, UInt(8.W)))

regVec(wrIdx) := din // 注意这里是:=,不要用成=号了。
val dout = regVec(reIdx)

带有初始化的Reg定义:

1
2
3
4
5
6
7
8
9
val initVal = RegInit(VecInit(0.U(3.W), 1, 2))  // 第一个规定位宽时,默认将后面两个数据的位宽也规定为3
val selVal = initVal(sel)
initVal(0) := a
initVal(1) := b
initVal(2) := c

// 使用初始化将所有值赋值为同一个初始值的方式
val resetRegFile = RegInit(VecInit(Seq.fill(32)(0.U(32.W))))
val rdReg = resetRegFile(sel)

Conbine Vec and Bundle

将Vec放在Bundle中

这个很好理解,就是在结构体中加入了一个数组:

1
2
3
4
class BundleVec extends Bundle{
val field = UInt(8.W)
val vector = Vec(4, UInt(8.W))
}

将Bundle组成Vec

这个也好理解,就是一个结构体数组:

1
val vecBundle = Wire(8, new BundleVec)

给结构体赋初值

这个需要看一下,先定义为Wire型,赋值完成后,再用RegInit进行定义。

1
2
3
4
val initVal = Wire(new Channel()) 
initVal.data := 0.U
initVal.valid := false.B
val channelReg = RegInit(initVal)

部分赋值的问题

在Verilog中,定义了一个向量之后是可以对其任意的位进行赋值,但是在Chisel中不可以,只能够全部进行赋值,下面这种操作会引发报错:

1
2
3
val assignWord = Wire(UInt(16.W)) 
assignWord(7, 0) := lowByte
assignWord(15, 8) := highByte

书中给出的解决方案就是使用Bundle或者是Bool类型的Vec来解决:

1
2
3
4
5
6
7
8
9
val assignWord = Wire(UInt(16.W)) 
class Split extends Bundle {
val high = UInt(8.W)
val low = UInt(8.W)
}
val split = Wire(new Split())
split.low := lowByte
split.high := highByte
assignWord := split.asUInt()

从这个例子中,也解决了前面的一个疑惑(见Bundle部分),加不加()好像是没有什么影响的。

IO, Reg and Wire

上述定义UInt、SInt、Bits等类型的时候并不会生成相应的电路,只有当包裹进Wire、Reg和IO的时候,才会有实际的物理意义

=, :=的区别在于,=用于定义中,而:=用于赋值一个已经存在的硬件模块。

Wire的带有初始值的定义:

1
val wire = WireDefault(0.U(8.W))

Reg带有初始值的定义:

1
val reg = RegInit(0.U(8.W))

Practice

写一个点灯小程序作为练习。

点灯需要一个输出IO引脚,以及模块中默认含有的reset和clk信号。逻辑很简单,就是使用计数器实现定时功能,当计数器溢出的时候,产生IO翻转信号即可。

chisel程序的时候需要借用模板,解决环境配置的问题(实验开始前,需要安装scala、sbt环境,直接sudo apt insall scala sbt即可,中途可能会出现卡住的情况,查找后发现可能是cdn的问题,耐心等待即可),安装完成后开始实验。

第一步是从官方仓库中clone chisel模板:

1
git clone https://github.com/freechipsproject/chisel-template

更改其中的src文件夹下内容,逻辑代码写在main文件夹下即可,自己创建的src/hello.scala

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package test
import chisel3._
import chisel3.experimental._


class Hello extends Module {
val io = IO(new Bundle {
val led = Output(UInt(1.W))
})
val CNT_MAX = (500_000_000 / 2 - 1).U

val blkReg = RegInit(0.U(1.W))
val cntReg = RegInit(0.U(32.W))

cntReg := cntReg + 1.U
when (cntReg === CNT_MAX ) {
cntReg := 0.U
blkReg := ~blkReg
}
io.led := blkReg
}

src/test/helloTest内容为:

1
2
3
4
5
6
7
package test

import chisel3.stage.ChiselGeneratorAnnotation

object testMain extends App {
(new chisel3.stage.ChiselStage).execute(Array("--target-dir", "generated/and"), Seq(ChiselGeneratorAnnotation(() => new Hello)))
}

完成上述代码编写之后,回到仓库的根目录下,运行:

1
sbt "test:runMain test.testMain"

可看见生成的generate文件夹中有生成的可综合的verilog代码,完成逻辑设计。转入vivado中,综合、
实现、烧录代码,验证结果。

目前遇到的一些问题:

  • 按照chisel默认的设置,生成的verilog代码的reset的信号是高有效,于是手动改了一下v代码,后面学习后再看看应该如何设置。
  • chisel中的:=更新操作符是如何适用于不同的时钟域的,例如:计数器1和2的时钟关系是倍频关系如何处理(除了判断计数值外),现在还没有找到相应的办法。(:=是否是每个时钟更新?如何跨时钟域?)