# 2023.11.07-Verilog语法
+ 2024.02.21-Chisel + + → +
diff --git a/404.html b/404.html new file mode 100644 index 0000000..9cba9aa --- /dev/null +++ b/404.html @@ -0,0 +1,20 @@ + + +
+ + +Verilog 是区分大小写的。
格式自由,可以在一行内编写,也可跨多行编写。
每个语句必须以分号为结束符。空白符(换行、制表、空格)都没有实际的意义,在编译阶段可忽略。例如下面两中编程方式都是等效的。
**wire** [1:0] results ;**assign** results = (a == 1'b0) ? 2'b01 : (b==1'b0) ? 2'b10 : 2'b11 ;
+
**wire** [1:0] results ;
+**assign** results = (a == 1'b0) ? 2'b01 :(b==1'b0) ? 2'b10 :2'b11 ;
+
Verilog 中有 2 种注释方式:
用 //
进行单行注释:
reg [3:0] counter ; // A definition of counter register
+
用**/* */
**进行跨行注释:
wire [11:0] addr ;
+/*
+Next are notes with multiple lines.
+Codes here cannot be compiled.
+*/
+assign addr = 12'b0 ;
+
标识符(identifier)可以是任意一组字母、数字、$ 符号和 _(下划线)符号的合,但标识符的第一个字符必须是字母或者下划线,不能以数字或者美元符开始。
另外,标识符是区分大小写的。
关键字是 Verilog 中预留的用于定义语言结构的特殊标识符。
Verilog 中关键字全部为小写。
**reg** [3:0] counter ; *//reg 为关键字, counter 为标识符*
+**input** clk; *//input 为关键字,clk 为标识符*
+**input** CLK; *//CLK 与 clk是 2 个不同的标识符*
+
+ 10. Verilog 多路分支语句 + + → +
case 语句是一种多路条件分支的形式,可以解决 if 语句中有多个条件选项时使用不方便的问题。
case 语句格式如下:
case(case_expr)
+ condition1 : true_statement1 ;
+ condition2 : true_statement2 ;
+ ……
+ default : default_statement ;
+endcase
+
case 语句执行时,如果 condition1 为真,则执行 true_statement1 ; 如果 condition1 为假,condition2 为真,则执行 true_statement2;依次类推。如果各个 condition 都不为真,则执行 default_statement 语句。
default 语句是可选的,且在一个 case 语句中不能有多个 default 语句。
条件选项可以有多个,不仅限于 condition1、condition2 等,而且这些条件选项不要求互斥。虽然这些条件选项是并发比较的,但执行效果是谁在前且条件为真谁被执行。
ture_statement1 等执行语句可以是一条语句,也可以是多条。如果是多条执行语句,则需要用 begin 与 end 关键字进行说明。
下面用 case 语句代替 if 语句实现了一个 4 路选择器的功能。仿真结果与 testbench 可参考条件语句 (opens new window)一章,两者完全一致。
**module** mux4to1(
+ **input** [1:0] sel ,
+ **input** [1:0] p0 ,
+ **input** [1:0] p1 ,
+ **input** [1:0] p2 ,
+ **input** [1:0] p3 ,
+ **output** [1:0] sout);
+
+ **reg** [1:0] sout_t ;
+ **always** @(*)
+ **case**(sel)
+ 2'b00: **begin**
+ sout_t = p0 ;
+ **end**
+ 2'b01: sout_t = p1 ;
+ 2'b10: sout_t = p2 ;
+ **default**: sout_t = p3 ;
+ **endcase**
+ **assign** sout = sout_t ;
+ **endmodule**
+
case 语句中的条件选项表单式不必都是常量,也可以是 x 值或 z 值。
当多个条件选项下需要执行相同的语句时,多个条件选项可以用逗号分开,放在同一个语句块的候选项中。
**case**(sel)
+ 2'b00: sout_t = p0 ;
+ 2'b01: sout_t = p1 ;
+ 2'b10: sout_t = p2 ;
+ 2'b11: sout_t = p3 ;
+ 2'bx0, 2'bx1, 2'bxz, 2'bxx, 2'b0x, 2'b1x, 2'bzx :
+ sout_t = 2'bxx ;
+ 2'bz0, 2'bz1, 2'bzz, 2'b0z, 2'b1z :
+ sout_t = 2'bzz ;
+ **default**: $display("Unexpected input control!!!");**endcase**
+
casex
/casez
语句casex、 casez 语句是 case 语句的变形,用来表示条件选项中的无关项。
casex 用 "x" 来表示无关值,casez 用问号 "?" 来表示无关值。
两者的实现的功能是完全一致的,语法与 case 语句也完全一致。
**module** mux4to1(
+ **input** [3:0] sel ,
+ **input** [1:0] p0 ,
+ **input** [1:0] p1 ,
+ **input** [1:0] p2 ,
+ **input** [1:0] p3 ,
+ **output** [1:0] sout);
+
+ **reg** [1:0] sout_t ;
+ **always** @(*)
+ **casez**(sel)
+ 4'b???1: sout_t = p0 ;
+ 4'b??1?: sout_t = p1 ;
+ 4'b?1??: sout_t = p2 ;
+ 4'b1???: sout_t = p3 ;
+ **default**: sout_t = 2'b0 ;
+ **endcase**
+ **assign** sout = sout_t ;
+ **endmodule**
+
+ ← + + 1. Verilog 基础语法 + + 2. Verilog 数值表示 + + → +
Verilog HDL 有下列四种基本的值来表示硬件电路中的电平逻辑:
x
意味着信号数值的不确定,即在实际电路里,信号可能为 1,也可能为 0。
z
意味着信号处于高阻状态,常见于信号(input, reg)没有驱动时的逻辑结果。例如一个 pad 的 input 呈现高阻状态时,其逻辑值和上下拉的状态有关系。上拉则逻辑值为 1,下拉则为 0 。
数字声明时,合法的基数格式有 4 中,包括:十进制('d 或 'D),十六进制('h 或 'H),二进制('b 或 'B),八进制('o 或 'O)。数值可指明位宽,也可不指明位宽。
4'b1011 *// 4bit 数值*
+32'h3022_c0de *// 32bit 的数值 一个16进制的数需要4bit来表示*
+
其中,下划线 _
是为了增强代码的可读性。
一般直接写数字时,默认为十进制表示,例如下面的 3 种写法是等效的:
counter = 'd100; *//一般会根据编译器自动分频位宽,常见的为32bit*
+counter = 100;
+counter = 32'h64;
+
通常在表示位宽的数字前面加一个减号来表示负数。例如:
-6'd15
+-15
+
需要注意的是,减号放在基数和数字之间是非法的,例如下面的表示方法是错误的:
4'd-2 //非法说明
+
实数表示方法主要有两种方式:
30.123
+6.0
+3.0
+0.001
+
1.2e4 //大小为12000
+1_0001e4 //大小为100010000
+1E-3 //大小为0.001
+
字符串是由双引号包起来的字符队列。字符串不能多行书写,即字符串中不能包含回车符。Verilog 将字符串当做一系列的单字节 ASCII 字符队列。例如,为存储字符串 "www.runoob.com", 需要 14*8bit 的存储单元。例如:
**reg** [0: 14*8-1] str ;
+**initial** **begin**
+ str = "www.runoob.com";
+**end**
+
+ ← + + 10. Verilog 多路分支语句 + + 2023.11.15-Vivado + + → +
在 Vivado 或类似的 FPGA 开发环境中,您会遇到 RTL (Register Transfer Level) 分析中的 Schematic 和 Synthesis 过程中的 Schematic。它们虽然看起来相似,但代表了不同阶段的电路设计和功能。
RTL Analysis Schematic:
Synthesis Schematic:
总结来说,RTL Schematic 关注于逻辑设计的表示,而 Synthesis Schematic 则关注于该设计在 FPGA 上的物理实现。理解两者的区别对于 FPGA 开发至关重要,因为这影响着您如何优化设计以及解决可能出现的问题。
在 Vivado 或类似的 FPGA 设计项目中,您会遇到几个关键的资源指标,如 LUT、FF、IO 和 BUFG。这些指标是 FPGA 设计和资源利用的重要部分。下面是每个术语的简要解释:
理解这些资源对于 FPGA 设计至关重要,因为它们直接影响您的设计能否在特定的 FPGA 上实现,以及该设计的性能和效率。在 Vivado 的项目汇总中,这些参数帮助您评估设计对 FPGA 资源的利用情况。
FPGA (Field-Programmable Gate Array) 是一种高度灵活的可编程硬件,它由多个不同的元件构成,共同实现复杂的数字逻辑功能。以下是 FPGA 内部主要组成部分的概述:
FPGA 的关键优势在于其高度可编程性和灵活性。设计师可以通过编程定义 LUTs、MUXes、Flip-Flops 以及互连的配置和组合方式,从而在没有物理改动硬件的情况下实现几乎任何数字逻辑设计。这种灵活性使得 FPGA 在快速原型制作、定制硬件加速和可重配置系统中非常有价值。
FPGA (Field-Programmable Gate Array) 是一种高度灵活的可编程硬件,它由多个不同的元件构成,共同实现复杂的数字逻辑功能。以下是 FPGA 内部主要组成部分的概述:
FPGA 的关键优势在于其高度可编程性和灵活性。设计师可以通过编程定义 LUTs、MUXes、Flip-Flops 以及互连的配置和组合方式,从而在没有物理改动硬件的情况下实现几乎任何数字逻辑设计。这种灵活性使得 FPGA 在快速原型制作、定制硬件加速和可重配置系统中非常有价值。
+ ← + + 2. Verilog 数值表示 + + 2023.11.16-阻塞与非阻塞赋值 + + → +
组合逻辑电路使用阻塞赋值
时序逻辑电路使用非阻塞赋值
在 Verilog 中,阻塞赋值(Blocking Assignment)和非阻塞赋值(Non-Blocking Assignment)是两种不同的信号赋值方法,它们在时序逻辑的模拟和硬件描述语言(HDL)代码的编写中有着重要的区别。
=
进行赋值。举例:
a = b;
+c = a;
+
在这个例子中,c = a;
会等待 a = b;
完成后才执行。
<=
进行赋值。举例:
a <= b;
+c <= a;
+
在这个例子中,c <= a;
不会等待 a <= b;
的完成。a
和 c
的赋值看似同时发生。
在一个always块中,不同地方对同一个变量的赋值会被同时执行,最终结果是变量变成最后一次赋值的结果(前面的操作被覆盖)
+ ← + + 2023.11.15-Vivado + + 3. Verilog 数据类型 + + → +
Verilog 最常用的 2 种数据类型就是线网(wire)与寄存器(reg),其余类型可以理解为这两种数据类型的扩展或辅助。
wire 类型表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动。如果没有驱动元件连接到 wire 型变量,缺省值一般为 "Z"。举例如下:
**wire** interrupt;
+**wire** flag1, flag2;
+**wire** gnd = 1'b0;
+
线网型还有其他数据类型,包括 wand,wor,wri,triand,trior,trireg
等。这些数据类型用的频率不是很高,这里不做介绍。
寄存器(reg)用来表示存储单元,它会保持数据原有的值,直到被改写。声明举例如下:
**reg** clk_temp;
+**reg** flag1, flag2;
+
例如在 always 块中,寄存器可能被综合成边沿触发器,在组合逻辑中可能被综合成 wire 型变量。寄存器不需要驱动源,也不一定需要时钟信号。在仿真时,寄存器的值可在任意时刻通过赋值操作进行改写。例如:
**reg** rstn ;
+**initial** **begin**
+ rstn = 1'b0;
+ #100;
+ rstn = 1'b1;
+**end**
+
当位宽大于 1 时,wire 或 reg 即可声明为向量的形式。例如:
**reg** [3:0] counter ; *//声明4bit位宽的寄存器counter*
+**wire** [32-1:0] gpio_data; *//声明32bit位宽的线型变量gpio_data*
+**wire** [8:2] addr ; *//声明7bit位宽的线型变量addr,位宽范围为8:2*
+**reg** [0:31] data ; *//声明32bit位宽的寄存器变量data, 最高有效位为0*
+
对于上面的向量,我们可以指定某一位或若干相邻位,作为其他逻辑使用。例如:
**wire** [9:0] data_low = data[0:9] ;addr_temp[3:2] = addr[8:7] + 1'b1 ;
+
Verilog 支持可变的向量域选择,例如:
**reg** [31:0] data1 ;
+**reg** [7:0] byte1 [3:0];
+**integer** j ;
+**always**@* **begin**
+ **for** (j=0; j<=3;j=j+1) **begin**
+ byte1[j] = data1[(j+1)*8-1 : j*8];
+ *//把data1[7:0]…data1[31:24]依次赋值给byte1[0][7:0]…byte[3][7:0]*
+ **end
+end**
+
Verilog 还支持指定 bit 位后固定位宽的向量域选择访问。
*//下面 2 种赋值是等效的*
+A = data1[31-: 8] ;
+A = data1[31:24] ;
+*//下面 2 种赋值是等效的*
+B = data1[0+ : 8] ;
+B = data1[0:7] ;
+
对信号重新进行组合成新的向量时,需要借助大括号。例如:
**wire** [31:0] temp1, temp2 ;
+**assign** temp1 = {byte1[0][7:0], data1[31:8]}; *//数据拼接*
+**assign** temp2 = {32{1'b0}}; *//赋值32位的数值0*
+
整数,实数,时间等数据类型实际也属于寄存器类型。
整数类型用关键字 integer 来声明。声明时不用指明位宽,位宽和编译器有关,一般为32 bit。reg 型变量为无符号数,而 integer 型变量为有符号数。例如:
**reg** [31:0] data1 ;
+**reg** [3:0] byte1 [7:0]; *//数组变量,后续介绍*
+**integer** j ; *//整型变量,用来辅助生成数字电路*
+**always**@* **begin**
+ **for** (j=0; j<=3;j=j+1) **begin**
+ byte1[j] = data1[(j+1)*8-1 : j*8];
+ *//把data1[7:0]…data1[31:24]依次赋值给byte1[0][7:0]…byte[3][7:0]*
+ **end
+end**
+
此例中,integer 信号 j 作为辅助信号,将 data1 的数据依次赋值给数组 byte1。综合后实际电路里并没有 j 这个信号,j 只是辅助生成相应的硬件电路。
实数用关键字 real 来声明,可用十进制或科学计数法来表示。实数声明不能带有范围,默认值为 0。如果将一个实数赋值给一个整数,则只有实数的整数部分会赋值给整数。例如:
**real** data1 ;
+**integer** temp ;
+**initial** **begin**
+ data1 = 2e3 ;
+ data1 = 3.75 ;
+**end**
+
+**initial** **begin**
+ temp = data1 ; *//temp 值的大小为3*
+**end**
+
Verilog 使用特殊的时间寄存器 time 型变量,对仿真时间进行保存。其宽度一般为 64 bit,通过调用系统函数 $time 获取当前仿真时间。例如:
**time** current_time ;
+**initial** **begin**
+ #100 ;
+ current_time = $time ; *//current_time 的大小为 100*
+**end**
+
在 Verilog 中允许声明 reg, wire, integer, time, real
及其向量类型的数组。
数组维数没有限制。线网数组也可以用于连接实例模块的端口。数组中的每个元素都可以作为一个标量或者向量,以同样的方式来使用,形如:数组名[下标]。对于多维数组来讲,用户需要说明其每一维的索引。例如:
**integer** flag [7:0] ; *//8个整数组成的数组*
+**reg** [3:0] counter [3:0] ; *//由4个4bit计数器组成的数组*
+**wire** [7:0] addr_bus [3:0] ; *//由4个8bit wire型变量组成的数组*
+**wire** data_bit[7:0][5:0] ; *//声明1bit wire型变量的二维数组*
+**reg** [31:0] data_4d[11:0][3:0][3:0][255:0] ; *//声明4维的32bit数据变量数组*
+
下面显示了对数组元素的赋值操作:
flag [1] = 32'd0 ; *//将flag数组中第二个元素赋值为32bit的0值*
+counter[3] = 4'hF ; *//将数组counter中第4个元素的值赋值为4bit 十六进制数F,等效于counter[3][3:0] = 4'hF,即可省略宽度;*
+**assign** addr_bus[0] = 8'b0 ; *//将数组addr_bus中第一个元素的值赋值为0*
+**assign** data_bit[0][1] = 1'b1; *//将数组data_bit的第1行第2列的元素赋值为1,这里不能省略第二个访问标号,即 assign data_bit[0] = 1'b1; 是非法的。*
+data_4d[0][0][0][0][15:0] = 15'd3 ; *//将数组data_4d中标号为[0][0][0][0]的寄存器单元的15~0bit赋值为3*
+
虽然数组与向量的访问方式在一定程度上类似,但不要将向量和数组混淆。向量是一个单独的元件,位宽为 n;数组由多个元件组成,其中每个元件的位宽为 n 或 1。它们在结构的定义上就有所区别。
存储器变量就是一种寄存器数组,可用来描述 RAM 或 ROM 的行为。例如:
**reg** membit[0:255] ; *//256bit的1bit存储器*
+**reg** [7:0] mem[0:1023] ; *//1Kbyte存储器,位宽8bit*
+mem[511] = 8'b0 ; *//令第512个8bit的存储单元值为0*
+
参数用来表示常量,用关键字 parameter 声明,只能赋值一次。例如:
**parameter** data_width = 10'd32 ;
+**parameter** i=1, j=2, k=3 ;
+**parameter** mem_size = data_width * 10 ;
+
但是,通过实例化的方式,可以更改参数在模块中的值。此部分以后会介绍。
局部参数用 localparam 来声明,其作用和用法与 parameter 相同,区别在于它的值不能被改变。所以当参数只在本模块中调用时,可用 localparam 来说明。
字符串保存在 reg 类型的变量中,每个字符占用一个字节(8bit)。因此寄存器变量的宽度应该足够大,以保证不会溢出。
字符串不能多行书写,即字符串中不能包含回车符。如果寄存器变量的宽度大于字符串的大小,则使用 0 来填充左边的空余位;如果寄存器变量的宽度小于字符串大小,则会截去字符串左边多余的数据。例如,为存储字符串 "run.runoob.com", 需要 14*8bit 的存储单元:
**reg** [0: 14*8-1] str ;
+**initial** **begin**
+ str = "run.runoob.com";
+**end**
+
有一些特殊字符在显示字符串中有特殊意义,例如换行符,制表符等。如果需要在字符串中显示这些特殊的字符,则需要在前面加前缀转义字符 ** 。例如下表所示:
转义字符 | 显示字符 |
---|---|
\n | 换行 |
\t | 制表符 |
%% | % |
\ | \ |
" | " |
\ooo | 1到3个8进制数字字符 |
其实,在 SystemVerilog(主要用于 Verilog 仿真的编程语言)语言中,已经可以直接用关键字 string 来表示字符串变量类型,这为 Verilog 的仿真带来了极大的便利。有兴趣的学者可以简单学习下 SystemVerilog。
+ ← + + 2023.11.16-阻塞与非阻塞赋值 + + 4. Verilog 表达式 + + → +
表达式由操作符和操作数构成,其目的是根据操作符的意义得到一个计算结果。表达式可以在出现数值的任何地方使用。例如:
a^b ; *//a与b进行异或操作*
+address[9:0] + 10'b1 ; *//地址累加*
+flag1 && flag2 ; *//逻辑与操作*
+
操作数可以是任意的数据类型,只是某些特定的语法结构要求使用特定类型的操作数。
操作数可以为常数,整数,实数,线网,寄存器,时间,位选,域选,存储器及函数调用等。
**module** test;
+*//实数*
+**real** a, b, c;
+c = a + b ;
+*//寄存器*
+**reg** [3:0] cprmu_1, cprmu_2 ;
+**always** @(**posedge** clk) **begin**
+ cprmu_2 = cprmu_1 ^ cprmu_2 ;
+**end**
+
+*//函数*
+**reg** flag1 ;
+flag = calculate_result(A, B);
+ *//非法操作数*
+**reg** [3:0] res;
+**wire** [3:0] temp;
+**always**@ (*)**begin**
+ res = cprmu_2 – cprmu_1 ;
+ *//temp = cprmu_2 – cprmu_1 ; //不合法,always块里赋值对象不能是wire型*
+**end
+endmodule**
+
Verilog 中提供了大约 9 种操作符,分别是算术、关系、等价、逻辑、按位、归约、移位、拼接、条件操作符。
大部分操作符与 C 语言中类似。同类型操作符之间,除条件操作符从右往左关联,其余操作符都是自左向右关联。圆括号内表达式优先执行。例如下面每组的 2 种写法都是等价的。
//自右向左关联,两种写法等价
+A+B-C ;
+(A+B)-C ;
+
+//自右向左关联,两种写法等价,结果为 B、D 或 F
+A ? B : C ? D : F ;
+A ? B : (C ? D : F) ;
+
+//自右向左关联,两种写法不等价
+(A ? B : C) ? D : F ; //结果 D 或 F
+A ? B : C ? D : F ; //结果为 B、D 或 F
+
不同操作符之间,优先级是不同的。下表列出了操作符优先级从高至低的排列顺序。当没有圆括号时,Verilog 会根据操作符优先级对表达式进行计算。为了避免由操作符优先级导致的计算混乱,在不确定优先级时,建议用圆括号将表达式区分开来。
操作符 | 操作符号 | 优先级 |
---|---|---|
单目运算 | + - ! ~ | 最高 |
乘、除、取模 | * / % | |
加减 | + - | |
移位 | << >> | |
关系 | < <= > >= | |
等价 | == != === !=== | |
归约 | & ~& | |
^ ~^ | ||
~ | ||
逻辑 | && | |
条件 | ?: | 最低 |
算术操作符包括单目操作符和双目操作符。
双目操作符对 2 个操作数进行算术运算,包括乘(*)、除(/)、加(+)、减(-)、求幂(**)、取模(%)。
**reg** [3:0] a, b;**reg** [4:0] c ;
+a = 4'b0010 ;
+b = 4'b1001 ;
+c = a+b; *//结果为c=b'b1011*
+c = a/b; *//结果为c=4,取整*
+
如果操作数某一位为 X,则计算结果也会全部出现 X。例如:
b = 4'b100x ;
+c = a+b ; *//结果为c=4'bxxxx*
+
对变量进行声明时,要根据变量的操作符对变量的位宽进行合理声明,不要让结果溢出。上述例子中,相加的 2 个变量位宽为 4bit,那么结果寄存器变量位宽最少为 5bit。否则,高位将被截断,导致结果高位丢失。无符号数乘法时,结果变量位宽应该为 2 个操作数位宽之和。
**reg** [3:0] mula ;**reg** [1:0] mulb;**reg** [5:0] res ;
+mula = 4'he ;
+mulb = 2'h3 ;
+res = mula * mulb ; *//结果为res=6'h2a, 数据结果没有丢失位数*
+
-4 //表示负4
++3 //表示正3
+
负数表示时,可以直接在十进制数字前面增加一个减号 -,也可以指定位宽。因为负数使用二进制补码来表示,不指定位宽来表示负数,编译器在转换时,会自动分配位宽,从而导致意想不到的结果。例如:
mula = -4'd4 ;
+mulb = 2 ;
+res = mula * mulb ; *//计算结果为res=-6'd8, 即res=6'h38,正常*
+res = mula * (-'d4) ; *//(4的32次幂-4) * 2, 结果异常*
+
关系操作符有大于(>),小于(<),大于等于(>=),小于等于(<=)。
关系操作符的正常结果有 2 种,真(1)或假(0)。
如果操作数中有一位为 x 或 z,则关系表达式的结果为 x。
A = 4 ;
+B = 3 ;
+X = 3'b1xx ;
+
+A > B *//为真*
+A <= B *//为假*
+A >= Z *//为X,不确定*
+
等价操作符包括逻辑相等(==),逻辑不等(!=),全等(===),非全等(!==)。
等价操作符的正常结果有 2 种:为真(1)或假(0)。
逻辑相等/不等操作符不能比较 x 或 z,当操作数包含一个 x 或 z,则结果为不确定值。
全等比较时,如果按位比较有相同的 x 或 z,返回结果也可以为 1,即全等比较可比较 x 或 z。所以,全等比较的结果一定不包含 x。举例如下:
A = 4 ;
+B = 8'h04 ;
+C = 4'bxxxx ;
+D = 4'hx ;
+A == B *//为真*
+A == (B + 1) *//为假*
+A == C *//为X,不确定*
+A === C *//为假,返回值为0*
+C === D *//为真,返回值为1*
+
逻辑操作符主要有 3 个:&&(逻辑与), ||(逻辑或),!(逻辑非)。
逻辑操作符的计算结果是一个 1 bit 的值,0 表示假,1 表示真,x 表示不确定。
如果一个操作数不为 0,它等价于逻辑 1;如果一个操作数等于 0,它等价于逻辑 0。如果它任意一位为 x 或 z,它等价于 x。
如果任意一个操作数包含 x,逻辑操作符运算结果不一定为 x。
逻辑操作符的操作数可以为变量,也可以为表达式。例如:
A = 3;
+B = 0;
+C = 2'b1x ;
+
+A && B *// 为假*
+A || B *// 为真*! A *// 为假*! B *// 为真*
+A && C *// 为X,不确定*
+A || C *// 为真,因为A为真*(A==2) && (! B) *//为真,此时第一个操作数为表达式*
+
按位操作符包括:取反(~),与(&),或(|),异或(^),同或(~^)。
按位操作符对 2 个操作数的每 1 bit 数据进行按位操作。
如果 2 个操作数位宽不相等,则用 0 向左扩展补充较短的操作数。
取反操作符只有一个操作数,它对操作数的每 1 bit 数据进行取反操作。
下图给出了按位操作符的逻辑规则。
| &(与) | 0 | 1 | x | | |(或) | 0 | 1 | x | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 0 | 0 | 0 | 0 | | 0 | 0 | 1 | x | +| 1 | 0 | 1 | x | | 1 | 1 | 1 | 1 | +| x | 0 | x | x | | x | x | 1 | x |
^(异或) | 0 | 1 | x | ~^(同或) | 0 | 1 | x | |
---|---|---|---|---|---|---|---|---|
0 | 0 | 1 | x | 0 | 1 | 0 | x | |
1 | 1 | 0 | x | 1 | 0 | 1 | x | |
x | x | x | x | x | x | x | x |
A = 4'b0101 ;
+B = 4'b1001 ;
+C = 4'bx010 ;
+~A *//4'b1010*
+A & B *//4'b0001*
+A | B *//4'b1101*
+A^B *//4'b1100*
+A ~^ B *//4'b0011*
+B | C *//4'b1011*
+B&C *//4'bx000*
+
归约操作符包括:归约与(&),归约与非(~&),归约或(|),归约或非(~|),归约异或(^),归约同或(~^)。
归约操作符只有一个操作数,它对这个向量操作数逐位进行操作,最终产生一个 1bit 结果。
逻辑操作符、按位操作符和归约操作符都使用相同的符号表示,因此有时候容易混淆。区分这些操作符的关键是分清操作数的数目,和计算结果的规则。
A = 4'b1010 ;
+&A ; //结果为 1 & 0 & 1 & 0 = 1'b0,可用来判断变量A是否全1
+~|A ; //结果为 ~(1 | 0 | 1 | 0) = 1'b0, 可用来判断变量A是否为全0
+^A ; //结果为 1 ^ 0 ^ 1 ^ 0 = 1'b0
+
移位操作符包括左移(<<),右移(>>),算术左移(<<<),算术右移(>>>)。
移位操作符是双目操作符,两个操作数分别表示要进行移位的向量信号(操作符左侧)与移动的位数(操作符右侧)。
算术左移和逻辑左移时,右边低位会补 0。
逻辑右移时,左边高位会补 0;而算术右移时,左边高位会补充符号位,以保证数据缩小后值的正确性。
A = 4'b1100 ;
+B = 4'b0010 ;
+A = A >> 2 ; *//结果为 4'b0011*
+A = A << 1; *//结果为 4'b1000*
+A = A <<< 1 ; *//结果为 4'b1000*
+C = B + (A>>>2); *//结果为 2 + (-4/4) = 1, 4'b0001*
+
拼接操作符用大括号 {,} 来表示,用于将多个操作数(向量)拼接成新的操作数(向量),信号间用逗号隔开。
拼接符操作数必须指定位宽,常数的话也需要指定位宽。例如:
A = 4'b1010 ;
+B = 1'b1 ;
+Y1 = {B, A[3:2], A[0], 4'h3 }; *//结果为Y1='b1100_0011*
+Y2 = {4{B}, 3'd4}; *//结果为 Y2=7'b111_1100*
+Y3 = {32{1'b0}}; *//结果为 Y3=32h0,常用作寄存器初始化时匹配位宽的赋初值*
+
条件表达式有 3 个操作符,结构描述如下:
condition_expression ? true_expression : false_expression
+
计算时,如果 condition_expression 为真(逻辑值为 1),则运算结果为 true_expression;如果 condition_expression 为假(逻辑值为 0),则计算结果为 false_expression。
assign hsel = (addr[9:8] == 2'b0) ? hsel_p1 : hsel_p2 ;
+//当信号 addr 高 2bit 为 0 时,hsel 赋值为 hsel_p1; 否则,将 hsel_p2 赋值给 hsel。
+
其实,条件表达式类似于 2 路(或多路)选择器,其描述方式完全可以用 if-else 语句代替。
当然条件操作符也能进行嵌套,完成一个多次选择的逻辑。例如:
**assign** hsel = (addr[9:8] == 2'b00) ? hsel_p1 :
+ (addr[9:8] == 2'b01) ? hsel_p2 :
+ (addr[9:8] == 2'b10) ? hsel_p3 :
+ (addr[9:8] == 2'b11) ? hsel_p4 ;
+
+ ← + + 3. Verilog 数据类型 +
2024.02.21-1. Introduction to Scala
2024.02.23-2.1 First Chisel Module
2024.02.24-2.2 Combinational Logic
2024.02.26-2.4 Sequential Logic
2024.02.29-2.6 More on ChiselTest
2024.03.01-3.1 Generators: Parameters
2024.03.03-3.2 Generators: Collections
+ ← + + 2023.11.07-Verilog语法 + + 一生一芯计划 + + → +
var
and val
var numberOfKittens = 6
+val kittensPerHouse = 101
+val alphabet = "abcdefghijklmnopqrstuvwxyz"
+var done = false
+
+/*
+numberOfKittens: Int = 6
+kittensPerHouse: Int = 101
+alphabet: String = "abcdefghijklmnopqrstuvwxyz"
+done: Boolean = false
+*/
+
变量使用var
声明,常量则使用val
if (done) {
+ println("we are done")
+}
+else if (numberOfKittens < kittensPerHouse) {
+ println("more kittens!")
+ numberOfKittens += 1
+}
+else {
+ done = true
+}
+
类似于C++,如果只有一行,可以省略大括号
val likelyCharactersSet = if (alphabet.length == 26)
+ "english"
+else
+ "not english"
+
+println(likelyCharactersSet)
+// likelyCharactersSet: String = "english"
+
if
这一串的返回值又所选择的分支的最后一行确定
// Simple scaling function with an input argument, e.g., times2(3) returns 6
+// Curly braces can be omitted for short one-line functions.
+def times2(x: Int): Int = 2 * x
+
+// More complicated function
+def distance(x: Int, y: Int, returnPositive: Boolean): Int = {
+ val xy = x * y
+ if (returnPositive) xy.abs else -xy.abs
+}
+
// Overloaded function
+def times2(x: Int): Int = 2 * x
+def times2(x: String): Int = 2 * x.toInt
+
+times2(5) // 10
+times2("7") // 14
+
def asciiTriangle(rows: Int) {
+
+ // This is cute: multiplying "X" makes a string with many copies of "X"
+ // Unit means no return value
+ def printRow(columns: Int): Unit = println("X" * columns)
+
+ if(rows > 0) {
+ printRow(rows)
+ asciiTriangle(rows - 1) // Here is the recursive call
+ }
+}
+
+// printRow(1) // This would not work, since we're calling printRow outside its scope
+asciiTriangle(6)
+
+// Output:
+XXXXXX
+XXXXX
+XXXX
+XXX
+XX
+X
+
Consider the following method definition.
def myMethod(count: Int, wrap: Boolean, wrapValue: Int= 24): Unit= { ... }
+
When calling the method, you will often see the parameter names along with the passed-in values.
myMethod(count= 10, wrap= false, wrapValue= 23)
+
Using named parameters, you can even call the function with a different ordering.
myMethod(wrapValue= 23, wrap= false, count= 10)
+
For frequently called methods, the parameter ordering may be obvious. But for less common methods and, in particular, boolean arguments, including the names with calls can make your code a lot more readable. If methods have a long list of arguments of the same type, using names also decreases the chance of error. Parameters to class definitions also use this named argument scheme (they are actually just the parameters to the constructor method for the class).
When certain parameters have default values (that don't need to be overridden), callers only have to pass (by name) specific arguments that do not use defaults. Notice that the parameter wrapValue
has a default value of 24. Therefore, this will work as if 24 had been passed in.
myMethod(wrap= false, count= 10)
+
val x = 7
+val y = 14
+val list1 = List(1, 2, 3)
+val list2 = x :: y :: y :: Nil // An alternate notation for assembling a list
+ // Nil就是一个空的List[Nothing],即一个可以封装任何类型元素但又没有元素的容器
+
+val list3 = list1 ++ list2 // Appends the second list to the first list
+val m = list2.length
+val s = list2.size
+
+val headOfList = list1.head // Gets the first element of the list
+val restOfList = list1.tail // Get a new list with first element removed
+
+val third = list1(2) // Gets the third element of a list (0-indexed)
+
+// Output:
+x: Int = 7
+y: Int = 14
+list1: List[Int] = List(1, 2, 3)
+list2: List[Int] = List(7, 14, 14)
+list3: List[Int] = List(1, 2, 3, 7, 14, 14)
+m: Int = 3
+s: Int = 3
+headOfList: Int = 1
+restOfList: List[Int] = List(2, 3)
+third: Int = 3
+
for
Statementfor (i <- 0 to 7) { print(i + " ") } // include 7
+println()
+
+i <- 0 until 7 // exclude 7
+i <- 0 to 10 by 2
+
print
用于连续输出不换行,而 println
用于输出后换行。
<-
是用在for循环构造中的一个操作符,表示从一个集合中逐一取出元素
package
用于定义一个命名空间,它可以包含类、对象和特质(traits),以及其他包。包主要用于组织和管理代码,防止命名冲突,并提供访问控制。class
是定义数据结构及其行为的蓝图。它可以包含数据成员(属性)和方法。类用于实例化对象,每个对象都可以拥有不同的属性值。**import
语句可以用来引入包、类、对象,甚至是特定的方法或属性。这意味着你可以使用import
**来引用几乎任何你需要的代码实体。以下是一些例子:
import scala.collection.mutable._
,这里的**_
相当于Java中的``,表示引入mutable
**包下的所有成员。import scala.collection.mutable.ListBuffer
,这表示只引入**ListBuffer
**类。import scala.collection.mutable.{ArrayBuffer, LinkedList}
,这表示同时引入**ArrayBuffer
和LinkedList
**两个类。import java.lang.System.{out => stdout}
,这里还演示了将**System.out
重命名为stdout
**,以便在代码中使用简化的名称。// 从chisel3.iotesters包中引入特定的几个类:ChiselFlatSpec、Driver、和PeekPokeTester
+import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
+
// WrapCounter counts up to a max value based on a bit size
+class WrapCounter(counterBits: Int) {
+
+ val max: Long = (1 << counterBits) - 1
+ var counter = 0L
+
+ def inc(): Long = {
+ counter = counter + 1
+ if (counter > max) {
+ counter = 0
+ }
+ counter
+ }
+ println(s"counter created with max value $max")
+}
+
class WrapCounter
-- This is the definition of WrapCounter.
(counterBits: Int)
-- Creating a WrapCounter requires an integer parameter, nicely named to suggest it is the bit width of the counter.
Braces ({}) delimit a block of code. Most classes use a code block to define variables, constants, and methods (functions).
val max: Long =
-- the class contains a member variable max, declared as type Long
and initialized as the class is created.
(1 << counterBits) - 1
computes the maximum value that can be contained in counterBits bits. Since max was created with val
it cannot be changed.
A variable counter is created and initialized to 0L. The L says that 0 is a long value; thus, counter is inferred to be Long.
max and counter are commonly called member variables of the class.
A class method inc is defined which takes no arguments and returns a Long value.
The body of the method inc is a code block that has:
counter = counter + 1
increments counter.if (counter > max) { counter = 0 }
tests if the counter is greater than the max value and sets it back to zero if it is.counter
-- The last line of the code block is important.
+if
then else
statement defines its true and false clauses with code blocks, it can return a value i.e., val result = if (10 * 10 > 90) "greater" else "lesser"
would create a val
with the value "greater".println(s"counter created with max value $max")
prints a string to standard output. Because the println is directly in the defining code block, it is part of the class initialization code and is run, i.e. prints out the string, every time an instance of this class is created.
The string printed in this case is an interpolated string.
println(s"doubled max is ${max + max}")
.${...}
.val x = new WrapCounter(2)
+x.inc() // Increments the counter
+
+// Member variables of the instance x are visible to the outside, unless they are declared private
+if(x.counter == x.max) {
+ println("counter is about to wrap")
+}
+
+x inc() // Scala allows the dots to be omitted; this can be useful for making embedded DSL's look more natural
+
有时候实例化类时不需要使用**new
关键字,这通常是因为该类定义了一个或多个apply
方法。apply
方法可以在类的伴生对象(companion object)中定义,允许你直接通过类名加括号的方式创建类的实例,而不需要显式地使用new
关键字。这种方式提供了一种更简洁的语法来创建对象,同时也可以在apply
**方法内部执行初始化操作或参数处理,为对象创建提供更多的灵活性和控制。
例如,假设有一个名为**Person
的类及其伴生对象,伴生对象中定义了apply
**方法:
class Person(val name: String)
+
+object Person {
+ def apply(name: String): Person = new Person(name)
+}
+
在这个例子中,可以通过调用**Person
伴生对象的apply
方法来创建Person
类的实例,而不需要使用new
**关键字:
val person = Person("Alice") // 相当于调用 Person.apply("Alice")
+
这里,**Person("Alice")
实际上调用的是伴生对象中的apply
方法,该方法内部使用new
关键字创建了Person
**类的实例。这种方式使得代码看起来更简洁,同时保留了通过构造函数创建对象的灵活性。
Code blocks are delimited by braces. A block can contain zero or more lines of Scala code. The last line of Scala code becomes the return value (which may be ignored) of the code block. A code block with no lines would return a special null-like object called Unit
. Code blocks are used throughout Scala: they are the bodies of class definitions, they form function and method definitions, they are the clauses of if
statements, and they are the bodies of for
and many other Scala operators.
Code blocks can take parameters. In the case of class and method definitions, these parameters look like those in most conventional programming languages. In the example below, c
and s
are parameters of the code blocks.
// A one-line code block doesn't need to be enclosed in {}
+def add1(c: Int): Int = c + 1
+
+class RepeatString(s: String) {
+ val repeatedString = s + s
+}
+
IMPORTANT: There is another way in which code blocks may be parameterized. Here is an example.
val intList = List(1, 2, 3)
+val stringList = intList.map { i =>
+ i.toString
+}
+
使用**map
方法对intList
中的每个元素应用一个函数,该函数将整数转换为其对应的字符串表示。具体来说,map
方法遍历intList
中的每个元素(用i
表示),并对每个元素执行i.toString
**操作,将其转换为字符串。This type of code block is called an anonymous function, and more details on anonymous functions are provided in a later module.
val path = System.getProperty("user.dir") + "/source/load-ivy.sc"
+interp.load.module(ammonite.ops.Path(java.nio.file.FileSystems.getDefault().getPath(path)))
+import chisel3._
+import chisel3.util._
+import chisel3.tester._
+import chisel3.tester.RawTester.test
+import dotvisualizer._
+
这两句代码在使用Chisel(一种硬件描述语言)时,涉及到Ammonite脚本的动态加载。第一句定义了一个**path
变量,它通过获取系统属性"user.dir"
(当前用户目录)并附加上"/source/load-ivy.sc"
路径,用于指定一个Scala脚本文件的位置。第二句使用Ammonite的interp.load.module
**方法动态加载这个指定路径下的Scala脚本文件。
动态加载脚本在使用Chisel编写代码时可以有多个用途,如:
load-ivy.sc
**脚本可以用来引入或更新Ammonite会话中的Ivy依赖,确保代码运行时有必要的库支持。import chisel3._
:基础的Chisel功能,包括定义硬件组件的基本构建块。import chisel3.util._
:提供了一些实用工具和额外的硬件构建块,比如计数器、移位寄存器等。import chisel3.tester._
:提供了测试Chisel硬件设计的工具和框架。import chisel3.tester.RawTester.test
:是**chisel3.tester
**中的一个具体的测试功能,用于执行硬件测试。class Passthrough extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(4.W))
+ val out = Output(UInt(4.W))
+ })
+ io.out := io.in
+}
+
+// with parameter
+class PassthroughGenerator(width: Int) extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(width.W))
+ val out = Output(UInt(width.W))
+ })
+ io.out := io.in
+}
+
class Passthrough extends Module {
+
我们声明一个叫做Passthrough
的新模块。Module
是Chisel内置的一个类,所有硬件模块都必须扩展它
val io = IO(...)
+
我们在一个特殊的**io
变量中声明所有的输入和输出端口。它必须被命名为io
,并且是一个IO
对象或实例,这需要形如IO(_instantiated_bundle_)
**的东西
在Chisel中,io
、in
和out
被声明为val
(不可变引用)而不是**var
(可变引用),因为它们代表硬件模块的接口。在硬件设计中,接口的结构(例如信号的数量、类型和方向)在编译时确定且不会改变。虽然信号的值在模拟过程中会变化,但信号的定义(即接口)是固定的。使用val
**声明这些接口强调了它们是不变的结构,而信号值的变化则通过信号之间的连接和赋值来体现,这与软件编程中变量的概念有所不同。
new Bundle {
+ val in = Input(...)
+ val out = Output(...)
+}
+
我们声明了一个新的硬件结构类型(Bundle),它包含了一些命名的信号**in
和out
**,分别具有输入和输出的方向。
在Chisel中,Bundle
是一种用于定义一组相关信号的类,类似于Verilog中的module
内部信号或VHDL中的record
。它允许开发者将多个信号组合成一个单一的复合类型,这样可以更方便地管理和传递数据结构。每个**Bundle
内的信号可以有不同的类型和方向(如输入Input
、输出Output
**),使其成为定义模块接口和内部数据结构的强大工具。
UInt(4.W)
+
我们声明了信号的硬件类型。在这个案例中,它是宽度为4的无符号整数。
io.out := io.in
+
我们将我们的输入端口连接到我们的输出端口,这样**io.in
驱动io.out
。注意,:=
操作符是一个*Chisel***操作符,它表示右手边的信号驱动左手边的信号。它是一个有方向的操作符。
硬件构建语言(HCLs)的一个整洁之处在于我们可以使用底层的编程语言作为脚本语言。例如,在声明我们的Chisel模块之后,我们接着使用Scala调用Chisel编译器将Chisel的**Passthrough
翻译成Verilog的Passthrough
。这个过程被称为*精炼***。
println(getVerilog(new Passthrough))
+
module Passthrough(
+ input clock,
+ input reset,
+ input [3:0] io_in,
+ output [3:0] io_out
+);
+ assign io_out = io_in; // @[cmd2.sc 6:10]
+endmodule
+
println(getFirrtl(new Passthrough))
+
circuit Passthrough :
+ module Passthrough :
+ input clock : Clock
+ input reset : UInt<1>
+ output io : { flip in : UInt<4>, out : UInt<4>}
+
+ io.out <= io.in @[cmd2.sc 6:10]
+
// Scala Code: `test` runs the unit test.
+// test takes a user Module and has a code block that applies pokes and expects to the
+// circuit under test (c)
+test(new Passthrough()) { c =>
+ c.io.in.poke(0.U) // Set our input to value 0
+ c.io.out.expect(0.U) // Assert that the output correctly has 0
+ c.io.in.poke(1.U) // Set our input to value 1
+ c.io.out.expect(1.U) // Assert that the output correctly has 1
+ c.io.in.poke(2.U) // Set our input to value 2
+ c.io.out.expect(2.U) // Assert that the output correctly has 2
+}
+println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
+
+// Test with width 10
+test(new PassthroughGenerator(10)) { c =>
+ c.io.in.poke(0.U(10.W)) // Set our input to value 0
+ c.io.out.expect(0.U(10.W)) // Assert that the output correctly has 0
+ c.io.in.poke(1.U(10.W)) // Set our input to value 1
+ c.io.out.expect(1.U(10.W)) // Assert that the output correctly has 1
+ c.io.in.poke(2.U(10.W)) // Set our input to value 2
+ c.io.out.expect(2.U(10.W)) // Assert that the output correctly has 2
+}
+
c.io.in.poke(0.U)
:设置模块的输入**in
**为0。c.io.out.expect(0.U)
:检查模块的输出**out
**是否为0,确保电路按预期工作。poke
方法设置输入值,并用expect
**方法验证输出值。在Scala中,可以直接在函数调用后跟一个代码块,这是因为Scala支持高阶函数,即可以接受函数作为参数的函数。在这个例子中,**test
函数接受两个参数:一个是Passthrough
模块的实例,另一个是一个匿名函数(或称为代码块),这个匿名函数以c
作为参数进行操作。这种语法使得代码更加简洁易读,允许直接在调用函数时定义行为逻辑,非常适合进行单元测试等场景。当一个函数的最后一个参数是函数类型时,可以使用特殊的语法糖允许将这个函数参数写在方法调用的外部。这种语法不仅使得代码更加清晰,而且在使用匿名函数或代码块作为参数时尤其有用,因为它允许代码块在视觉上更为突出,从而提高了代码的可读性。这就是为什么test(new Passthrough())
**后面可以直接跟一个代码块的原因。
c =>
是一个函数字面量(匿名函数)的语法,用于定义一个函数。这里,c
是函数的参数,=>
后面跟着的是函数体。在这个上下文中,c
代表传递给测试代码块的模块实例(如Passthrough
模块实例),然后在代码块内部,你可以使用c
来访问和操作这个实例的输入和输出端口。在Scala的函数字面量中,参数类型通常是通过上下文推断出来的,不需要显式声明。在test(new Passthrough()) { c => ... }
这段代码中,c
是由test
函数根据其参数类型推断出的Passthrough
模块实例。也就是说,当你写c =>
时,c
的类型(在这个例子中是Passthrough
模块实例)是由test
函数的定义确定的,根据这个函数期望的参数类型。这就是为什么可以直接使用c
来访问Passthrough
实例的成员,如c.io.in
和c.io.out
,而不需要额外的类型声明。
Note that the `poke` and `expect` use chisel hardware literal notation. Both operations expect literals of the correct type.
+If `poke`ing a `UInt()` you must supply a `UInt` literal (example: `c.io.in.poke(10.U)`, likewise if the input is a `Bool()` the `poke` would expect either `true.B` or `false.B`.
+
class PrintingModule extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(4.W))
+ val out = Output(UInt(4.W))
+ })
+ io.out := io.in
+
+ printf("Print during simulation: Input is %d\n", io.in)
+ // chisel printf has its own string interpolator too
+ printf(p"Print during simulation: IO is $io\n")
+
+ println(s"Print during generation: Input is ${io.in}")
+// s用于一般的Scala字符串插值,而p专门为Chisel设计,用于更方便地在仿真中打印硬件信号和对象
+}
+
+test(new PrintingModule ) { c =>
+ c.io.in.poke(3.U)
+ c.io.out.expect(3.U)
+ c.clock.step(5) // circuit will print
+
+ println(s"Print during testing: Input is ${c.io.in.peek()}")
+}
+
Elaborating design...
+Print during generation: Input is UInt<4>(IO in unelaborated PrintingModule)
+Done elaborating.
+Print during simulation: Input is 3
+Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3)
+Print during simulation: Input is 3
+Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3)
+Print during simulation: Input is 3
+Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3)
+Print during simulation: Input is 3
+Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3)
+Print during simulation: Input is 3
+Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3)
+Print during testing: Input is UInt<4>(3)
+Print during simulation: Input is 0
+Print during simulation: IO is AnonymousBundle(in -> 0, out -> 0)
+test PrintingModule Success: 0 tests passed in 7 cycles in 0.003471 seconds 2016.88 Hz
+
这段代码定义了一个**PrintingModule
类,它扩展了Chisel的Module
**,用于演示在不同阶段打印信息:
printf
语句:这些在仿真时每个时钟周期都会打印。**printf("Print during simulation: Input is %d\n", io.in)
会打印输入信号的值,而printf(p"Print during simulation: IO is $io\n")
会打印io
**对象的信息。这些仅在仿真(运行时)生效。println
语句:这句话在模块的生成阶段打印,即代码编译时,打印到终端或控制台。它不会在仿真时打印,因为它是Scala的打印语句,不是Chisel的。println
语句:这在Scala测试环境中执行,用于打印测试时的信息。如**println(s"Print during testing: Input is ${c.io.in.peek()}")
**将在测试过程中打印输入信号的当前值。c.io.in.poke(3.U)
**设置输入为3。c.io.out.expect(3.U)
**期望输出为3,这个测试会通过,因为输出应该与输入相同。c.clock.step(5)
推进仿真时钟5个周期,这期间printf
**语句会打印信息。Print during simulation: Input is 0
是因为被重置回到默认状态c.clock.step(5)
**,推进了5个时钟周期,加上测试开始前后的各1个周期综上,**println
用于代码生成阶段和测试代码中,打印到Scala的执行环境;printf
**用于仿真阶段,打印到仿真的输出中。
class MyModule extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(4.W))
+ val out = Output(UInt(4.W))
+ })
+
+ // Scala expression
+ val two = 1 + 1
+ println(two)
+ // Chisel expression
+ val utwo = 1.U + 1.U
+ println(utwo)
+ // incorrect
+ val twotwo = 1.U + 1
+
+ io.out := io.in
+}
+
class MyOperators extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(4.W))
+ val out_add = Output(UInt(4.W))
+ val out_sub = Output(UInt(4.W))
+ val out_mul = Output(UInt(4.W))
+ })
+
+ io.out_add := 1.U + 4.U
+ io.out_sub := 2.U - 1.U
+ io.out_mul := 4.U * 2.U
+}
+
+// 没有参数时可以省略小括号
+test(new MyOperators) {c =>
+ c.io.out_add.expect(5.U)
+ c.io.out_sub.expect(1.U)
+ c.io.out_mul.expect(8.U)
+}
+
class MyOperatorsTwo extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(4.W))
+ val out_mux = Output(UInt(4.W))
+ val out_cat = Output(UInt(4.W))
+ })
+
+ val s = true.B
+ io.out_mux := Mux(s, 3.U, 0.U) // should return 3.U, since s is true
+ io.out_cat := Cat(2.U, 1.U) // concatenates 2 (b10) with 1 (b1) to give 5 (101)
+}
+
+println(getVerilog(new MyOperatorsTwo))
+
+test(new MyOperatorsTwo) { c =>
+ c.io.out_mux.expect(3.U)
+ c.io.out_cat.expect(5.U)
+}
+
The Mux
operates like a traditional ternary operator, with the order (select, value if true, value if false)
The Cat
ordering is MSB then LSB (where B refers to bit or bits), and only takes two arguments.
class MAC extends Module {
+ val io = IO(new Bundle {
+ val in_a = Input(UInt(4.W))
+ val in_b = Input(UInt(4.W))
+ val in_c = Input(UInt(4.W))
+ val out = Output(UInt(8.W))
+ })
+
+ io.out := (io.in_a * io.in_b) + io.in_c
+}
+
+test(new MAC) { c =>
+ val cycles = 100
+ import scala.util.Random
+ for (i <- 0 until cycles) {
+ val in_a = Random.nextInt(16)
+ val in_b = Random.nextInt(16)
+ val in_c = Random.nextInt(16)
+ c.io.in_a.poke(in_a.U)
+ c.io.in_b.poke(in_b.U)
+ c.io.in_c.poke(in_c.U)
+ c.io.out.expect((in_a * in_b + in_c).U)
+ }
+}
+
The following circuit arbitrates data coming from a FIFO into two parallel processing units. The FIFO and processing elements (PEs) communicate with ready-valid interfaces. Construct the arbiter to send data to whichever PE is ready to receive data, prioritizing PE0 if both are ready to receive data. Remember that the arbiter should tell the FIFO that it's ready to receive data when at least one of the PEs can receive data. Also, wait for a PE to assert that it's ready before asserting that the data are valid. You will likely need binary operators to complete this exercise.
class Arbiter extends Module {
+ val io = IO(new Bundle {
+ // FIFO
+ val fifo_valid = Input(Bool())
+ val fifo_ready = Output(Bool())
+ val fifo_data = Input(UInt(16.W))
+
+ // PE0
+ val pe0_valid = Output(Bool())
+ val pe0_ready = Input(Bool())
+ val pe0_data = Output(UInt(16.W))
+
+ // PE1
+ val pe1_valid = Output(Bool())
+ val pe1_ready = Input(Bool())
+ val pe1_data = Output(UInt(16.W))
+ })
+
+ io.fifo_ready := io.pe0_ready || io.pe1_ready
+ io.pe0_valid := io.fifo_valid && io.pe0_ready
+ io.pe1_valid := io.fifo_valid && !io.pe0_ready && io.pe1_ready
+ io.pe0_data := io.fifo_data
+ io.pe1_data := io.fifo_data
+}
+
+test(new Arbiter) { c =>
+ import scala.util.Random
+ val data = Random.nextInt(65536)
+ c.io.fifo_data.poke(data.U)
+
+ for (i <- 0 until 8) {
+ c.io.fifo_valid.poke((((i >> 0) % 2) != 0).B)
+ c.io.pe0_ready.poke((((i >> 1) % 2) != 0).B)
+ c.io.pe1_ready.poke((((i >> 2) % 2) != 0).B)
+
+ c.io.fifo_ready.expect((i > 1).B)
+ c.io.pe0_valid.expect((i == 3 || i == 7).B)
+ c.io.pe1_valid.expect((i == 5).B)
+
+ if (i == 3 || i ==7) {
+ c.io.pe0_data.expect((data).U)
+ } else if (i == 5) {
+ c.io.pe1_data.expect((data).U)
+ }
+ }
+}
+println("SUCCESS!!")
+
数据线(如**io.pe0_data
** 和 io.pe1_data
)通常会持续地带有它们可能需要使用的数据信号(本例中为**io.fifo_data
)。但是,这些数据是否被“接收”或“采用”通常由valid
**信号来控制。
测试代码设计用来验证**Arbiter
模块的行为。测试通过随机生成一个数据,然后使用不同的组合的fifo_valid
,pe0_ready
和pe1_ready
**信号来模拟不同的工作情况。
c.io.fifo_data.poke(data.U)
**模拟从FIFO发送的数据。for (i <- 0 until 8)
**遍历8种不同的信号组合状态。c.io.fifo_valid.poke
,**c.io.pe0_ready.poke
和c.io.pe1_ready.poke
根据i
**的不同值模拟不同的信号状态,使用位操作来确定每个信号是否应该被激活。c.io.fifo_ready.expect
,**c.io.pe0_valid.expect
和c.io.pe1_valid.expect
**是对仲裁器预期行为的断言检查。i
的值表示PE0或PE1应该接收数据(如i == 3 || i == 7
是PE0,i == 5
是PE1),则使用expect
断言来检查io.pe0_data
或io.pe1_data
**与FIFO的数据相同。class ParameterizedAdder(saturate: Boolean) extends Module {
+ val io = IO(new Bundle {
+ val in_a = Input(UInt(4.W))
+ val in_b = Input(UInt(4.W))
+ val out = Output(UInt(4.W))
+ })
+ val sum = io.in_a +& io.in_b
+ if(saturate){
+ io.out := Mux(sum>15.U,15.U,sum)
+ }else{
+ io.out := sum
+ }
+}
+
+for (saturate <- Seq(true, false)) {
+ test(new ParameterizedAdder(saturate)) { c =>
+ // 100 random tests
+ val cycles = 100
+ import scala.util.Random
+ import scala.math.min
+ for (i <- 0 until cycles) {
+ val in_a = Random.nextInt(16)
+ val in_b = Random.nextInt(16)
+ c.io.in_a.poke(in_a.U)
+ c.io.in_b.poke(in_b.U)
+ if (saturate) {
+ c.io.out.expect(min(in_a + in_b, 15).U)
+ } else {
+ c.io.out.expect(((in_a + in_b) % 16).U)
+ }
+ }
+
+ // ensure we test saturation vs. truncation
+ c.io.in_a.poke(15.U)
+ c.io.in_b.poke(15.U)
+ if (saturate) {
+ c.io.out.expect(15.U)
+ } else {
+ c.io.out.expect(14.U)
+ }
+ }
+}
+println("SUCCESS!!")
+
在Chisel中,+&
是一个用于加法的运算符,它会考虑输入的进位,得到一个比最大输入位宽更宽的结果。如果输入是4位UInt
,标准加法结果**io.in_a + io.in_b
会是4位,可能会截断超出的位。而+&
加法会产生一个5位的结果,这可以用于在必要时实现饱和加法逻辑。连接一个4位的UInt
线到一个5位的UInt
**线(一个4.W的量等于5.W的量),默认会截断最高位(MSB)。这样,你可以用这个方法来轻松实现非饱和加法器,只保留5位和的低4位。
class LastConnect extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(4.W))
+ val out = Output(UInt(4.W))
+ })
+ io.out := 1.U
+ io.out := 2.U
+ io.out := 3.U
+ io.out := 4.U
+}
+
+// Test LastConnect
+test(new LastConnect) { c => c.io.out.expect(4.U) }
+
当有多个值被赋值给一个量时,最后的那个生效
when
, elsewhen
, and otherwise
when(someBooleanCondition) {
+ // things to do when true
+}.elsewhen(someOtherBooleanCondition) {
+ // things to do on this condition
+}.otherwise {
+ // things to do if none of th boolean conditions are true
+}
+
when
是一种特殊的构造,它用于硬件生成。它类似于软件编程中的if
语句,但是在硬件描述中,when
会生成实际的硬件逻辑,如多路复用器。而标准的if
语句通常用于生成时(编译时)的控制流,并不直接生成硬件。if
语句在Scala(因此在Chisel的生成时)可以有返回值,可以用于赋值或者作为表达式的一部分。相比之下,when
是一种专门为硬件设计提供的控制结构,用于生成条件硬件逻辑,如多路复用器或条件赋值,但它不返回值。因此,当你需要根据输入信号在运行时动态地选择硬件行为时,你会使用when
。而当你在编译时根据某些条件生成不同的硬件模块时,你会使用**if
**。
在Scala中,**==
用于基本类型和对象的相等性比较,而在Chisel中(一个建立在Scala之上的硬件构建语言),===
用于硬件信号之间的相等性比较。这是因为Chisel需要区分硬件操作和Scala的软件操作,===
在Chisel中被定义为生成硬件电路中的等于比较逻辑。而在Scala的if
语句中,==
**仍然用于比较两个值是否相等。这种区分确保了代码的清晰性,以及硬件设计中逻辑正确性的维护。因此,在when
中使用===
来生成判断相等的硬件电路
Wire
ConstructWire
是一种基本的硬件构造类型,用于创建一个可以在模块内部读取和写入的信号。它主要用于表示组合逻辑或暂存数据,允许在硬件描述中定义中间变量或内部连接。使用Wire
时,需要指定信号的数据类型,如UInt
或SInt
等。Wire
类型的变量在定义时不持有初始值,需要在逻辑中显式赋值。在使用过程中,可以根据需要多次对其赋值,但在每个时钟周期结束时,Wire
仅保留最后一次赋值的结果。val myWire = Wire(UInt(32.W))
List(1, 2, 3, 4).permutations.foreach { case i0 :: i1 :: i2 :: i3 :: Nil =>
+ println(s"Sorting $i0 $i1 $i2 $i3")}
+List(1, 2, 3, 4).permutations.foreach { case i0 :: i1 :: i2 :: _ :: Nil =>
+ println(s"Sorting $i0 $i1 $i2")}
+
使用Scala的集合和模式匹配功能来遍历**List(1, 2, 3, 4)
的所有排列组合。List(1, 2, 3, 4).permutations
生成一个包含所有可能排列的列表的迭代器。foreach
**循环遍历这些排列。
在**foreach
的代码块中,case i0 :: i1 :: i2 :: i3 :: Nil =>
是一个模式匹配表达式,用于解构每个排列列表。这个表达式匹配一个包含四个元素的列表,将这四个元素分别绑定到变量i0
、i1
、i2
、i3
。Nil
**在这里表示列表的末尾,确保列表只有这四个元素。这种方式允许直接访问每个排列中的元素,然后可以将它们打印出来或用于其他逻辑操作。
class Polynomial extends Module {
+ val io = IO(new Bundle {
+ val select = Input(UInt(2.W))
+ val x = Input(SInt(32.W))
+ val fOfX = Output(SInt(32.W))
+ })
+
+ val result = Wire(SInt(32.W))
+ val square = Wire(SInt(32.W))
+
+ square := io.x * io.x
+ when(io.select === 0.U){
+ result := square - 2.S * io.x + 1.S
+ }.elsewhen(io.select === 1.U) {
+ result := 2.S * square + 6.S * io.x + 3.S
+ }.otherwise {
+ result := 4.S * square - 10.S * io.x - 5.S
+ }
+
+ io.fOfX := result
+}
+
+// Test Polynomial
+test(new Polynomial) { c =>
+ for(x <- 0 to 20) {
+ for(select <- 0 to 2) {
+ c.io.select.poke(select.U)
+ c.io.x.poke(x.S)
+ c.io.fOfX.expect(poly(select, x).S)
+ }
+ }
+}
+
Grad students pass through four states in their career: Idle, Coding, Writing, and Graduating. These states transition based off three inputs: Coffee, Ideas they come up with, and Pressure from their advisor to make progress. Once they Graduate, they return to the Idle state. The FSM diagram below shows these states and transitions. Any unlabelled transition (i.e. when there are no inputs) returns a grad student to the Idle state instead of staying in the current state. The input precedence is coffee > idea > pressure, so when in the Idle state and receiving both coffee and pressure, a graduate student will move to the Coding state.
检查逻辑:
// state map
+def states = Map("idle" -> 0, "coding" -> 1, "writing" -> 2, "grad" -> 3)
+
+// life is full of question marks
+def gradLife (state: Int, coffee: Boolean, idea: Boolean, pressure: Boolean): Int = {
+ var nextState = states("idle")
+ if(state == states("idle")){
+ if(coffee) nextState = states("coding")
+ else if(idea) nextState = states("idle")
+ else if(pressure) nextState = states("writing")
+ else nextState = states("idle")
+ }else if(state == states("coding")){
+ if(coffee) nextState = states("coding")
+ else if(idea) nextState = states("writing")
+ else if(pressure) nextState = states("writing")
+ else nextState = states("idle")
+ }else if(state == states("writing")){
+ if(coffee) nextState = states("writing")
+ else if(idea) nextState = states("writing")
+ else if(pressure) nextState = states("grad")
+ else nextState = states("idle")
+ }
+ nextState
+}
+
+// some sanity checks
+(0 until states.size).foreach{ state => assert(gradLife(state, false, false, false) == states("idle")) }
+assert(gradLife(states("writing"), true, false, true) == states("writing"))
+assert(gradLife(states("idle"), true, true, true) == states("coding"))
+assert(gradLife(states("idle"), false, true, true) == states("idle"))
+assert(gradLife(states("grad"), false, false, false) == states("idle"))
+
Chisel:
class GradLife extends Module {
+ val io = IO(new Bundle {
+ val state = Input(UInt(2.W))
+ val coffee = Input(Bool())
+ val idea = Input(Bool())
+ val pressure = Input(Bool())
+ val nextState = Output(UInt(2.W))
+ })
+
+ val idle :: coding :: writing :: grad :: Nil = Enum(4)
+
+ when(io.state === idle){
+ when(io.coffee) {io.nextState := coding}
+ .elsewhen(io.idea) {io.nextState := idle}
+ .elsewhen(io.pressure) {io.nextState := writing}
+ .otherwise {io.nextState := idle}
+ } .elsewhen (io.state === coding) {
+ when (io.coffee) { io.nextState := coding }
+ .elsewhen (io.idea || io.pressure) { io.nextState := writing }
+ .otherwise {io.nextState := idle}
+ } .elsewhen (io.state === writing) {
+ when (io.coffee || io.idea) { io.nextState := writing }
+ .elsewhen (io.pressure) { io.nextState := grad }
+ .otherwise {io.nextState := idle}
+ }.otherwise {io.nextState := idle}
+}
+
+// Test
+test(new GradLife) { c =>
+ // verify that the hardware matches the golden model
+ for (state <- 0 to 3) {
+ for (coffee <- List(true, false)) {
+ for (idea <- List(true, false)) {
+ for (pressure <- List(true, false)) {
+ c.io.state.poke(state.U)
+ c.io.coffee.poke(coffee.B)
+ c.io.idea.poke(idea.B)
+ c.io.pressure.poke(pressure.B)
+ c.io.nextState.expect(gradLife(state, coffee, idea, pressure).U)
+ }
+ }
+ }
+ }
+}
+
A Reg
holds its output value until the rising edge of its clock, at which time it takes on the value of its input.
class RegisterModule extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(12.W))
+ val out = Output(UInt(12.W))
+ })
+
+ // val register : UInt = Reg(UInt(12.W))
+ val register = Reg(UInt(12.W))
+ register := io.in + 1.U
+ io.out := register
+}
+
+test(new RegisterModule) { c =>
+ for (i <- 0 until 100) {
+ c.io.in.poke(i.U)
+ c.clock.step(1)
+ c.io.out.expect((i + 1).U)
+ }
+}
+println("SUCCESS!!")
+
Notice: One important note is that Chisel distinguishes between types (like UInt
) and hardware nodes (like the literal 2.U
, or the output of myReg
).
// legal because a Reg needs a data type as a model
+val myReg = Reg(UInt(2.W))
+// error because `2.U` is already a hardware node and can't be used as a model
+val myReg = Reg(2.U)
+
RegInit
The register in RegisterModule
was initialized to random data for simulation. Unless otherwised specified, registers do not have a reset value (or a reset). The way to create a register that resets to a given value is with RegInit
.
// The first argument is a type node that specified the datatype and its width.
+// The second argument is a hardware node that specified the reset value, in this case 0.
+val myReg = RegInit(UInt(12.W), 0.U)
+
+// It is a hardware node that specifies the reset value, but normally `0.U`.
+val myReg = RegInit(0.U(12.W))
+
RegInit
不仅初始化,也创建了这个reg,因此不需要先创建再init
class RegInitModule extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(12.W))
+ val out = Output(UInt(12.W))
+ })
+
+ val register = RegInit(0.U(12.W))
+ register := io.in + 1.U
+ io.out := register
+}
+
RegNext
RegNext
在 Chisel 中是一个用于创建寄存器并在下一个时钟周期将输入信号的值传递给该寄存器的便捷方法。它简化了寄存器的声明和初始化,使得您可以轻松地创建一个将当前输入信号的值保存到下一个时钟周期的寄存器。使用**RegNext
**时,可以指定一个初始值,如果不指定,则寄存器在复位时的值是未定义的
在 Chisel 中使用 RegNext
的基本语法如下:
val myReg = RegNext(inputSignal, initValue)
+
inputSignal
是你希望在下一个时钟周期传递给寄存器的信号。initValue
是可选参数,用于指定寄存器在复位时的初始值。如果不提供初始值,寄存器在复位时的值是未定义的。class MyShiftRegister(val init: Int = 1) extends Module {
+ val io = IO(new Bundle {
+ val in = Input(Bool())
+ val out = Output(UInt(4.W))
+ })
+
+ val state = RegInit(UInt(4.W), init.U)
+ val stateTemp = (state << 1.U) + io.in.asUInt
+ state := stateTemp
+ io.out := state
+}
+
+test(new MyShiftRegister()) { c =>
+ var state = c.init
+ for (i <- 0 until 10) {
+ // poke in LSB of i (i % 2)
+ c.io.in.poke(((i % 2) != 0).B)
+ // update expected state
+ state = ((state * 2) + (i % 2)) & 0xf
+ c.clock.step(1)
+ c.io.out.expect(state.U)
+ }
+}
+println("SUCCESS!!")
+
// n is the output width (number of delays - 1)
+// init state to init
+class MyOptionalShiftRegister(val n: Int, val init: BigInt = 1) extends Module {
+ val io = IO(new Bundle {
+ val en = Input(Bool())
+ val in = Input(Bool())
+ val out = Output(UInt(n.W))
+ })
+
+ val state = RegInit(init.U(n.W))
+
+ when(io.en){
+ state := state << 1 | io.in
+ }
+ io.out := state
+}
+
+// test different depths
+for (i <- Seq(3, 4, 8, 24, 65)) {
+ println(s"Testing n=$i")
+ test(new MyOptionalShiftRegister(n = i)) { c =>
+ val inSeq = Seq(0, 1, 1, 1, 0, 1, 1, 0, 0, 1)
+ var state = c.init
+ var i = 0
+ c.io.en.poke(true.B)
+ while (i < 10 * c.n) {
+ // poke in repeated inSeq
+ val toPoke = inSeq(i % inSeq.length)
+ c.io.in.poke((toPoke != 0).B)
+ // update expected state
+ state = ((state * 2) + toPoke) & BigInt("1"*c.n, 2)
+ c.clock.step(1)
+ c.io.out.expect(state.U)
+
+ i += 1
+ }
+ }
+}
+println("SUCCESS!!")
+
Notice: Chisel中变量被声明为常量val
,因此一个变量只能被赋值一次,因为这表示硬件电路连接,但是会根据输入等的不同而具有不同的值。因此不能多次给一个变量赋值,如果需要,可以把中间值重新命名为一个val
来调用
Chisel模块默认使用隐式的时钟和复位信号,每个内部创建的寄存器都会使用这些默认信号。在某些情况下,你可能需要覆盖这种默认行为,比如使用生成时钟或复位信号的黑盒,或者设计多时钟系统。Chisel提供了**withClock() {}
、withReset() {}
和withClockAndReset() {}
等构造来处理这些情况,允许分别或同时覆盖时钟和复位。需要注意的是,至本教程编写时,复位信号总是同步的并且是Bool
类型。时钟在Chisel中有其自身的类型(Clock
),并且应该相应声明。Bool
类型可以通过调用asClock()
转换为Clock
类型,但需要确保这样做是合理的。同时,chisel-testers
**目前对多时钟设计的支持并不完全。
// we need to import multi-clock features
+import chisel3.experimental.{withClock, withReset, withClockAndReset}
+
+class ClockExamples extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(10.W))
+ val alternateReset = Input(Bool())
+ val alternateClock = Input(Clock())
+ val outImplicit = Output(UInt())
+ val outAlternateReset = Output(UInt())
+ val outAlternateClock = Output(UInt())
+ val outAlternateBoth = Output(UInt())
+ })
+
+ val imp = RegInit(0.U(10.W))
+ imp := io.in
+ io.outImplicit := imp
+
+ withReset(io.alternateReset) {
+ // everything in this scope with have alternateReset as the reset
+ val altRst = RegInit(0.U(10.W))
+ altRst := io.in
+ io.outAlternateReset := altRst
+ }
+
+ withClock(io.alternateClock) {
+ val altClk = RegInit(0.U(10.W))
+ altClk := io.in
+ io.outAlternateClock := altClk
+ }
+
+ withClockAndReset(io.alternateClock, io.alternateReset) {
+ val alt = RegInit(0.U(10.W))
+ alt := io.in
+ io.outAlternateBoth := alt
+ }
+}
+
+println(getVerilog(new ClockExamples))
+
通过**import chisel3.experimental.{withClock, withReset, withClockAndReset}
引入了多时钟特性。ClockExamples
模块定义了一个10位宽的输入io.in
,以及替代的复位和时钟信号io.alternateReset
和io.alternateClock
**。模块输出了四种不同情况下的寄存器值:使用默认时钟和复位、只替换复位、只替换时钟、同时替换时钟和复位。
withReset(io.alternateReset) {...}
块定义了一个新的作用域,其中所有寄存器的复位信号被替换为io.alternateReset
。在这个作用域内,**altRst
寄存器在被替代复位信号复位时初始化为0,并在每个时钟周期将io.in
**的值赋给它。withClock(io.alternateClock) {...}
块定义了另一个作用域,其中所有寄存器的时钟信号被替换为io.alternateClock
。在这个作用域内,**altClk
寄存器在被替代时钟信号驱动时初始化为0,并在每个时钟周期将io.in
**的值赋给它。withClockAndReset(io.alternateClock, io.alternateReset) {...}
块同时替换了寄存器的时钟和复位信号为io.alternateClock
和io.alternateReset
。在这个作用域内,**alt
寄存器同时被替代的时钟和复位信号控制,初始化为0,并在每个时钟周期将io.in
**的值赋给它。class My4ElementFir(b0: Int, b1: Int, b2: Int, b3: Int) extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(8.W))
+ val out = Output(UInt(8.W))
+ })
+
+ val reg_1 = RegInit(0.U(8.W))
+ val reg_2 = RegInit(0.U(8.W))
+ val reg_3 = RegInit(0.U(8.W))
+
+ reg_1 := io.in
+ reg_2 := reg_1
+ reg_3 := reg_2
+
+ // 或者使用RegNext来一并定义初始化及赋值
+ val reg_1 = RegNext(io.in, 0.U)
+ val reg_2 = RegNext(reg_1, 0.U)
+ val reg_3 = RegNext(reg_2, 0.U)
+
+ io.out := b0.U(8.W) * io.in + b1.U(8.W) * reg_1 + b2.U(8.W) * reg_2 + b3.U(8.W) * reg_3
+}
+
一个有限脉冲响应(FIR)滤波器生成器。生成器的**length
参数决定了滤波器的抽头数目,即滤波器的长度。这个生成器有三个输入:in
(滤波器的输入信号)、valid
(一个布尔值,表示输入是否有效)和consts
(一个向量,包含所有抽头的系数)。还有一个输出out
**,即滤波器的输出。
taps
**是一个序列,包含输入和一系列寄存器,用于实现滤波器的延迟线。valid
**信号为真时,序列中的每个元素(除了第一个)被更新为前一个元素的值。out
**是抽头值和对应系数乘积之和。这个结构允许滤波器动态处理不同长度的输入,通过改变**consts
**向量的内容来改变滤波器的特性。
class MyManyDynamicElementVecFir(length: Int) extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(8.W))
+ val valid = Input(Bool())
+ val out = Output(UInt(8.W))
+ val consts = Input(Vec(length, UInt(8.W)))
+ })
+
+ // Such concision! You'll learn what all this means later.
+ val taps = Seq(io.in) ++ Seq.fill(io.consts.length - 1)(RegInit(0.U(8.W)))
+ taps.zip(taps.tail).foreach { case (a, b) => when (io.valid) { b := a } }
+
+ io.out := taps.zip(io.consts).map { case (a, b) => a * b }.reduce(_ + _)
+}
+
+visualize(() => new MyManyDynamicElementVecFir(4))
+
val io = IO(new Bundle {...})
定义了模块的接口,包括8位宽的输入in
,一个有效信号**valid
,8位宽输出out
,和长度为length
的系数向量consts
**。
**val taps = Seq(io.in) ++ Seq.fill(io.consts.length - 1)(RegInit(0.U(8.W)))
这行代码在FIR滤波器实现中创建了一个名为taps
的序列,用于存储当前和之前的输入值,从而实现数据的时间序列延迟。首先,它将输入信号io.in
作为序列的第一个元素。随后,使用++
操作符将io.in
与一个新的序列连接起来,后者通过Seq.fill(io.consts.length - 1)(RegInit(0.U(8.W)))
创建,其中包含length - 1
个初始化为0的8位寄存器。这样,taps
序列就包含了一个输入信号和length - 1
个延迟寄存器,总共length
个元素,每个元素对应滤波器的一个抽头。在Chisel中,虽然io.in
不是寄存器,但taps
序列可以包含不同类型的元素,因为在Chisel里,所有这些都被视为Data
类型的子类,可以被综合为硬件。在这个上下文中,io.in
**是直接的输入信号,而后续元素是寄存器类型,但它们共同构成了一个序列,用于表示滤波器的不同时间点上的信号值。这种混合类型的序列是可行的,并可以在Chisel生成的硬件中正确表达相应的逻辑。
**taps.zip(taps.tail).foreach { case (a, b) => when (io.valid) { b := a } }
在输入valid
为真时,将taps
**序列中每个元素的值传递到下一个元素,实现数据在寄存器间的移动。
zip
是一个方法,它将两个集合中对应位置的元素组成一对,生成一个新的集合。在这里,taps.zip(taps.tail)
的作用是将taps
列表中的每个元素与其后面的元素配对。tail
是一个方法,返回除第一个元素外的列表所有元素。例如,如果taps
是[in, reg1, reg2, reg3]
,那么**taps.tail
就是[reg1, reg2, reg3]
。taps.zip(taps.tail)
的结果将是[(in, reg1), (reg1, reg2), (reg2, reg3)]
。这样,foreach
就可以遍历这些配对,根据valid
**信号更新寄存器的值,实现数据的逐级传递。
case (a, b) =>
是模式匹配的语法,用于解构元组,将zip
操作生成的元素对分别赋值给a
(当前元素)和**b
**(下一个元素)。
io.out := taps.zip(io.consts).map { case (a, b) => a * b }.reduce(_ + _)
计算输出out
,即将每个延迟元素与其对应的系数相乘,并将所有乘积求和得到最终结果。
在这段代码中,**map
和reduce
**是Scala中的集合操作方法:
map
:对集合中的每个元素应用一个函数。这里**map { case (a, b) => a * b }
对每对(a, b)
应用乘法操作,a
来自taps
,b
来自io.consts
**,分别代表寄存器中的数据和滤波器的系数。reduce
:对集合中的元素应用一个二元操作,逐步将集合减少为单一结果。这里的**reduce(_ + _)
**将所有乘法结果相加,得到最终的滤波输出。不使用**foreach
是因为foreach
仅用于执行操作而不返回结果,而这里的目的是计算经过滤波器后的输出值,需要通过map
和reduce
**聚合计算结果。
iotesters | ChiselTest | |
---|---|---|
poke | poke(c.io.in1, 6) | c.io.in1.poke(6.U) |
peek | peek(c.io.out1) | c.io.out1.peek() |
expect | expect(c.io.out1, 6) | c.io.out1.expect(6.U) |
step | step(1) | c.clock.step(1) |
initiate | Driver.execute(...) { c => | test(...) { c => |
class QueueModule[T <: Data](ioType: T, entries: Int) extends MultiIOModule {
+ val in = IO(Flipped(Decoupled(ioType)))
+ val out = IO(Decoupled(ioType))
+ out <> Queue(in, entries)
+}
+
class QueueModule[T <: Data](ioType: T, entries: Int) extends MultiIOModule
定义了一个泛型队列模块,其中**T <: Data
表示T
是Data
类型或其子类型。ioType
是队列中数据的类型,entries
**是队列的大小。Decoupled(ioType)
是一个Chisel提供的高级接口,自动包含了valid
、**bits
和ready
信号。valid
和bits
组合用于传输有效数据,而ready
信号用于流量控制。当你声明一个Decoupled
**接口时,这些信号都会被自动创建。IO(Flipped(Decoupled(ioType)))
创建了一个输入端口,**Flipped
意味着通常的输入输出方向被反转(即原本是输出的valid
和bits
成为输入,原本是输入的ready
成为输出),Decoupled
**表示它是一个可以被反压的接口。out <> Queue(in, entries)
将输出端口**out
连接到一个新建的Queue
实例,Queue(in, entries)
创建了一个队列,其中in
是输入端口,entries
是队列大小。<>
是连接操作符,表示双向连接。确保了数据可以从in
流向队列,经过处理后,再从队列流向out
**。**EnqueueNow
和expectDequeueNow
是用于测试队列行为的方法。EnqueueNow
用于立即将数据入队,而不需要等待队列准备好。expectDequeueNow
**用于立即从队列中出队数据,并验证其值是否符合预期。这两个方法都是在基于队列的测试中非常有用,使得测试代码可以直接控制和验证队列中数据的流动。这样,测试者可以确保队列正确地处理了入队和出队操作,并且数据的传输符合设计的预期。
test(new QueueModule(UInt(9.W), entries = 200)) { c =>
+ // Example testsequence showing the use and behavior of Queue
+ c.in.initSource()
+ c.in.setSourceClock(c.clock)
+ c.out.initSink()
+ c.out.setSinkClock(c.clock)
+
+ val testVector = Seq.tabulate(200){ i => i.U }
+
+ testVector.zip(testVector).foreach { case (in, out) =>
+ c.in.enqueueNow(in)
+ c.out.expectDequeueNow(out)
+ }
+}
+
There is some required boiler plate initSource
, setSourceClock
, etc that is necessary to ensure that the ready
and valid
fields are all initialized correctly at the beginning of the test.
EnqueueSeq
允许你一次性将一个序列的元素批量入队,这对于测试需要连续多个数据处理的场景特别有用。DequeueSeq
, 相应地,用于一次性从队列中出队多个元素,并验证这些元素是否符合预期的序列。enqueueSeq
must finish before the expectDequeueSeq
can begin. This example would fail if the testVector
's size is made larger than the queue depth, because the queue would fill up and not be able to complete the enqueueSeq
.
**fork
和join
用于创建并发测试,允许同时执行多个操作或测试场景。使用fork
可以启动一个并发的测试过程,这个过程可以与主测试流程同时运行。可以在fork
后使用多个测试命令定义并发执行的操作。随后,join
**用于等待所有并发启动的测试过程完成。这样,你可以在一个测试中执行多个并行操作,例如同时对多个模块输入不同的信号,或者同时观察多个输出。这在需要模拟复杂交互或并行处理时特别有用。
test(new QueueModule(UInt(9.W), entries = 200)) { c =>
+ // Example testsequence showing the use and behavior of Queue
+ c.in.initSource()
+ c.in.setSourceClock(c.clock)
+ c.out.initSink()
+ c.out.setSinkClock(c.clock)
+
+ val testVector = Seq.tabulate(300){ i => i.U }
+
+ fork {
+ c.in.enqueueSeq(testVector)
+ }.fork {
+ c.out.expectDequeueSeq(testVector)
+ }.join()
+}
+
虽然**fork
启动了两个并发过程,似乎表明c.in.enqueueSeq(testVector)
和c.out.expectDequeueSeq(testVector)
应该同时执行,但实际上,它们在逻辑上是有先后顺序的。enqueueSeq
首先向队列中填充数据,而expectDequeueSeq
则等待这些数据从队列中出现并验证它们。在ChiselTest中,fork
创建的并发线程会同时开始执行,但是expectDequeueSeq
**自然会在等待有数据可以出队之后才开始验证,确保了数据的正确流向和测试的逻辑顺序。
class ParameterizedScalaObject(param1: Int, param2: String) {
+ println(s"I have parameters: param1 = $param1 and param2 = $param2")
+}
+val obj1 = new ParameterizedScalaObject(4, "Hello")
+val obj2 = new ParameterizedScalaObject(4 + 2, "World")
+
class ParameterizedWidthAdder(in0Width: Int, in1Width: Int, sumWidth: Int) extends Module {
+ require(in0Width >= 0)
+ require(in1Width >= 0)
+ require(sumWidth >= 0)
+ val io = IO(new Bundle {
+ val in0 = Input(UInt(in0Width.W))
+ val in1 = Input(UInt(in1Width.W))
+ val sum = Output(UInt(sumWidth.W))
+ })
+ // a +& b includes the carry, a + b does not
+ io.sum := io.in0 +& io.in1
+}
+
+println(getVerilog(new ParameterizedWidthAdder(1, 4, 6)))
+
The above code block has some require(...)
statements. These are pre-elaboration assertions, which are useful when your generator only works with certain parameterizations or when some parameterizations are mutually exclusive or nonsensical. The above code block checks that widths are non-negative.
/** Sort4 sorts its 4 inputs to its 4 outputs */
+class Sort4(ascending: Boolean) extends Module {
+ val io = IO(new Bundle {
+ val in0 = Input(UInt(16.W))
+ val in1 = Input(UInt(16.W))
+ val in2 = Input(UInt(16.W))
+ val in3 = Input(UInt(16.W))
+ val out0 = Output(UInt(16.W))
+ val out1 = Output(UInt(16.W))
+ val out2 = Output(UInt(16.W))
+ val out3 = Output(UInt(16.W))
+ })
+
+ // this comparison funtion decides < or > based on the module's parameterization
+ def comp(l: UInt, r: UInt): Bool = {
+ if (ascending) {
+ l < r
+ } else {
+ l > r
+ }
+ }
+
+ val row10 = Wire(UInt(16.W))
+ val row11 = Wire(UInt(16.W))
+ val row12 = Wire(UInt(16.W))
+ val row13 = Wire(UInt(16.W))
+
+ when(comp(io.in0, io.in1)) {
+ row10 := io.in0 // preserve first two elements
+ row11 := io.in1
+ }.otherwise {
+ row10 := io.in1 // swap first two elements
+ row11 := io.in0
+ }
+
+ when(comp(io.in2, io.in3)) {
+ row12 := io.in2 // preserve last two elements
+ row13 := io.in3
+ }.otherwise {
+ row12 := io.in3 // swap last two elements
+ row13 := io.in2
+ }
+
+ val row21 = Wire(UInt(16.W))
+ val row22 = Wire(UInt(16.W))
+
+ when(comp(row11, row12)) {
+ row21 := row11 // preserve middle 2 elements
+ row22 := row12
+ }.otherwise {
+ row21 := row12 // swap middle two elements
+ row22 := row11
+ }
+
+ val row20 = Wire(UInt(16.W))
+ val row23 = Wire(UInt(16.W))
+ when(comp(row10, row13)) {
+ row20 := row10 // preserve the first and the forth elements
+ row23 := row13
+ }.otherwise {
+ row20 := row13 // swap the first and the forth elements
+ row23 := row10
+ }
+
+ when(comp(row20, row21)) {
+ io.out0 := row20 // preserve first two elements
+ io.out1 := row21
+ }.otherwise {
+ io.out0 := row21 // swap first two elements
+ io.out1 := row20
+ }
+
+ when(comp(row22, row23)) {
+ io.out2 := row22 // preserve first two elements
+ io.out3 := row23
+ }.otherwise {
+ io.out2 := row23 // swap first two elements
+ io.out3 := row22
+ }
+}
+
val map = Map("a" -> 1)
+val a = map.get("a")
+println(a)
+val b = map.get("b")
+println(b)
+
在Scala中,Map.get(key)
方法返回一个Option
类型:如果键存在,则返回Some(value)
;如果键不存在,则返回**None
。在您的例子中,map.get("a")
返回Some(1)
,因为"a"是映射中的一个键,值为1。然而,map.get("b")
返回None
,因为"b"不是映射中的键。Some
和None
**用于Scala中以安全且表达性的方式处理值的存在或缺失,避免空指针异常。
val some = Some(1)
+val none = None
+println(some.get) // Returns 1
+// println(none.get) // Errors!
+println(some.getOrElse(2)) // Returns 1
+println(none.getOrElse(2)) // Returns 2
+
getOrElse
是一个常用于Option
类型的方法,它允许你为Option
可能不包含值(即为None
)的情况提供一个默认值。getOrElse
接受一个参数,这个参数是当Option
为None
时将返回的值。如果Option
是Some
,则**getOrElse
会返回包裹在Some
**中的值。
class DelayBy1(resetValue: Option[UInt] = None) extends Module {
+ val io = IO(new Bundle {
+ val in = Input( UInt(16.W))
+ val out = Output(UInt(16.W))
+ })
+ val reg = if (resetValue.isDefined) { // resetValue = Some(number)
+ RegInit(resetValue.get)
+ } else { //resetValue = None
+ Reg(UInt())
+ }
+ reg := io.in
+ io.out := reg
+}
+
+println(getVerilog(new DelayBy1))
+println(getVerilog(new DelayBy1(Some(3.U))))
+
将 resetValue
默认初始化为 Option[UInt] = None
,Reg(UInt())
可以从上下文中自动推断,不过最好还是指定位宽
// 还可以用match来实现ifelse
+class DelayBy1(resetValue: Option[UInt] = None) extends Module {
+ val io = IO(new Bundle {
+ val in = Input( UInt(16.W))
+ val out = Output(UInt(16.W))
+ })
+ val reg = resetValue match {
+ case Some(r) => RegInit(r)
+ case None => Reg(UInt())
+ }
+ reg := io.in
+ io.out := reg
+}
+
Scala中的匹配概念在Chisel中被广泛使用,是每个Chisel程序员必须理解的基础知识。Scala提供的match操作符支持以下功能:
val mixedList = List(1, "string", false)
**。// y is an integer variable defined somewhere else in the code
+val y = 7
+/// ...
+val x = y match {
+ case 0 => "zero" // One common syntax, preferred if fits in one line
+ case 1 => // Another common syntax, preferred if does not fit in one line.
+ "one" // Note the code block continues until the next case
+ case 2 => { // Another syntax, but curly braces are not required
+ "two"
+ }
+ case _ => "many" // _ is a wildcard that matches all values
+}
+println("y is " + x) // out: y is many
+
y 为7,不匹配,因此采用默认值
=>
操作符后面的代码块都会继续执行,直到它到达match
的结束大括号或下一个case
**语句。match
会按照case
语句的顺序进行搜索,一旦匹配到一个case
语句,就不会再对其他case
**语句进行检查。_
**作为通配符,来处理任何未找到匹配的值。def animalType(biggerThanBreadBox: Boolean, meanAsCanBe: Boolean): String = {
+ (biggerThanBreadBox, meanAsCanBe) match {
+ case (true, true) => "wolverine"
+ case (true, false) => "elephant"
+ case (false, true) => "shrew"
+ case (false, false) => "puppy"
+ }
+}
+println(animalType(true, true)) // wolverine
+
val sequence = Seq("a", 1, 0.0)
+sequence.foreach { x =>
+ x match {
+ case s: String => println(s"$x is a String")
+ case s: Int => println(s"$x is an Int")
+ case s: Double => println(s"$x is a Double")
+ case _ => println(s"$x is an unknown type!")
+ }
+}
+
Seq
是Scala集合中的一个接口,它代表序列,而List
是Seq
的一个具体实现。在这个例子中,可以直接用List("a", 1, 0.0)
来代替Seq("a", 1, 0.0)
,而不会影响**foreach
和match
**语句的行为。
If you want to match on whether a value has one of many types, use the following syntax. Note that you must use an _
when matching.
val sequence = Seq("a", 1, 0.0)
+sequence.foreach { x =>
+ x match {
+ case _: Int | _: Double => println(s"$x is a number!")
+ case _ => println(s"$x is an unknown type!")
+ }
+}
+
类型匹配在Scala中有一些限制。由于Scala运行在JVM上,而JVM不保持多态类型信息,因此你不能在运行时基于它们进行匹配(因为这些类型信息已被擦除)。注意下面的例子始终匹配第一个case语句,因为**[String]
、[Int]
、[Double]
这些多态类型在运行时被擦除了,case语句实际上只是在匹配Seq
**。
val sequence = Seq(Seq("a"), Seq(1), Seq(0.0))
+sequence.foreach { x =>
+ x match {
+ case s: Seq[String] => println(s"$x is a String")
+ case s: Seq[Int] => println(s"$x is an Int")
+ case s: Seq[Double] => println(s"$x is a Double")
+ }
+}
+
在Scala中,类型擦除指的是JVM在运行时不保留泛型的具体类型信息。因此,当你对**Seq[String]
、Seq[Int]
或Seq[Double]
进行模式匹配时,JVM实际上无法区分这些Seq
的元素类型,因为泛型信息[String]
、[Int]
、[Double]
已经被擦除,只留下了基础的Seq
类型。所以,这些case语句在运行时都被视为对Seq
类型的匹配,而无法区分具体是哪种Seq
。因此,匹配总是成功于第一个case,无论其实际参数是什么类型的Seq
。这就是为什么在运行时你看到的行为似乎是它总是匹配Seq
**的原因。
有时我们希望IO端口能够根据需要选择性地包含或排除。例如,在调试时可能需要查看一些内部状态,但在生成器用于系统时希望将其隐藏。或者,你的生成器可能有一些输入在某些情况下不需要连接,因为存在合理的默认值。
示例中展示了一个可选地接收进位信号的一位加法器。如果包含进位,**io.carryIn
将是Some[UInt]
类型并包含在IO束中;如果不包含进位,io.carryIn
将是None
**类型并从IO束中排除。
class HalfFullAdder(val hasCarry: Boolean) extends Module {
+ val io = IO(new Bundle {
+ val a = Input(UInt(1.W))
+ val b = Input(UInt(1.W))
+ val carryIn = if (hasCarry) Some(Input(UInt(1.W))) else None
+ val s = Output(UInt(1.W))
+ val carryOut = Output(UInt(1.W))
+ })
+ val sum = io.a +& io.b +& io.carryIn.getOrElse(0.U)
+ io.s := sum(0)
+ io.carryOut := sum(1)
+}
+
class HalfFullAdder(val hasCarry: Boolean) extends Module {
+ val io = IO(new Bundle {
+ val a = Input(UInt(1.W))
+ val b = Input(UInt(1.W))
+ val carryIn = Input(if (hasCarry) UInt(1.W) else UInt(0.W))
+ val s = Output(UInt(1.W))
+ val carryOut = Output(UInt(1.W))
+ })
+ val sum = io.a +& io.b +& io.carryIn
+ io.s := sum(0)
+ io.carryOut := sum(1)
+}
+
也可以用一个0宽度的wire来代替None。An IO with width zero is pruned from the emitted Verilog, and anything that tries to use the value of a zero-width wire gets a constant zero.
为了减少大量重复的模板代码,Scala引入了*隐式(implicits)*的概念,允许编译器为你自动进行一些语法简化。由于很多操作是在背后进行,隐式使用可能显得很神奇。
隐式参数的一个常见用途是当你的代码在深层的函数调用中需要访问某个顶层变量时,可以使用隐式参数自动传递这个变量,而不是手动在每个函数调用中传递它。
object CatDog {
+ implicit val numberOfCats: Int = 3
+ //implicit val numberOfDogs: Int = 5
+
+ def tooManyCats(nDogs: Int)(implicit nCats: Int): Boolean = nCats > nDogs
+
+ val imp = tooManyCats(2) // Argument passed implicitly!
+ val exp = tooManyCats(2)(1) // Argument passed explicitly!
+}
+CatDog.imp
+CatDog.exp
+
首先,我们定义了一个隐式值**numberOfCats
。在给定的作用域中,同一类型的隐式值只能有一个。然后,我们定义了一个函数,它接受两个参数列表;第一个是任何显式参数,第二个是任何隐式参数。当我们调用tooManyCats
**时,我们可以省略第二个隐式参数列表(让编译器为我们找到它),或者显式提供一个参数(这个参数可以与隐式值不同)。
隐式参数可能失败的情况包括:
**object
定义了一个单例对象,它是一个类的单一实例。与class
不同,当你定义一个object
时,Scala会自动为你创建这个类的一个实例。你不需要使用new
关键字来创建它的实例。在这个例子中,CatDog
是一个单例对象,可以直接访问其成员,无需创建实例。这在定义工具方法或当你需要一个全局唯一的实体时非常有用,比如这里的numberOfCats
隐式值和tooManyCats
**方法。
sealed trait Verbosity
+implicit case object Silent extends Verbosity
+case object Verbose extends Verbosity
+
+class ParameterizedWidthAdder(in0Width: Int, in1Width: Int, sumWidth: Int)(implicit verbosity: Verbosity)
+extends Module {
+ def log(msg: => String): Unit = verbosity match {
+ case Silent =>
+ case Verbose => println(msg)
+ }
+ require(in0Width >= 0)
+ log(s"in0Width of $in0Width OK")
+ require(in1Width >= 0)
+ log(s"in1Width of $in1Width OK")
+ require(sumWidth >= 0)
+ log(s"sumWidth of $sumWidth OK")
+ val io = IO(new Bundle {
+ val in0 = Input(UInt(in0Width.W))
+ val in1 = Input(UInt(in1Width.W))
+ val sum = Output(UInt(sumWidth.W))
+ })
+ log("Made IO")
+ // 对于结果位宽自然容纳进位的情况,直接使用+也是可行的。
+ io.sum := io.in0 + io.in1
+ log("Assigned output")
+}
+
+println(getVerilog(new ParameterizedWidthAdder(1, 4, 5)))
+println(getVerilog(new ParameterizedWidthAdder(1, 4, 5)(Verbose)))
+
Verbosity
的密封特质(sealed trait
)和两个实现这个特质的对象。sealed trait Verbosity
表示Verbosity
是一个可以被继承的类型,但所有继承它的类必须定义在同一个文件中,这有助于实现模式匹配的完整性检查。implicit case object Silent extends Verbosity
定义了一个隐式的单例对象Silent
,它是Verbosity
的一个实现,可以在需要Verbosity
类型的隐式参数时自动使用。case object Verbose extends Verbosity
定义了另一个名为Verbose
的单例对象,也是Verbosity
的实现。case object
**通常用于代表不可变、无状态的值或单例定义.msg: => String
这样的参数定义使用了名为“call-by-name”的参数传递机制。这种机制意味着,只有在函数内部实际使用到msg
时,传入的字符串表达式才会被计算。这对于条件日志记录来说非常有用,因为它允许延迟计算日志消息直到确实需要打印消息时。例如,如果verbosity
是Silent
,则**msg
**根本不会被计算,这样就避免了不必要的性能开销。隐式函数(也称为隐式转换)用于减少模板代码。更具体地说,它们用于自动将一个Scala对象转换为另一个对象。
在下面的例子中,我们有两个类,Animal
和Human
。Animal
有一个species
字段,但Human
没有。然而,通过实现一个隐式转换,我们可以在Human
上调用species
方法。这意味着即使Human
类原本不包含species
字段,通过隐式转换,我们也可以像访问自己的属性一样访问species
,就好像这个属性是**Human
**类的一部分一样。
class Animal(val name: String, val species: String)
+class Human(val name: String)
+implicit def human2animal(h: Human): Animal = new Animal(h.name, "Homo sapiens")
+val me = new Human("Adam")
+println(me.species)
+
/**
+ * A naive implementation of an FIR filter with an arbitrary number of taps.
+ */
+class ScalaFirFilter(taps: Seq[Int]) {
+ var pseudoRegisters = List.fill(taps.length)(0)
+
+ def poke(value: Int): Int = {
+ pseudoRegisters = value :: pseudoRegisters.take(taps.length - 1)
+ var accumulator = 0
+ for(i <- taps.indices) {
+ accumulator += taps(i) * pseudoRegisters(i)
+ }
+ accumulator
+ }
+}
+
当**taps
变为Seq[Int]
时,意味着类的用户可以在构造类时传递任意长度的Int
**序列。
使用**var pseudoRegisters = List.fill(taps.length)(0)
创建了一个List
,用于存储前几个周期的值。选择List
**是因为其添加元素到头部和移除最后一个元素的语法非常简单。理论上可以使用Scala集合家族中的任何成员。这个列表被初始化为全零。
我们的类添加了一个poke函数/方法,模拟将新输入放入过滤器并循环时钟。
**pseudoRegisters = value :: pseudoRegisters.take(taps.length - 1)
首先使用列表的take
方法保留除最后一个元素外的所有元素,然后使用::
列表连接操作符将value
**添加到缩减版列表的头部。
一个简单的for循环和累加器用于求和列表中每个元素与其对应抽头系数的乘积。仅含**accumulator
**的行将该值作为函数结果返回。
为了避免使用繁杂的手动验证,这里使用Golden Model来生成期望的值,并与Chisel的结果对比
val goldenModel = new ScalaFirFilter(Seq(1, 1, 1, 1))
+
+test(new My4ElementFir(1, 1, 1, 1)) { c =>
+ for(i <- 0 until 100) {
+ val input = scala.util.Random.nextInt(8)
+
+ val goldenModelResult = goldenModel.poke(input)
+
+ c.io.in.poke(input.U)
+
+ c.io.out.expect(goldenModelResult.U, s"i $i, input $input, gm $goldenModelResult, ${c.io.out.peek().litValue}")
+
+ c.clock.step(1)
+ }
+
+}
+
注意:这里软件上的Golden Model没有考虑位宽,而硬件则与位宽有很大关系。这里只考虑了8以内,即3bit的数,因此不存在这个问题
class MyManyElementFir(consts: Seq[Int], bitWidth: Int) extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(bitWidth.W))
+ val out = Output(UInt(bitWidth.W))
+ })
+
+ val regs = mutable.ArrayBuffer[UInt]()
+ for(i <- 0 until consts.length) {
+ if(i == 0) regs += io.in
+ else regs += RegNext(regs(i - 1), 0.U)
+ }
+
+ val muls = mutable.ArrayBuffer[UInt]()
+ for(i <- 0 until consts.length) {
+ muls += regs(i) * consts(i).U
+ }
+
+ val scan = mutable.ArrayBuffer[UInt]()
+ for(i <- 0 until consts.length) {
+ if(i == 0) scan += muls(i)
+ else scan += muls(i) + scan(i - 1)
+ }
+
+ io.out := scan.last
+}
+
val regs = mutable.ArrayBuffer[UInt]()
声明了一个名为regs
的不可变变量,它被初始化为一个可变的ArrayBuffer
,其中包含**UInt
类型的元素。ArrayBuffer
是一个可变的序列,允许在序列两端高效地添加或删除元素,适用于需要动态修改元素的场景。在这里,regs
可以被用来存储和更新UInt
类型的数据,但由于使用了val
,regs
本身的引用是不可变的,尽管它指向的ArrayBuffer
**内容是可变的。regs += io.in
这行代码的意思是将io.in
这个UInt
信号添加到regs
这个ArrayBuffer
中。这里没有直接的数值相加操作,而是将io.in
这个元素添加(追加)到regs
这个列表的末尾。regs
是一个容器,可以逐个添加元素,即使一开始regs
是空的。这行代码的作用是初始化regs
列表的第一个元素,后续元素则在循环中通过RegNext
**添加。regs
作为一个ArrayBuffer
,可以包含不同类型的**UInt
元素。在这种情况下,io.in
是一个Input(UInt)
类型,而RegNext(regs(i - 1), 0.U)
生成的是一个Reg(UInt)
类型。尽管io.in
和通过RegNext
创建的寄存器在硬件层面扮演不同的角色(一个是输入信号,另一个是寄存器),但它们都是UInt
类型,可以存储在同一个ArrayBuffer
**中。在Chisel生成的硬件逻辑中,这将创建一个信号和寄存器链,其中信号和寄存器可以互相连接。scan
数组缓存逐步累加的结果而不是直接对muls
求和,是为了展示在每一步如何逐渐累积计算的中间值。这种方法在某些复杂的FIR滤波器设计中可以提供更多的灵活性,比如在需要逐个访问累加过程中的中间结果时。尽管在这个特定例子中,只需要最终的累加结果,直接对muls
求和看似更直接,但展开累加过程可以帮助理解和调试滤波器的行为,尤其是在更复杂或参数化的设计中。然而,如果目标只是获取最终的累加和,直接使用muls.reduce(_ + _)
**确实会更简洁高效。def r(): Int = {
+ scala.util.Random.nextInt(1024)
+}
+
+/**
+ * run a test comparing software and hardware filters
+ * run for at least twice as many samples as taps
+ */
+def runOneTest(taps: Seq[Int]) {
+ val goldenModel = new ScalaFirFilter(taps)
+
+ test(new MyManyElementFir(taps, 32)) { c =>
+ for(i <- 0 until 2 * taps.length) {
+ val input = r()
+
+ val goldenModelResult = goldenModel.poke(input)
+
+ c.io.in.poke(input.U)
+
+ c.io.out.expect(goldenModelResult.U, s"i $i, input $input, gm $goldenModelResult, ${c.io.out.peek().litValue}")
+
+ c.clock.step(1)
+ }
+ }
+}
+
+for(tapSize <- 2 until 100 by 10) {
+ val taps = Seq.fill(tapSize)(r()) // create a sequence of random coefficients
+
+ runOneTest(taps)
+}
+
c.clock.step(1)
通常放在对输出进行期望检查(expect
)之后,因为我们希望在提供输入后推进仿真一个时钟周期,然后在下一个时钟边沿检查输出。这样可以确保寄存器已经更新到了因输入变化而触发的新状态。s"i $i, input $input, gm $goldenModelResult, ${c.io.out.peek().litValue}"
是Scala的字符串插值,用于构造包含变量值的字符串。这里它构建了一个描述当前测试状态的字符串,包括迭代次数i
,当前输入input
,金标准模型goldenModel
的结果goldenModelResult
,以及从待测试模块的输出c.io.out
**中提取的值。这对于调试和理解测试失败的上下文非常有用。在FIR生成器的IO中添加了一个额外的**consts
向量,允许在电路生成后从外部改变系数。这是通过Chisel集合类型Vec
实现的。Vec
支持许多Scala集合方法,但只能包含Chisel硬件元素。仅在普通Scala集合无法满足需求的情况下使用Vec
**,主要是以下两种情况:1. 在Bundle中需要元素集合,通常是作为IO使用的Bundle。2. 需要通过硬件部分的索引访问集合(如寄存器文件)。
原因在于**Vec
能够创建一组硬件元素的集合,而这些硬件元素可以在生成的硬件电路中被索引和操作。相反,普通的Scala集合,如List
或Seq
,仅仅在Scala软件环境中存在,它们不能直接映射到硬件电路中。因此,当定义硬件模块的IO接口或需要在硬件级别按索引访问元素时,应该使用Vec
**。
class MyManyDynamicElementVecFir(length: Int) extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(8.W))
+ val out = Output(UInt(8.W))
+ val consts = Input(Vec(length, UInt(8.W)))
+ })
+
+ // Reference solution
+ val regs = RegInit(VecInit(Seq.fill(length)(0.U(8.W))))
+ for(i <- 0 until length) {
+ if(i == 0) regs(i) := io.in
+ else regs(i) := regs(i - 1)
+ }
+
+ val muls = Wire(Vec(length, UInt(8.W)))
+ for(i <- 0 until length) {
+ if(i == 0) muls(i) := io.in * io.consts(i)
+ else muls(i) := regs(i - 1) * io.consts(i)
+ }
+
+ val scan = Wire(Vec(length, UInt(8.W)))
+ for(i <- 0 until length) {
+ if(i == 0) scan(i) := muls(i)
+ else scan(i) := muls(i) + scan(i - 1)
+ }
+
+ io.out := scan(length - 1)
+}
+
VecInit
用于创建一个Vec
,这是Chisel中的一种集合类型,专门用于存储硬件元素。Seq.fill(length - 1)(0.U(8.W))
生成一个长度为length - 1
,每个元素初始化为8位宽的0的序列。VecInit
将这个序列转换成一个Vec
,以便在硬件设计中使用。**RegInit
则将这个Vec
**初始化为寄存器,使得其值在复位时为指定的初始值。这种方式常用于定义具有多个初始相同值的寄存器数组。
Wire(Vec(length, UInt(8.W)))
用于创建一个具有 length
元素的向量,每个元素是 8 位无符号整数。这是在硬件描述语言中创建信号数组的标准方法,允许这些信号在生成的硬件电路中被实例化。
相比之下,mutable.ArrayBuffer[UInt]()
是 Scala 的一个集合类型,主要用于软件程序中的数据存储和处理。在 Chisel 的上下文中,你不能直接将 ArrayBuffer
用作硬件信号的容器,因为 ArrayBuffer
是一个可变的、仅在 Scala 软件执行环境中存在的数据结构,它不会被合成到硬件中。
简单来说:
Wire(Vec(length, UInt(8.W)))
在 Chisel 中创建一个硬件向量,这个向量可以在生成的硬件电路中存在并携带信号。mutable.ArrayBuffer[UInt]()
创建一个仅在 Scala 软件执行时存在的内存数组,它不能直接用于硬件设计。所以,在你的代码中使用 Wire(Vec(length, UInt(8.W)))
是为了定义一个可以在硬件层面操作和传递信号的向量,这对于硬件设计至关重要。
Register file: An array of registers that can be read from or written to via a number of read or write ports. Each port consists of an address and data field.
class RegisterFile(readPorts: Int) extends Module {
+ require(readPorts >= 0)
+ val io = IO(new Bundle {
+ val wen = Input(Bool())
+ val waddr = Input(UInt(5.W))
+ val wdata = Input(UInt(32.W))
+ val raddr = Input(Vec(readPorts, UInt(5.W)))
+ val rdata = Output(Vec(readPorts, UInt(32.W)))
+ })
+
+ // A Register of a vector of UInts
+ val reg = RegInit(VecInit(Seq.fill(32)(0.U(32.W))))
+
+ when(io.wen){
+ reg(io.waddr) := io.wdata
+ }
+ for(i <- 0 until readPorts){
+ when(io.raddr(i) === 0.U){
+ io.rdata(i) := 0.U
+ }.otherwise{
+ io.rdata(i) := reg(io.raddr(i))
+ }
+ }
+}
+
readPorts
表示寄存器文件的读端口数量。这个数量决定了在任何给定的时钟周期内,可以同时读取多少个独立寄存器的数据。每个读端口由其自己的读取地址(raddr
)和读取数据(rdata
)组成。在这种设置中,你可以在一个时钟周期内并行读取多个寄存器的值,而不是一次只能读取一个,这对于增加处理器的数据吞吐量非常有用。
具体到代码:
readPorts
:定义了有多少个并行的读端口可用于同时读取寄存器文件中的数据。io.raddr
:一个向量,包含了每个读端口对应的读取地址。每个读端口都可以独立地从寄存器文件中选择一个寄存器进行读取。io.rdata
:一个向量,用于输出每个读端口读取到的寄存器值。每个端口根据对应的**raddr
读取寄存器文件中的数据,并将其放置在rdata
**的相应位置。在 Chisel 中,DecoupledIO
是一种标准的准备就绪(ready-valid)接口,广泛用于不同模块间的数据传输,提供了一种带有流控制的通信机制。使用 DecoupledIO
可以有效地处理数据传输的同步问题,特别是在生产者(source)和消费者(sink)速率不匹配时,确保数据的正确传输与接收。
DecoupledIO
接口由以下三个主要部分组成:
valid
信号置为高电平。ready
信号置为高电平。UInt
或 Bool
到复杂的用户定义 Bundle
。在 DecoupledIO
接口中,数据传输在以下条件下发生:
valid
为高电平,表示其有数据要发送。ready
为高电平,表示其准备好接收数据。当且仅当同一时钟周期内 valid
和 ready
同时为高电平时,数据才会被传输。这允许在接收端或发送端任一端控制数据流,实现背压(backpressure)机制:
ready
为低),即使发送端有数据发送(valid
为高),数据也不会被传输。valid
为低),即使接收端准备好了(ready
为高),也不会有数据传输。DecoupledIO
非常适用于生产者和消费者速率不一致的情况,如:
以下是如何在 Chisel 中创建一个 DecoupledIO
接口的简单示例:
val data = UInt(8.W) // 定义数据宽度
+val decoupledData = Decoupled(data) // 创建 DecoupledIO 接口
+
这里,decoupledData
将是一个拥有 valid
、ready
和 bits
字段的 DecoupledIO
Bundle,可用于模块间的数据传输。
test(new Module {
+ // Example circuit using a Queue
+ val io = IO(new Bundle {
+ val in = Flipped(Decoupled(UInt(8.W)))
+ val out = Decoupled(UInt(8.W))
+ })
+ val queue = Queue(io.in, 2) // 2-element queue
+ io.out <> queue
+ }) { c =>
+ c.io.out.ready.poke(false.B)
+ c.io.in.valid.poke(true.B) // Enqueue an element
+ c.io.in.bits.poke(42.U)
+ println(s"Starting:")
+ println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
+ println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
+ c.clock.step(1)
+
+ c.io.in.valid.poke(true.B) // Enqueue another element
+ c.io.in.bits.poke(43.U)
+ // What do you think io.out.valid and io.out.bits will be?
+ println(s"After first enqueue:")
+ println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
+ println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
+ c.clock.step(1)
+
+ c.io.in.valid.poke(true.B) // Read a element, attempt to enqueue
+ c.io.in.bits.poke(44.U)
+ c.io.out.ready.poke(true.B)
+ // What do you think io.in.ready will be, and will this enqueue succeed, and what will be read?
+ println(s"On first read:")
+ println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
+ println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
+ c.clock.step(1)
+
+ c.io.in.valid.poke(false.B) // Read elements out
+ c.io.out.ready.poke(true.B)
+ // What do you think will be read here?
+ println(s"On second read:")
+ println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
+ println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
+ c.clock.step(1)
+
+ // Will a third read produce anything?
+ println(s"On third read:")
+ println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
+ println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
+ c.clock.step(1)
+}
+
Starting:
+ io.in: ready=1
+ io.out: valid=0, bits=0
+After first enqueue:
+ io.in: ready=1
+ io.out: valid=1, bits=42
+On first read:
+ io.in: ready=0
+ io.out: valid=1, bits=42
+On second read:
+ io.in: ready=1
+ io.out: valid=1, bits=43
+On third read:
+ io.in: ready=1
+ io.out: valid=0, bits=42
+
**val in = Flipped(Decoupled(UInt(8.W)))**
这里的**Flipped**
表示是**Input**
,因为**Decoupled**
默认是**Output**
**val queue = Queue(io.in, 2)**
连接 io.in
到队列的输入端: 这意味着 io.in
上发生的任何事情(比如信号变化)都会直接影响到队列。具体来说,当您在测试代码中设置 io.in.valid
和 io.in.bits
,您实际上是在向队列的入队端提供数据。
io.out <> queue
时,你实际上是在将模块的输出接口 io.out
和队列 queue
的出队接口双向连接。
具体来说,对于 DecoupledIO
接口:
io.out.valid
会连接到 queue.io.deq.valid
。io.out.bits
会连接到 queue.io.deq.bits
。queue.io.deq.ready
会连接到 io.out.ready
。这种连接方式确保了数据可以从 queue
的出队端传输到模块的输出 io.out
,同时允许 io.out
控制背压(通过 ready
信号)以及 queue
报告其状态(通过 valid
信号)。
开始时:
io.out.ready
被设为 false
,表示消费者还没准备好接收数据。io.in.valid
被设为 true
,并通过 io.in.bits
提供了值 42,尝试将其入队。io.in.ready
应该为 true
(队列未满,可以接收数据),而 io.out.valid
应该为 false
(数据尚未出队到输出端)。首次入队后:
io.in.ready
仍然为 true
,表明还可以接收更多数据。io.out.valid
被设置为 true
并且 io.out.bits
被更新为 42,因为队列内部确实有一个元素(42)准备好了并且尝试发送。即使 io.out.ready
是 false
,io.out.valid
和 io.out.bits
仍然会反映队列出口处的数据状态。io.out.valid
为 true
并且 io.out.bits
显示了数据(42),但由于 io.out.ready
为 false
,这次数据传输并不会真正完成。换句话说,42 在逻辑上是"准备发送"的,但由于缺乏接收准备就绪的确认(即 io.out.ready
为 false
),它实际上并没有"被接收"。首次读取尝试:
io.out.ready
设为 true
)和入队(值 44)。io.out
,io.out.valid
应变为 true
,io.out.bits
应显示 42。io.in.ready
为 0
。这是因为队列(queue
)此时已经满了。第二次读取:
io.out
准备好读取数据。io.out.valid
应为 true
,io.out.bits
应显示 43。第三次读取尝试:
io.out
依然准备好接收数据,但队列应该已经空了。io.out.valid
应该变回 false
,表示没有更多数据可读。注:
peek()
函数用于查看信号的当前值,而 .litValue
用于获取这个值作为一个 Scala 的字面量(literal)。因此,c.io.in.ready.peek().litValue
表示查看 c.io.in.ready
信号的当前值,并获取其字面量值。**c.clock.step(1)**
以后才会有输出一个 Arbiter 是用于解决多个请求源争用单一资源的组件。它的基本功能是在多个输入信号中选择一个进行输出,基于某种特定的优先级或策略。在处理多个并发请求访问同一资源(例如,总线或共享内存)时,仲裁器确保每个时刻只有一个选定的请求被服务,同时遵循公平性或优先级规则,避免资源冲突或死锁。
Arbiter
:这是一个静态优先级仲裁器,它总是优先选择索引较低的生产者。如果有多个请求同时到达,Arbiter
会根据请求信号的索引顺序来决定优先权,索引较小的请求者会获得优先服务。这意味着如果较低索引的请求者持续有请求,它将持续获得资源,而更高索引的请求者则需要等待。
示例代码如下:
test(new Module {
+ // Example circuit using a priority arbiter
+ val io = IO(new Bundle {
+ val in = Flipped(Vec(2, Decoupled(UInt(8.W))))
+ val out = Decoupled(UInt(8.W))
+ })
+ // Arbiter doesn't have a convenience constructor, so it's built like any Module
+ val arbiter = Module(new Arbiter(UInt(8.W), 2)) // 2 to 1 Priority Arbiter
+ arbiter.io.in <> io.in
+ io.out <> arbiter.io.out
+ }) { c =>
+ c.io.in(0).valid.poke(false.B)
+ c.io.in(1).valid.poke(false.B)
+ c.io.out.ready.poke(false.B)
+ println(s"Start:")
+ println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
+ println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
+ c.io.in(1).valid.poke(true.B) // Valid input 1
+ c.io.in(1).bits.poke(42.U)
+ c.io.out.ready.poke(true.B)
+ // What do you think the output will be?
+ println(s"valid input 1:")
+ println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
+ println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
+ c.io.in(0).valid.poke(true.B) // Valid inputs 0 and 1
+ c.io.in(0).bits.poke(43.U)
+ // What do you think the output will be? Which inputs will be ready?
+ println(s"valid inputs 0 and 1:")
+ println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
+ println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
+ c.io.in(1).valid.poke(false.B) // Valid input 0
+ // What do you think the output will be?
+ println(s"valid input 0:")
+ println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
+ println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
+}
+
Start:
+ in(0).ready=0, in(1).ready=0
+ out.valid=0, out.bits=0
+valid input 1:
+ in(0).ready=1, in(1).ready=1
+ out.valid=1, out.bits=42
+valid inputs 0 and 1:
+ in(0).ready=1, in(1).ready=0
+ out.valid=1, out.bits=43
+valid input 0:
+ in(0).ready=1, in(1).ready=0
+ out.valid=1, out.bits=43
+
初始状态检查:
io.in(0).valid
和 io.in(1).valid
都为 false
) 时,输出 (io.out.valid
) 也应为 false
,表示没有数据通过仲裁器。激活第二个输入:
然后,测试激活 io.in(1)
(即索引为 1 的输入),同时保持 io.out.ready
为 true
,这模拟了接收端准备好接收数据的情况。预期 io.in(1)
的数据应该通过到 io.out
。
两个输入的 ready
信号都是 1
。这是因为 Arbiter
的行为是基于它可以传递数据的能力。让我们分解这个情况:
当只有 io.in(1)
有效时:
in(0).ready=1
:这意味着 Arbiter
仲裁器准备好从 io.in(0)
接收数据,尽管此时 io.in(0)
没有数据(不是有效的)。ready
信号为 1
表示如果 io.in(0)
有数据,Arbiter
准备好接收它。in(1).ready=1
:同时,Arbiter
也表示准备好接收 io.in(1)
的数据,因为它是有效的。在这种情况下,由于 io.out.ready
被设置为 true
,Arbiter
知道输出端已经准备好接收数据,所以它将 io.in(1)
的 ready
也置为 1
。这里的关键是 ready
信号表示的是接收能力而非当前的数据有效性。即使 io.in(0)
当前没有标记为有效,Arbiter
也表明它准备好从该输入接收数据,这就是为什么在 io.in(1)
有效时,io.in(0).ready
也会是 1
。
同时激活两个输入:
io.in(0)
和 io.in(1)
都设置为有效。由于 Arbiter
的静态优先级策略,预期 io.in(0)
的数据(即优先级更高的输入)会被传递到 io.out
。io.in(0)
和 io.in(1)
都被设置为有效时,Arbiter
会根据其内部逻辑来选择一个输入。对于普通的 Arbiter
,它将优先选择索引较低的输入,即 io.in(0)
。io.in(0)
被选中,io.in(1)
的 ready
信号将会被置为 false
,而 io.in(0).ready
会是 true
,表示 Arbiter
准备接受 io.in(0)
的数据。io.in(1).bits
,这个数据也不会被 Arbiter
选择,因为 io.in(0)
有更高的优先级。只激活第一个输入:
io.in(0)
。即使之前 io.in(1)
被激活过,在这一步中只有 io.in(0)
有效,所以只有它的数据应该被传递到 io.out
。RRArbiter
:这是一个循环(round-robin)仲裁器,它按照循环的顺序为请求者提供服务,确保了长期的公平性。当一个请求被服务后,RRArbiter
会记住最后被服务的请求,并在下一个服务周期中优先考虑下一个请求者。这样可以确保即使在高负载下,所有请求者也能获得均等的服务机会。
示例代码如下:
val rrArbiter = Module(new RRArbiter(UInt(8.W), 2))
+rrArbiter.io.in(0) <> producer0
+rrArbiter.io.in(1) <> producer1
+consumer <> rrArbiter.io.out
+
注:Ariter 是组合电路,不需要step(1)
PopCount returns the number of high (1) bits in the input as a **UInt**
.
test(new Module {
+ // Example circuit using PopCount
+ val io = IO(new Bundle {
+ val in = Input(UInt(8.W))
+ val out = Output(UInt(8.W))
+ })
+ io.out := PopCount(io.in)
+ }) { c =>
+ // Integer.parseInt is used create an Integer from a binary specification
+ c.io.in.poke(**Integer.parseInt**("00000000", 2).U)
+ println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
+
+ c.io.in.poke(Integer.parseInt("00001111", 2).U)
+ println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
+
+ c.io.in.poke(Integer.parseInt("11001010", 2).U)
+ println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
+
+ c.io.in.poke(Integer.parseInt("11111111", 2).U)
+ println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
+
+}
+
in=0b0, out=0
+in=0b1111, out=4
+in=0b11001010, out=4
+in=0b11111111, out=8
+
Reverse returns the bit-reversed input
test(new Module {
+ // Example circuit using Reverse
+ val io = IO(new Bundle {
+ val in = Input(UInt(8.W))
+ val out = Output(UInt(8.W))
+ })
+ io.out := Reverse(io.in)
+ }) { c =>
+ // Integer.parseInt is used create an Integer from a binary specification
+ c.io.in.poke(Integer.parseInt("01010101", 2).U)
+ println(s"in=0b${**c.io.in.peek().litValue.toInt.toBinaryString**}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
+
+ c.io.in.poke(Integer.parseInt("00001111", 2).U)
+ println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
+
+ c.io.in.poke(Integer.parseInt("11110000", 2).U)
+ println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
+
+ c.io.in.poke(Integer.parseInt("11001010", 2).U)
+ println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
+}
+
in=0b1010101, out=0b10101010
+in=0b1111, out=0b11110000
+in=0b11110000, out=0b1111
+in=0b11001010, out=0b1010011
+
UIntToOH
UInt to OneHot
test(new Module {
+ // Example circuit using UIntToOH
+ val io = IO(new Bundle {
+ val in = Input(UInt(4.W))
+ val out = Output(UInt(16.W))
+ })
+ io.out := UIntToOH(io.in)
+ }) { c =>
+ c.io.in.poke(0.U)
+ println(s"in=${c.io.in.peek().litValue}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
+
+ c.io.in.poke(1.U)
+ println(s"in=${c.io.in.peek().litValue}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
+
+ c.io.in.poke(8.U)
+ println(s"in=${c.io.in.peek().litValue}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
+
+ c.io.in.poke(15.U)
+ println(s"in=${c.io.in.peek().litValue}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
+}
+
+in=0, out=0b1
+in=1, out=0b10
+in=8, out=0b100000000
+in=15, out=0b1000000000000000
+
OHToUInt
OneHot to UInt
test(new Module {
+ // Example circuit using OHToUInt
+ val io = IO(new Bundle {
+ val in = Input(UInt(16.W))
+ val out = Output(UInt(4.W))
+ })
+ io.out := OHToUInt(io.in)
+}) { c =>
+ c.io.in.poke(Integer.parseInt("0000 0000 0000 0001".replace(" ", ""), 2).U)
+ println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
+
+ c.io.in.poke(Integer.parseInt("0000 0000 1000 0000".replace(" ", ""), 2).U)
+ println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
+
+ c.io.in.poke(Integer.parseInt("1000 0000 0000 0001".replace(" ", ""), 2).U)
+ println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
+
+ // Some invalid inputs:
+ // None high
+ c.io.in.poke(Integer.parseInt("0000 0000 0000 0000".replace(" ", ""), 2).U)
+ println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
+
+ // Multiple high
+ c.io.in.poke(Integer.parseInt("0001 0100 0010 0000".replace(" ", ""), 2).U)
+ println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
+}
+
+in=0b1, out=0
+in=0b10000000, out=7
+in=0b1000000000000001, out=15
+in=0b0, out=0
+in=0b1010000100000, out=15
+
**PriorityMux**
Outputs the value associated with the lowest-index asserted select signal.
test(new Module {
+ // Example circuit using PriorityMux
+ val io = IO(new Bundle {
+ val in_sels = Input(Vec(2, Bool()))
+ val in_bits = Input(Vec(2, UInt(8.W)))
+ val out = Output(UInt(8.W))
+ })
+ io.out := PriorityMux(io.in_sels, io.in_bits)
+ }) { c =>
+ c.io.in_bits(0).poke(10.U)
+ c.io.in_bits(1).poke(20.U)
+
+ // Select higher index only
+ c.io.in_sels(0).poke(false.B)
+ c.io.in_sels(1).poke(true.B)
+ println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
+
+ // Select both - arbitration needed
+ c.io.in_sels(0).poke(true.B)
+ c.io.in_sels(1).poke(true.B)
+ println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
+
+ // Select lower index only
+ c.io.in_sels(0).poke(true.B)
+ c.io.in_sels(1).poke(false.B)
+ println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
+}
+
+in_sels=0, out=20
+in_sels=1, out=10
+in_sels=1, out=10
+
PriorityMux
会根据 in_sels
中的布尔值,从左到右(即从索引 0 开始)检查哪个输入是选中的,并输出第一个选中输入对应的 in_bits
值。因此,同时使 in_sels(0)
和 in_sels(1)
为 true
时,由于 in_sels(0)
的优先级更高,io.out
应该输出 in_bits(0)
的值,即 10.U
。
**Mux1H**
An **Mux1H**
provides an efficient implementation when it is guaranteed that exactly one of the select signals will be high. Behavior is undefined if the assumption is not true.
test(new Module {
+ // Example circuit using Mux1H
+ val io = IO(new Bundle {
+ val in_sels = Input(Vec(2, Bool()))
+ val in_bits = Input(Vec(2, UInt(8.W)))
+ val out = Output(UInt(8.W))
+ })
+ io.out := Mux1H(io.in_sels, io.in_bits)
+ }) { c =>
+ c.io.in_bits(0).poke(10.U)
+ c.io.in_bits(1).poke(20.U)
+
+ // Select index 1
+ c.io.in_sels(0).poke(false.B)
+ c.io.in_sels(1).poke(true.B)
+ println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
+
+ // Select index 0
+ c.io.in_sels(0).poke(true.B)
+ c.io.in_sels(1).poke(false.B)
+ println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
+
+ // Select none (invalid)
+ c.io.in_sels(0).poke(false.B)
+ c.io.in_sels(1).poke(false.B)
+ println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
+
+ // Select both (invalid)
+ c.io.in_sels(0).poke(true.B)
+ c.io.in_sels(1).poke(true.B)
+ println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
+}
+
+in_sels=0, out=20
+in_sels=1, out=10
+in_sels=0, out=0
+in_sels=1, out=30
+
**Counter**
A counter that can be incremented once every cycle, up to some specified limit, at which point it overflows. Note that it is not a Module, and its value is accessible.
test(new Module {
+ // Example circuit with two counters
+ val io = IO(new Bundle {
+ val count = Input(Bool())
+ val out = Output(UInt(2.W))
+ val totalCycles = Output(UInt(32.W)) // Assuming 32-bit is enough for cycle count
+ })
+
+ // Counter for controlled increments
+ val controlledCounter = Counter(3) // 3-count Counter (outputs range [0...2])
+ when(io.count) {
+ controlledCounter.inc()
+ }
+ io.out := controlledCounter.value
+
+ // Counter for total cycles, counting up to (2^32)-1
+ val totalCycleCounter = Counter(math.pow(2, 32).toInt)
+ totalCycleCounter.inc() // Increment every cycle
+ io.totalCycles := totalCycleCounter.value
+}) { c =>
+ c.io.count.poke(true.B)
+ println(s"start: controlled counter value=${c.io.out.peek().litValue}, total cycles=${c.io.totalCycles.peek().litValue}")
+
+ c.clock.step(1)
+ println(s"step 1: controlled counter value=${c.io.out.peek().litValue}, total cycles=${c.io.totalCycles.peek().litValue}")
+
+ c.clock.step(1)
+ println(s"step 2: controlled counter value=${c.io.out.peek().litValue}, total cycles=${c.io.totalCycles.peek().litValue}")
+
+ c.io.count.poke(false.B)
+ c.clock.step(1)
+ println(s"step without increment: controlled counter value=${c.io.out.peek().litValue}, total cycles=${c.io.totalCycles.peek().litValue}")
+
+ c.io.count.poke(true.B)
+ c.clock.step(1)
+ println(s"step again: controlled counter value=${c.io.out.peek().litValue}, total cycles=${c.io.totalCycles.peek().litValue}")
+}
+
+start: controlled counter value=0, total cycles=0
+step 1: controlled counter value=1, total cycles=1
+step 2: controlled counter value=2, total cycles=2
+step without increment: controlled counter value=2, total cycles=3
+step again: controlled counter value=0, total cycles=4
+
在 Chisel 中创建一个计数器时,如果您传递的参数是 n
,那么计数器能够表示的计数范围是从0到 n-1。所以,当您使用 Counter(n)
时,计数器的实际最大计数值是 n-1。
From the last module, we had the convolution part of the FIR filter written like this:
val muls = Wire(Vec(length, UInt(8.W)))
+for(i <- 0 until length) {
+ if(i == 0) muls(i) := io.in * io.consts(i)
+ else muls(i) := regs(i - 1) * io.consts(i)
+}
+
+val scan = Wire(Vec(length, UInt(8.W)))
+for(i <- 0 until length) {
+ if(i == 0) scan(i) := muls(i)
+ else scan(i) := muls(i) + scan(i - 1)
+}
+
+io.out := scan(length - 1)
+
As a recap, the idea is to multiply each element of **io.in**
with the corresponding element of **io.consts**
, and store it in **muls**
. Then, the elements in **muls**
are accumulated into **scan**
, with **scan(0) = muls(0)**
, **scan(1) = scan(0) + muls(1) = muls(0) + muls(1)**
, and in general **scan(n) = scan(n-1) + muls(n) = muls(0) + ... + muls(n-1) + muls(n)**
. The last element in **scan**
(equal to the sum of all **muls**
) is assigned to **io.out**
.
However, it's very verbose for what might be considered quite a simple operation. In fact, all that could be written in one line:
io.out := (taps zip io.consts).map { case (a, b) => a * b }.reduce(_ + _)
+
taps
是所有样本的列表,其中 taps(0) = io.in
,taps(1) = regs(0)
等等。(taps zip io.consts)
将两个列表 taps
和 io.consts
合并成一个列表,其中每个元素是一个元组,这个元组包含了在相应位置的输入元素。具体来说,它的值将是 [(taps(0), io.consts(0)), (taps(1), io.consts(1)), ..., (taps(n), io.consts(n))]
。记住,在 Scala 中点号是可选的,所以这等同于 (taps.zip(io.consts))
。.map { case (a, b) => a * b }
对列表中的元素应用一个匿名函数(接收两个元素的元组并返回它们的乘积),并返回结果。在这个情况下,结果等价于在冗长示例中的 muls
,其值为 [taps(0) * io.consts(0), taps(1) * io.consts(1), ..., taps(n) * io.consts(n)]
。.reduce(_ + _)
同样应用一个函数(元素的加法)到列表的元素上。然而,它接收两个参数:第一个是当前的累加值,第二个是列表元素(在第一次迭代中,它只是将前两个元素相加)。这些由括号中的两个下划线表示。那么结果,假设是从左到右的遍历,将会是 (((muls(0) + muls(1)) + muls(2)) + ...) + muls(n)
,更深层次括号内的结果先被计算。这就是卷积的输出结果。Formally, functions like **map**
and **reduce**
are called higher-order functions : they are functions that take functions as arguments. As it turns out (and hopefully, as you can see from the above example), these are very powerful constructs that encapsulate a general computational pattern, allowing you to concentrate on the application logic instead of flow control, and resulting in very concise code.
_
)来引用每个参数。在上面的例子中,reduce
函数接受两个参数,可以被指定为 _ + _
。虽然这很方便,但它受制于一组额外的复杂规则,所以如果不起作用,您可以尝试:reduce
可以被明确写成 (a, b) => a + b
,通用形式是把参数列表放在括号里,后面跟着 =>
,然后是引用这些参数的函数体。case
语句,如 case (a, b) => a * b
。这接收一个参数,一个两个元素的元组,并将其解包到变量 a
和 b
中,然后可以在函数体中使用它们。Scala 集合 API 中的主要类,如**List**
。这些高阶函数是这些 API 的一部分。实际上,上面的示例使用了**List**
上的**map**
和**reduce**
API。在这一节中,我们将通过示例和练习熟悉这些方法。在这些示例中,我们将对 Scala 数字(**Int**
)操作,以简化和明确表示,但因为 Chisel 运算符的行为类似,所以这些概念应该是通用的。
List[A].map
有类型签名 map[B](f: (A) ⇒ B): List[B]
。现在,将类型 A 和 B 视为 Int
或 UInt
,意味着它们可以是软件或硬件类型。它接受一个类型为 (f: (A) ⇒ B)
的参数,或者一个接受类型为 A(与输入列表的元素类型相同)的一个参数并返回类型为 B 的值的函数。map
然后返回一个新的类型为 B(参数函数的返回类型)的列表。
println(List(1, 2, 3, 4).map(x => x + 1)) // explicit argument list in function
+println(List(1, 2, 3, 4).map(_ + 1)) // equivalent to the above, but implicit arguments
+println(List(1, 2, 3, 4).map(_.toString + "a")) // the output element type can be different from the input element type
+
+println(List((1, 5), (2, 6), (3, 7), (4, 8)).map { case (x, y) => x*y }) // this unpacks a tuple, note use of curly braces
+
+// Related: Scala has a syntax for constructing lists of sequential numbers
+println(0 to 10) // to is inclusive , the end point is part of the result
+println(0 until 10) // until is exclusive at the end, the end point is not part of the result
+
+// Those largely behave like lists, and can be useful for generating indices:
+val myList = List("a", "b", "c", "d")
+println((0 until 4).map(myList(_)))
+
+// output:
+List(2, 3, 4, 5)
+List(2, 3, 4, 5)
+List(1a, 2a, 3a, 4a)
+List(5, 12, 21, 32)
+Range 0 to 10
+Range 0 until 10
+Vector(a, b, c, d)
+
**zipWithIndex**
**List.zipWithIndex**
has type signature **zipWithIndex: List[(A, Int)]**
. It takes no arguments, but returns a list where each element is a tuple of the original elements, and the index (with the first one being zero). So **List("a", "b", "c", "d").zipWithIndex**
would return **List(("a", 0), ("b", 1), ("c", 2), ("d", 3))**
println(List(1, 2, 3, 4).zipWithIndex) // note indices start at zero
+println(List("a", "b", "c", "d").zipWithIndex)
+println(List(("a", "b"), ("c", "d"), ("e", "f"), ("g", "h")).zipWithIndex) // tuples nest
+
+// output:
+List((1,0), (2,1), (3,2), (4,3))
+List((a,0), (b,1), (c,2), (d,3))
+List(((a,b),0), ((c,d),1), ((e,f),2), ((g,h),3))
+
**reduce**
List[A].map
has type signature similar to reduce(op: (A, A) ⇒ A): A
. (it's actually more lenient, A
only has to be a supertype of the List type, but we're not going to deal with that syntax here)
println(List(1, 2, 3, 4).reduce((a, b) => a + b)) // returns the sum of all the elements
+println(List(1, 2, 3, 4).reduce(_ * _)) // returns the product of all the elements
+println(List(1, 2, 3, 4).map(_ + 1).reduce(_ + _)) // you can chain reduce onto the result of a map
+println(List(1, 2, 3, 4).map(_*2).reduce(_*_)) // returns the product of the double of the elements of the input list.
+
+// output:
+10
+24
+14
+384
+
+// Important note: reduce will fail with an empty list
+println(List[Int]().reduce(_ * _))
+
**fold**
**List[A].fold**
is very similar to reduce, except that you can specify the initial accumulation value. It has type signature similar to **fold(z: A)(op: (A, A) ⇒ A): A**
. (like **reduce**
, the type of **A**
is also more lenient). Notably, it takes two argument lists, the first (**z**
) is the initial value, and the second is the accumulation function. Unlike **reduce**
, it will not fail with an empty list, instead returning the initial value directly.
println(List(1, 2, 3, 4).fold(0)(_ + _)) // equivalent to the sum using reduce
+println(List(1, 2, 3, 4).fold(1)(_ + _)) // like above, but accumulation starts at 1
+println(List().fold(1)(_ + _)) // unlike reduce, does not fail on an empty input
+println(List(1, 2, 3, 4).fold(2)(_*_)) // returns the double the product of the elements of the input list
+
+// output:
+10
+11
+1
+48
+
/*
+ * @lc app=leetcode.cn id=752 lang=cpp
+ *
+ * [752] 打开转盘锁
+ *
+ * https://leetcode.cn/problems/open-the-lock/description/
+ *
+ * algorithms
+ * Medium (52.77%)
+ * Likes: 653
+ * Dislikes: 0
+ * Total Accepted: 128.3K
+ * Total Submissions: 243.1K
+ * Testcase Example: '["0201","0101","0102","1212","2002"]\n"0202"'
+ *
+ * 你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8',
+ * '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0','0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。
+ *
+ * 锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。
+ *
+ * 列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
+ *
+ * 字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1 。
+ *
+ *
+ *
+ * 示例 1:
+ *
+ *
+ * 输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
+ * 输出:6
+ * 解释:
+ * 可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
+ * 注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
+ * 因为当拨动到 "0102" 时这个锁就会被锁定。
+ *
+ *
+ * 示例 2:
+ *
+ *
+ * 输入: deadends = ["8888"], target = "0009"
+ * 输出:1
+ * 解释:把最后一位反向旋转一次即可 "0000" -> "0009"。
+ *
+ *
+ * 示例 3:
+ *
+ *
+ * 输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"],
+ * target = "8888"
+ * 输出:-1
+ * 解释:无法旋转到目标数字且不被锁定。
+ *
+ *
+ *
+ *
+ * 提示:
+ *
+ *
+ * 1 <= deadends.length <= 500
+ * deadends[i].length == 4
+ * target.length == 4
+ * target 不在 deadends 之中
+ * target 和 deadends[i] 仅由若干位数字组成
+ *
+ *
+ */
+
class Solution
+{
+public:
+ int openLock(vector<string> &deadends, string target)
+ {
+ int num = 0;
+ queue<string> q;
+ set<string> visited(deadends.begin(), deadends.end());
+ if (visited.find("0000") != visited.end())
+ return -1;
+ q.push("0000");
+ visited.insert("0000");
+ while (!q.empty())
+ {
+ int ssize = q.size();
+ for (int i = 0; i < ssize; i++)
+ {
+ string str = q.front();
+ q.pop();
+ // if (visited.find(str) != visited.end())
+ // continue;
+ if (str == target)
+ return num;
+ for (int j = 0; j < 4; j++)
+ {
+ string up = turnUp(str, j);
+ string down = turnDown(str, j);
+ if (visited.find(up) == visited.end())
+ {
+ q.push(up);
+ visited.insert(up);
+ }
+ if (visited.find(down) == visited.end())
+ {
+ q.push(down);
+ visited.insert(down);
+ }
+ }
+ }
+ num++;
+ }
+ return -1;
+ }
+ string turnUp(string str, int index)
+ {
+ char old = str[index];
+ char nnew = (old != '9') ? (old + 1) : '0';
+ string newString = str;
+ newString[index] = nnew;
+ return newString;
+ }
+ string turnDown(string str, int index)
+ {
+ char old = str[index];
+ char nnew = (old != '0') ? (old - 1) : '9';
+ string newString = str;
+ newString[index] = nnew;
+ return newString;
+ }
+};
+
string
的某一个字符直接使用[]
索引修改即可deadends
**中的字符串以及已经访问过的字符串应该在尝试加入队列之前就被过滤掉,以防止它们被进一步处理。而不是从队列中拿出来的时候检查是不是在visited
中“0000”
也要检查int openLock(vector<string> &deadends, string target)
+ {
+ unordered_set<string> dead(deadends.begin(), deadends.end());
+ unordered_set<string> begin, end, *set1, *set2;
+
+ if (dead.find("0000") != dead.end() || dead.find(target) != dead.end())
+ return -1;
+
+ int step = 0;
+ begin.insert("0000");
+ end.insert(target);
+
+ while (!begin.empty() && !end.empty())
+ {
+ // 优化搜索,总是从较小的集合开始扩展
+ if (begin.size() > end.size())
+ {
+ set1 = &end;
+ set2 = &begin;
+ }
+ else
+ {
+ set1 = &begin;
+ set2 = &end;
+ }
+ unordered_set<string> temp;
+ for (auto it = set1->begin(); it != set1->end(); ++it)
+ {
+ string str = *it;
+ if (set2->find(str) != set2->end())
+ return step;
+ if (dead.find(str) != dead.end())
+ continue;
+ dead.insert(str); // 标记为已访问
+ for (int j = 0; j < 4; ++j)
+ {
+ string up = turnUp(str, j);
+ string down = turnDown(str, j);
+ if (dead.find(up) == dead.end())
+ temp.insert(up);
+ if (dead.find(down) == dead.end())
+ temp.insert(down);
+ }
+ }
+ step++;
+ *set1 = temp; // 更新当前正在扩展的集合
+ }
+ return -1; // 如果没有找到有效路径
+ }
+
temp
)的原因是在每一轮搜索中,我们需要更新当前层次的节点。由于在遍历当前层次的节点时不能直接修改正在遍历的集合(这会影响迭代器的有效性),因此我们先将新发现的节点存储在一个临时集合中。在当前层次的所有节点都遍历完毕后,我们再用这个临时集合来更新主集合,为下一轮搜索做准备。+ 2024.03.05-二叉树 + + → +
/*
+ * @lc app=leetcode.cn id=104 lang=cpp
+ *
+ * [104] 二叉树的最大深度
+ *
+ * https://leetcode.cn/problems/maximum-depth-of-binary-tree/description/
+ *
+ * algorithms
+ * Easy (77.10%)
+ * Likes: 1786
+ * Dislikes: 0
+ * Total Accepted: 1.2M
+ * Total Submissions: 1.6M
+ * Testcase Example: '[3,9,20,null,null,15,7]'
+ *
+ * 给定一个二叉树 root ,返回其最大深度。
+ *
+ * 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
+ *
+ *
+ *
+ * 示例 1:
+ *
+ *
+ *
+ *
+ *
+ *
+ * 输入:root = [3,9,20,null,null,15,7]
+ * 输出:3
+ *
+ *
+ * 示例 2:
+ *
+ *
+ * 输入:root = [1,null,2]
+ * 输出:2
+ *
+ *
+ *
+ *
+ * 提示:
+ *
+ *
+ * 树中节点的数量在 [0, 10^4] 区间内。
+ * -100 <= Node.val <= 100
+ *
+ *
+ */
+
+// @lc code=start
+/**
+ * Definition for a binary tree node.
+ * struct TreeNode {
+ * int val;
+ * TreeNode *left;
+ * TreeNode *right;
+ * TreeNode() : val(0), left(nullptr), right(nullptr) {}
+ * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
+ * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
+ * };
+ */
+// @lc code=end
+
class Solution
+{
+public:
+ int res;
+ int maxDepth(TreeNode *root)
+ {
+ if (root == nullptr)
+ return 0;
+ int tempLeft = maxDepth(root->left);
+ int tempRight = maxDepth(root->right);
+ return res = max(tempLeft, tempRight) + 1;
+ }
+};
+
class Solution {
+public:
+ int res, temp;
+ int maxDepth(TreeNode* root) {
+ traverse(root);
+ return res;
+ }
+private:
+ void traverse(TreeNode* node){
+ if(node == nullptr){
+ res = max(res,temp);
+ return;
+ }
+ temp++;
+ traverse(node->left);
+ traverse(node->right);
+ temp--;
+ }
+};
+
/*
+ * @lc app=leetcode.cn id=543 lang=cpp
+ *
+ * [543] 二叉树的直径
+ *
+ * https://leetcode.cn/problems/diameter-of-binary-tree/description/
+ *
+ * algorithms
+ * Easy (59.72%)
+ * Likes: 1495
+ * Dislikes: 0
+ * Total Accepted: 387.9K
+ * Total Submissions: 649.5K
+ * Testcase Example: '[1,2,3,4,5]'
+ *
+ * 给你一棵二叉树的根节点,返回该树的 直径 。
+ *
+ * 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。
+ *
+ * 两节点之间路径的 长度 由它们之间边数表示。
+ *
+ *
+ *
+ * 示例 1:
+ *
+ *
+ * 输入:root = [1,2,3,4,5]
+ * 输出:3
+ * 解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
+ *
+ *
+ * 示例 2:
+ *
+ *
+ * 输入:root = [1,2]
+ * 输出:1
+ *
+ *
+ *
+ *
+ * 提示:
+ *
+ *
+ * 树中节点数目在范围 [1, 10^4] 内
+ * -100 <= Node.val <= 100
+ *
+ *
+ */
+
+// @lc code=start
+/**
+ * Definition for a binary tree node.
+ * struct TreeNode {
+ * int val;
+ * TreeNode *left;
+ * TreeNode *right;
+ * TreeNode() : val(0), left(nullptr), right(nullptr) {}
+ * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
+ * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
+ * };
+ */
+class Solution {
+public:
+ int res = 0;
+ int diameterOfBinaryTree(TreeNode* root) {
+ traverse(root,res);
+ return res;
+ }
+ // cal the maxDepth
+ int traverse(TreeNode* node,int& res){
+ if(node == nullptr){
+ return 0;
+ }
+ int leftMax = traverse(node->left,res);
+ int rightMax = traverse(node->right,res);
+ int nodeMax = max(leftMax,rightMax)+1;
+ res = max(res,leftMax+rightMax);
+ return nodeMax;
+ }
+};
+// @lc code=end
+