Chisel 语法总结(持续更新)

前言

本文是笔者学习过程中总结的笔记,会随着学习过程不断更新
欢迎点赞收藏,长期翻看~

变量命名法

camelCase(驼峰命名法)
函数和变量都是小写开头,而类(类型),比如模块名,就使用大写开头。
例如:cntReg

变量的定义与赋值

  • scala的变量类型
    • val 不可变变量==(chisel中使用)==
    • var 可变变量(了解即可)
    • 例如val number = Wire(UInt())
    • 如果val number = a | b ,则val的值不能再由=或:=来更改,只能由这个表达式中的a和b来更改
  • 变量赋值
    • number := 10.U
    • 注意区分定义的 =

三种信号类型

  • BitsUIntSInt

  • W用来定义位宽![[Pasted image 20240925105135.png]]

  • 有符号整数编码方式为二进制补码

常量

  • 此处用10进制表示![[Pasted image 20240925105317.png]]

  • 同时可以定义宽度![[Pasted image 20240925105339.png]]

    • .W不能省略,有另一层含义![[Pasted image 20240925105656.png]]

    • 可以不定义宽度,则编译器会自动帮你判断合适的宽度

    • 但是推荐自己显式定义好宽度

  • 用其他进制表示

    • 用字符串的形式表现,第一位为进制![[Pasted image 20240925110150.png]]

    • h为16进制,o为8进制,b为2进制

    • 下划线 _ 用于分割,编译时会省略

逻辑值 Bool型

  • 定义变量:
Bool()
  • 定义常量
true.B
false.B

位运算符

操作符描述数据类型
&按位与UIntSIntBool
|按位或UIntSIntBool
^按位异或UIntSIntBool
~按位取反UIntSIntBool
<<左移(左移都是低位补0)UIntSInt
>>对于UInt是逻辑右移,对于SInt是算术右移UIntSInt

算数右移:右移后最高位(符号位)要保持不变
逻辑右移:右移后最高位直接补0

算术运算符

操作符描述数据类型
++%加(不保留进位)UIntSInt
+&加(保留进位)UIntSInt
--%减(不保留进位)UIntSInt
-&减(保留进位)UIntSInt
*UIntSInt
/UIntSInt
%取余UIntSInt
  • 位宽
  1. 对于加减法,结果宽度为操作数中最宽的那个宽度;
  2. 对于乘法,结果宽度为操作数的宽度之和;
  3. 对于除法和取余,结果宽度通常为被除数的宽度;

逻辑运算符

操作符描述数据类型
&&逻辑与Bool
| |逻辑或Bool
!逻辑非Bool

比较运算符

(等于、不等于与其他语言不同)

操作符描述数据类型
>大于UIntSInt,返回Bool
>=大于等于UIntSInt,返回Bool
<小于UIntSInt,返回Bool
<=小于等于UIntSInt,返回Bool
===等于UIntSInt,返回Bool
=/=不等于UIntSInt,返回Bool

规约运算符(逐位运算)

最高位与次高位运算,再与次次高位运算,直至算出最后一个结果为止

操作符描述数据类型
.andR与规约UIntSInt,返回Bool
.orR或规约UIntSInt,返回Bool
.xorR异或规约UIntSInt,返回Bool

位字段操作符

操作符描述数据类型
x(n)提取第nUIntSInt,返回Bool
x(end, start)提取第start到第endUIntSInt,返回UInt
Fill(n, x)位向量x复制nUInt,返回UInt
a ## b位向量拼接(不推荐使用)UIntSInt,返回UInt
Cat(a, b, ...)位向量拼接UIntSInt,返回UInt

2-1多路选择器

  • val y = Mux(sel, a, b)
  • sel为选择信号,为1时选择a,为0时选择b

硬件类型

寄存器型(Reg型)

  • 定义寄存器
  1. 带初始值的寄存器
//0.U为寄存器的复位值,8.W为位宽
val reg = RegInit(0.U(8.W))

//reg为输入
reg:= inVal
  1. 带输入和初始值的寄存器
    val bothReg = RegNext(d, 0.U)
    (d为输入,0.U为初始值常数)

  2. 什么都没有的寄存器

//只规定了存储的变量类型 和 位宽
val delayReg = Reg(UInt(4.W)) 

//delayIn为输入
delayReg := delayIn
  1. 带使能的寄存器
//inVal为输入,0.U为初始值
//4.W为数据位宽,enable为使能
val resetEnableReg2 = RegEnable(inVal, 0.U(4.W), enable)

  1. 寄存器组
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
}

示意图:
![[Pasted image 20241005122734.png]]

switch-is 语句 (无优先级)

作用:

  • 用于构造编码器、译码器![[Pasted image 20241005205354.png]]![[Pasted image 20241005205401.png]]在这里插入图片描述
//定义一个支持四种算法的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综合出的电路有优先级![[Pasted image 20241005200504.png]]

    由图可知,第一个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接口是一种简单的控制流接口,包含:

  1. data:发送端向接收端发送的数据;
  2. valid:发送端到接收端的信号,用于指示发送的数据是否有效;
  3. ready:接收端到发送端的信号,用于指示是否可以接收数据;
    ![[Pasted image 20241009110042.png]]
//此包包含了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的集合是DataData的子集
  • Data是Chisel类型系统的根类型
  • myMux函数有三个参数,一个布尔值的条件,一个用于true路径的值,一个用于false路径的值。两个路径的值的类型都是TT会在调用函数的时候给定。
  • 定义一个线网默认值为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))


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值