目录
前言
本文是笔者学习过程中总结的笔记,会随着学习过程不断更新
欢迎点赞收藏,长期翻看~
变量命名法
camelCase(驼峰命名法)
函数和变量都是小写开头,而类(类型),比如模块名,就使用大写开头。
例如:cntReg
变量的定义与赋值
- scala的变量类型
- val 不可变变量==(chisel中使用)==
- var 可变变量(了解即可)
- 例如
val number = Wire(UInt())
- 如果val number = a | b ,则val的值不能再由=或:=来更改,只能由这个表达式中的a和b来更改
- 变量赋值
number := 10.U
- 注意区分定义的 =
三种信号类型
-
Bits
、UInt
和SInt
-
W用来定义位宽
-
有符号整数编码方式为二进制补码
常量
-
此处用10进制表示
-
同时可以定义宽度
-
.W不能省略,有另一层含义
-
可以不定义宽度,则编译器会自动帮你判断合适的宽度
-
但是推荐自己显式定义好宽度
-
-
用其他进制表示
-
用字符串的形式表现,第一位为进制
-
h为16进制,o为8进制,b为2进制
-
下划线 _ 用于分割,编译时会省略
-
逻辑值 Bool型
- 定义变量:
Bool()
- 定义常量
true.B
false.B
位运算符
操作符 | 描述 | 数据类型 |
---|---|---|
& | 按位与 | UInt 、SInt 、Bool |
| | 按位或 | UInt 、SInt 、Bool |
^ | 按位异或 | UInt 、SInt 、Bool |
~ | 按位取反 | UInt 、SInt 、Bool |
<< | 左移(左移都是低位补0) | UInt 、SInt |
>> | 对于UInt 是逻辑右移,对于SInt 是算术右移 | UInt 、SInt |
算数右移:右移后最高位(符号位)要保持不变
逻辑右移:右移后最高位直接补0
算术运算符
操作符 | 描述 | 数据类型 |
---|---|---|
+ 或+% | 加(不保留进位) | UInt 、SInt |
+& | 加(保留进位) | UInt 、SInt |
- 或-% | 减(不保留进位) | UInt 、SInt |
-& | 减(保留进位) | UInt 、SInt |
* | 乘 | UInt 、SInt |
/ | 除 | UInt 、SInt |
% | 取余 | UInt 、SInt |
- 位宽
- 对于加减法,结果宽度为操作数中最宽的那个宽度;
- 对于乘法,结果宽度为操作数的宽度之和;
- 对于除法和取余,结果宽度通常为被除数的宽度;
逻辑运算符
操作符 | 描述 | 数据类型 |
---|---|---|
&& | 逻辑与 | Bool |
| | | 逻辑或 | Bool |
! | 逻辑非 | Bool |
比较运算符
(等于、不等于与其他语言不同)
操作符 | 描述 | 数据类型 |
---|---|---|
> | 大于 | UInt 、SInt ,返回Bool |
>= | 大于等于 | UInt 、SInt ,返回Bool |
< | 小于 | UInt 、SInt ,返回Bool |
<= | 小于等于 | UInt 、SInt ,返回Bool |
=== | 等于 | UInt 、SInt ,返回Bool |
=/= | 不等于 | UInt 、SInt ,返回Bool |
规约运算符(逐位运算)
最高位与次高位运算,再与次次高位运算,直至算出最后一个结果为止
操作符 | 描述 | 数据类型 |
---|---|---|
.andR | 与规约 | UInt 、SInt ,返回Bool |
.orR | 或规约 | UInt 、SInt ,返回Bool |
.xorR | 异或规约 | UInt 、SInt ,返回Bool |
位字段操作符
操作符 | 描述 | 数据类型 |
---|---|---|
x(n) | 提取第n 位 | UInt 、SInt ,返回Bool |
x(end, start) | 提取第start 到第end 位 | UInt 、SInt ,返回UInt |
Fill(n, x) | 位向量x 复制n 次 | UInt ,返回UInt |
a ## b | 位向量拼接(不推荐使用) | UInt 、SInt ,返回UInt |
Cat(a, b, ...) | 位向量拼接 | UInt 、SInt ,返回UInt |
2-1多路选择器
val y = Mux(sel, a, b)
- sel为选择信号,为1时选择a,为0时选择b
硬件类型
寄存器型(Reg型)
- 定义寄存器
- 带初始值的寄存器
//0.U为寄存器的复位值,8.W为位宽
val reg = RegInit(0.U(8.W))
//reg为输入
reg:= inVal
-
带输入和初始值的寄存器
val bothReg = RegNext(d, 0.U)
(d为输入,0.U为初始值常数) -
什么都没有的寄存器
//只规定了存储的变量类型 和 位宽
val delayReg = Reg(UInt(4.W))
//delayIn为输入
delayReg := delayIn
- 带使能的寄存器
//inVal为输入,0.U为初始值
//4.W为数据位宽,enable为使能
val resetEnableReg2 = RegEnable(inVal, 0.U(4.W), enable)
- 寄存器组
val lotsOfRegs = RegInit(Vec(10, UInt(32.W)))
-
寄存器的输入
reg := d
-
寄存器的输出
val q = reg
-
寄存器的时钟
自动连接到全局时钟,上升沿触发 -
寄存器的复位
自动连接到全局reset,同步复位
线网型(Wire型)
- 定义线网型变量
val number = WireDefault(10.U(4.W))
(10.U为默认值,4.W为位宽)
或
val number = Wire(UInt())
(UInt为类型,但此处没有规定默认值,不推荐使用) - 线网的赋值
number := 10.U
输入输出型(IO型)
命名规范:”xxIO“ , ”xx“为名字,”IO“表示类型
//定义一个变量xxIO,用IO函数来定义
//其中的输入输出端口为Bundle定义的两个变量
val xxIO = IO(new Bundle {
val in_a = Input(UInt(8.W))
val out_b = Output(UInt(8.W))
})
规范:
可以将相关的IO用同一个Bundle,将IO口进行分类,
最后再由一个总的Bundle来包裹起来
Bundle类型(捆绑包)(面向对象)
- 定义
//定义一个类来定义一个bundle(也就是一组信号的集合)
//这个类拓展自`Bundle`类
class Channel() extends Bundle {
val data = UInt(32.W)
val valid = Bool()
}
- 使用
//定义一个wire型变量ch,且类型为Channel
val ch = Wire(new Channel())
//将ch中的对象data赋值为123.U
ch.data := 123.U
//将ch中的对象valid赋值为true.B
ch.valid := true.B
//获取ch中对象的值
val b = ch.valid
Vec类型 (向量)
- 定义
//定义一个wire变量,类型为向量
//该向量有三个元素,且三个元素的类型都为UInt,位宽都为4
val v = Wire(Vec(3, UInt(4.W)))
//将wire型向量的第0位赋值位1.U
v(0) := 1.U
//将wire型向量的第1位赋值位3.U
v(1) := 3.U
//将wire型向量的第2位赋值位5.U
v(2) := 5.U
//定义一个常数idx,值为1.U,位宽为2.W
val idx = 1.U(2.W)
//定义一个变量a,值为向量v的第idx位
val a = v(idx)
输入输出翻转
Flipped可以将Bundle中的Input、Output类型对调 翻转
//定义一个名为OutIO的Bundle
class OutIO extends Bundle {
val instr = Output(UInt(32.W))
val pc = Output(UInt(32.W))
}
//定义一个InIO
//其值为:
//class InIO extends Bundle {
// val instr = Input(UInt(32.W))
// val pc = Input(UInt(32.W))
//}
val InIO = Flipped(new OutIO())
整体连接 (Bulk Connection)<>
- 可以将bundle中同名的元素相连
- 符号两边的bundle中的元素,必须名字、数量相同
使用示例
//导入必须的包
import chisel3._
import chisel3.util._
//定义Fetch模块和Decode模块共有的IO口
//也就是Fetch将输出,而Decode将输入的口
class FetchDecodeIO extends Bundle {
val instr = Output(UInt(32.W))
val pc = Output(UInt(32.W))
}
//定义Fetch模块
class Fetch extends Module {
//定义io口
val io = IO(new Bundle {
//定义两个模块共有的部分
val fetchdecodeIO = new FetchDecodeIO()
val a = Input(UInt(32.W))
val b = Input(UInt(32.W))
})
// fetch阶段的实现省略
io.fetchdecodeIO.instr := io.a
io.fetchdecodeIO.pc := io.b
}
//定义Decode模块和Execute模块共有的IO口
class DecodeExecuteIO extends Bundle {
val aluOp = Output(UInt(5.W))
val regA = Output(UInt(32.W))
val regB = Output(UInt(32.W))
}
class Decode extends Module {
val io = IO(new Bundle {
//Flipped表示将Input和Output对调
//此处因为对于Fetch来说是Output
//对于Decode来说是Input,所以要对调以下
val fetchdecodeIO = Flipped(new FetchDecodeIO())
val decodeexecuteIO = new DecodeExecuteIO()
})
// decode阶段的实现省略
io.decodeexecuteIO.aluOp := io.fetchdecodeIO.instr(5, 0)
io.decodeexecuteIO.regA := io.fetchdecodeIO.instr
io.decodeexecuteIO.regB := io.fetchdecodeIO.pc
}
//定义Decode模块和Execute模块共有的IO口
class ExecuteTopIO extends Bundle {
val result1 = Output(UInt(32.W))
val result2 = Output(UInt(32.W))
}
class Execute extends Module {
val io = IO(new Bundle {
//此处将Decode传来的信号,定义为Input
val decodeexecuteIO = Flipped(new DecodeExecuteIO())
val executetopIO = new ExecuteTopIO()
})
// execute阶段的实现省略
io.executetopIO.result1 := io.decodeexecuteIO.regA
io.executetopIO.result2 := io.decodeexecuteIO.regB
}
class Top extends Module {
val io = IO(new Bundle {
val a = Input(UInt(32.W))
val b = Input(UInt(32.W))
//此处不反转是因为Excute是Top中的最后一个模块了
//它的输出就是Top的输出
//因此此处直接就是Output类型
val executetopIO = new ExecuteTopIO()
})
//实例化三个模块
val fetch = Module(new Fetch())
val decode = Module(new Decode())
val execute = Module(new Execute())
fetch.io.a := io.a
fetch.io.b := io.b
//用<>将共有的信号链接
fetch.io.fetchdecodeIO <> decode.io.fetchdecodeIO
decode.io.decodeexecuteIO <> execute.io.decodeexecuteIO
io.executetopIO <> execute.io.executetopIO
}
//创建函数入口
object MyModule extends App {
// emitVerilog(new Count10(), Array("--target-dir", "generated"))
println(getVerilogString(new Top()))
}
函数
在模块中定义
在模块中定义,只能在当前模块中使用
例如:
// 返回计数器的函数
def genCounter(n: Int) = {
val cntReg = RegInit(0.U(8.W))
cntReg := Mux(cntReg === n.U, 0.U, cntReg + 1.U)
//最后一行是函数的返回值,这里返回的就是`cntReg`
cntReg
}
// 可以直接用这个的函数创建各种不同上限的计数器
val counter10 = genCounter(n=10)
val counter99 = genCounter(n=99)
在object中定义
可以在其他地方使用
函数名为apply方法
这样定义的函数可以像chisel3内置的函数一样调用
package gencounter
import chisel3._
//object的名字就是调用的函数名
object genCounter {
// 返回计数器的函数
//函数名称为apply
def apply(n: Int) = {
val cntReg = RegInit(0.U(32.W))
cntReg := Mux(cntReg === n.U, 0.U, cntReg + 1.U)
//最后一行是函数的返回值,这里返回的就是`cntReg`
cntReg
}
}
使用
import gencounter.genCounter
//区别在这
val counterN = gencounter.genCounter(n=8)
函数名为自定义名称
这样调用需要一层一层解引用
package gencounter
import chisel3._
//object的名字就是调用的函数名
object genCounter {
// 返回计数器的函数
//函数名称为mydef
def mydef(n: Int) = {
val cntReg = RegInit(0.U(32.W))
cntReg := Mux(cntReg === n.U, 0.U, cntReg + 1.U)
//最后一行是函数的返回值,这里返回的就是`cntReg`
cntReg
}
}
使用
import gencounter.genCounter
//区别在这
val counterN = gencounter.genCounter.mydef(n=8)
BlackBox
作用
- 在chisel中例化用verilog写的模块
如何使用
//导入必须的包
import chisel3._
import chisel3.util._
//创建BlackBox类型
//Map 用来指定参数
class BUFGCE extends BlackBox(Map("SIM_DEVICE" -> "7SERIES")) {
//所有的信号都要自己创建,包括clk和rst
//而普通的Module模块则不需要创建clk和rst
val io = IO(new Bundle {
val I = Input(Clock())
val CE = Input(Bool())
val O = Output(Clock())
})
}
//在Top模块中例化BlackBox
class Top extends Module {
val io = IO(new Bundle {})
//BlackBox的例化也用Module
val bufgce = Module(new BUFGCE)
// 连接BUFGCE的时钟输入端口到顶层模块的时钟信号
bufgce.io.I := clock
}
生成如下verilog代码
module Top(
input clock,
input reset
);
wire bufgce_I; // @[hello.scala 18:24]
wire bufgce_CE; // @[hello.scala 18:24]
wire bufgce_O; // @[hello.scala 18:24]
BUFGCE #(.SIM_DEVICE("7SERIES")) bufgce ( // @[hello.scala 18:24]
.I(bufgce_I),
.CE(bufgce_CE),
.O(bufgce_O)
);
assign bufgce_I = clock; // @[hello.scala 20:17]
assign bufgce_CE = 1'h0;
endmodule
可以不告诉chisel这个verilog文件在哪,毕竟到时候使用的是Verilog文件来仿真,则verilog们自己会找到自己的同类
注意事项
- BlackBox需要自行创建clk和rst,模块不会隐式帮你创建
BlackBox
类是不能直接测试的,必须要封装到测试代码中的一个类中
//创建一个正常的模块,内部就是BlackBox
class InlineAdder extends Module {
//输入输出端口和BlackBox的一样
val io = IO(new BlackBoxAdderIO)
//在内部例化BlackBox
val adder = Module(new InlineBlackBoxAdder)
//将BlackBox的端口与外部正常模块的端口相连
io <> adder.io
}
示意图:
switch-is 语句 (无优先级)
作用:
- 用于构造编码器、译码器
//定义一个支持四种算法的alu
class ALU extends Module {
val io = IO(new Bundle {
val a = Input(UInt(16.W))
val b = Input(UInt(16.W))
val fn = Input(UInt(2.W))
val y = Output(UInt(16.W))
})
// ALU的默认输出值
io.y := 0.U
// 选择ALU的功能
switch(io.fn) {
is(0.U) {io.y := io.a + io.b}
is(1.U) {io.y := io.a - io.b}
is(2.U) {io.y := io.a | io.b}
is(3.U) {io.y := io.a & io.b}
}
}
无优先级的多路选择器(复用器)
- 通常使用case语句(verilog)实现,所有分支处于同一优先级(并行),综合之后会得到一个多路选择器。
有优先级的多路选择器(复用器)
- 通常使用if-else语句(verilog)或者条件赋值语句(?:)实现,分支之间具有优先级(串行),可以得到类似级联的结构,由if语句综合之后采用的元器件多于case语句运用的元器件。而且if语句由于是串行级联的结构,所造成的延时往往比case语句大,所以对于多路选择器而言,一般选择case语句会比if语句时序更好一些。
when/elsewhen/otherwise 语句(有优先级)
- 使用思想:
- 要知道这个语句的结果是生成多路选择器
- 与软件中的if-else不同
//使用when语句的变量必须为Wire类型
//赋初始值(默认值)为0.U
val w = WireDefault(0.U)
when (cond) {
w := 1.U
} .elsewhen (cond2) {
w := 2.U
} .otherwise {
w := 3.U
}
注意事项:
-
when综合出的电路有优先级
由图可知,第一个when的优先级比后面的elsewhen优先级高
计数器的实现
- 利用加法器和寄存器的组合,一起其他器件
Tips:用函数写一个计数器的函数,可以生成多种情况的计数器
基础计数器
//构造寄存器
val cntReg = RegInit(0.U(4.W))
//如果到达指定的值就重置,否则就+1
when(cntReg === N) {
cntReg := 0.U
}
.otherwise {
cntReg := cntReg + 1.U
}
事件计数器(有条件的计数器)
//构造寄存器
val cntEventsReg = RegInit(0.U(4.W))
//当event===1时,才+1
//含义:记录event为1的次数
when(event) {
cntEventsReg := cntEventsReg + 1.U
}
//如果到了指定值就重置
.elsewhen(cntEventsReg === N) {
cntReg := 0.U
}
从上到下计数
正常我们需要比较计数器和指定值是否相等,则需要比较每一位,但如果我们从N-2计数到-1,即除了-1,其他全都非负,那么只需要比较最高位是否为复数的符号位“1”即可,就可以优化性能
val cntReg = RegInit(N(8.W))
//当减到0时 就重置为N-2
when(cntReg(7)===1)
{
cntReg := N-2
}
.otherwise
{
cntReg := cntReg - 1.U
}
用计数器生成新的时序
//计数器:记录上升沿的次数
val tickCounterReg = RegInit(0.U(32.W))
//计数器重置的条件:记录了指定数量的上升沿
val tick = tickCounterReg === (N-1).U
//计数器如果符合条件就重置,否则就+1
tickCounterReg := Mux(tick, 0.U, tickCounterReg + 1.U)
//寄存器:用来充当新的时序
val lowFrequCntReg = RegInit(0.U(4.W))
//每隔N个周期tick就会为1,然后下个周期重置为0
when (tick) {
//每N个时钟周期就+1
lowFrequCntReg := lowFrequCntReg + 1.U
}
定时器
//定义定时器
val cntReg = RegInit(0.U(8.W))
//定时器结束计时的flag
val done = cntReg === 0.U
//定时器的输入值
next = WireDefault(0.U)
cntReg := next
//注意when有优先级
//如果有load信号,就将din加载进去,从下个周期开始定时
when(load) {
next := din
}
//如果没有完成计时,就-1
.elsewhen (!done) {
next := cntReg - 1.U
}
//其他情况下默认为0,不开始计时
.otherwise {
next := 0.U
}
移位寄存器
以下有对 串行、并行 的 输入、输出 模式的解释
//定义寄存器
val shiftReg = Reg(UInt(4.W))
//选择串行输入还是并行输入
switch(func)
{
//串行输入din,为即将移入寄存器的值
is(0.U)
{
//cat将原寄存器的(2,0)和输入拼接为新的寄存器值
shiftReg := Cat(shiftReg(2, 0), din)
}
//并行输入d,即对所有寄存器赋值
is(1.U)
{
//允许加载信号时就加载信号
when(load) {
loadReg := d
}
//否则就移位,补零
.otherwise {
loadReg := Cat(0.U, loadReg(3, 1))
}
}
}
//串行输出,输出被移除的那个寄存器值
val dout = shiftReg(3)
//并行输出,输出所有寄存器的值
val q = shiftReg
内存
吃透Chisel语言.24.Chisel时序电路(四)——Chisel内存(Memory)详解_chisel syncreadmem-优快云博客
目前只需要用BlackBox并用DPI-C来实现
Ready-Valid 握手接口
Ready-Valid接口是一种简单的控制流接口,包含:
data
:发送端向接收端发送的数据;valid
:发送端到接收端的信号,用于指示发送的数据是否有效;ready
:接收端到发送端的信号,用于指示是否可以接收数据;
//此包包含了DecoupledIO
import chisel3.util._
//chisel种内置的接口
val out = DecoupledIO(UInt(8.W))
//Flipped反转Input和Output
val in = Flipped(DecoupledIO(UInt(8.W)))
//DecoupledIO大概长这样
class DecoupledIO[T <: Data](gen: T) extends Bundle {
val ready = Input(Bool())
val valid = Output(Bool())
val bits = Output(gen)
}
硬件生成器
必须的scala语法
Scala语句和Chisel语句不同
- 使用Scala语句就像写程序一样,一条一条运行,是硬件生成器
- 使用Chisel语句就是在描绘硬件,直接生成硬件,所有代码都是一起综合成verilog文件
变量类型
- val
赋值后不能重新赋值
val zero = 0
//下面这一句会报错
zero = 1
- var
赋值后可重新赋值
var x = 2
//不会报错
x = 3
for循环
//定义8位位宽的移位寄存器
val shiftReg = RegInit(0.U(8.W))
//将0号寄存器与输入相连
shiftReg(0) := inVal
//将0连1,1连2,.。。直到6连7
//i从1,一直循环到7
for (i <- 1 until 8) {
shiftReg(i) := shiftReg(i-1)
}
if-else条件判断
for (i <- 0 until 10) {
//Chisel中的等于为“===”
if (i%2 == 0) {
println(i + " is even")
} else {
println(i + " is odd")
}
}
参数化
可变位宽
//定义位宽为参数n,类型为Int
class ParamAdder(n: Int) extends Module {
val io = IO(new Bundle {
//在此处位宽用n来替代
val a = Input(UInt(n.W))
val b = Input(UInt(n.W))
val c = Output(UInt(n.W))
})
io.c := io.a + io.b
}
//将8作为参数传入,8会替代n
//显式定义参数
val add8 = Module(new ParamAdder(n=8))
val add16 = Module(new ParamAdder(n=16))
可变参数类型
//定义一个函数,可变参数类型
def myMux[T <: Data](sel: Bool, tPath: T, fPath: T): T = {
val ret = WireDefault(fPath)
//或者创建没有默认值的Wire
//.cloneType可以提取fPath的类型
//val ret = Wire(fPath.cloneType)
when (sel) {
ret := tPath
}
ret
}
//显式定义参数
//使用函数构造Mux,5.U为tPath,10.U为fPath
val resA = myMux(sel=selA, tPath=5.U, fPath=10.U)
//错误用法,tPath和fPath的类型应该一致
val resErr = myMux(sel=selA, tPath=5.U, fPath=10.S)
[T <: Data]
定义了类型参数T
的集合是Data
或Data
的子集Data
是Chisel类型系统的根类型myMux
函数有三个参数,一个布尔值的条件,一个用于true路径的值,一个用于false路径的值。两个路径的值的类型都是T
,T
会在调用函数的时候给定。- 定义一个线网默认值为
fPath
,如果条件为真的话就把值改为tPath
。这种情况是经典的Mux函数,函数的结尾我们返回了Mux的硬件。
如果参数类型为Bundle
//定义一个Bundle
class ComplexIO extends Bundle {
val d = UInt(10.W)
val b = Bool()
}
//用Wire类型来创建Bundle
val tVal = Wire(new ComplexIO)
tVal.b := true.B
tVal.d := 42.U
val fVal = Wire(new ComplexIO)
fVal.b := false.B
fVal.d := 13.U
// 在Mux中使用Bundle类型
val resB = myMux(selB, tVal, fVal)
对Bundle参数化
//创建一个参数化的Bundle
//private val就是在这个Bundle种创建一个私有的变量
//有点像c语言里的函数变量
//如果不加private val,则dt只是形参
class Port[T <: Data](private val dt: T) extends Bundle {
val address = UInt(8.W)
//如果dt是形参,则不能这样写
val data = dt.cloneType
}
//创建一个普通的Bundle
class Payload extends Bundle {
val data = UInt(16.W)
val flag = Bool()
}
//创建一个参数化的模块
class NocRouter2[T <: Data](dt: T, n: Int) extends Module {
val io = IO(new Bundle {
val inPort = Input(Vec(n, dt))
val outPort = Output(Vec(n, dt))
})
}
//new Port(new Payload)表示Port中的data是Bundle型的Payload,所以这是一个嵌套的Bundle
//而这个嵌套的Bundle又作为NocRouter2的参数,成为里面的数据dt
//最后用Module函数来实例化了这个模块
val router = Module(NocRouter2(new Port(new Payload), 2))