+ 2. 2024.02.21-Chisel + + → +
diff --git a/404.html b/404.html new file mode 100644 index 0000000..09f284d --- /dev/null +++ b/404.html @@ -0,0 +1,20 @@ + + +
+ + ++ ← + + 1. 一生一芯计划 + + 3. 2023.11.07-Verilog语法 + + → +
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 个不同的标识符*
+
+ 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
+
+ ← + + 1. Verilog 基础语法 + + 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。
+ ← + + 2. Verilog 数值表示 + + 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 数据类型 + + 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
+
+ ← + + 4. 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 在快速原型制作、定制硬件加速和可重配置系统中非常有价值。
+ ← + + 10. 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 +
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.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.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.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)
+
使用小括号而不显式使用 case 关键字: +当您直接提供一个匿名函数时,您通常只需要小括号即可。例如:
list.map((a, b) => a + b)
+
这里,您直接给出了一个处理元素的函数 (a, b) => a + b
,没有使用 case
。
使用大括号并显式使用 case
关键字:
+如果您想要在处理集合元素时使用模式匹配,您可以使用大括号并在里面使用 case
关键字。这种方式通常用于元组的解构或更复杂的模式匹配。例如:
list.map { case (a, b) => a + b }
+
在这个例子中,您使用 { case (a, b) => a + b }
对元组进行解构,并对解构后的元素应用函数。
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
+
// No inputs or outputs (two versions).
+// To produce no output, return the Unit type
+def hello1(): Unit = print("Hello!")
+def hello2 = print("Hello again!")
+
+// Math operation: one input and one output.
+def times2(x: Int): Int = 2 * x
+
+// Inputs can have default values, and explicitly specifying the return type is optional.
+// Note that we recommend specifying the return types to avoid surprises/bugs.
+def timesN(x: Int, n: Int = 2) = n * x
+
+// Call the functions listed above.
+hello1()
+hello2
+times2(4)
+timesN(4) // no need to specify n to use the default value
+timesN(4, 3) // argument order is the same as the order where the function was defined
+timesN(n=7, x=2) // arguments may be reordered and assigned to explicitly
+
// These are normal functions.
+def plus1funct(x: Int): Int = x + 1
+def times2funct(x: Int): Int = x * 2
+
+// These are functions as vals.
+// The first one explicitly specifies the return type.
+val plus1val: Int => Int = x => x + 1
+val times2val = (x: Int) => x * 2
+
+// Calling both looks the same.
+plus1funct(4)
+plus1val(4)
+plus1funct(x=4)
+//plus1val(x=4) // this doesn't work
+
val
,并且不指明参数type ⇒ type = x ⇒ x + 1
funcName = value ⇒ value
Functions that take or produce functions are called higher-order functions.
// create our function
+val plus1 = (x: Int) => x + 1
+val times2 = (x: Int) => x * 2
+
+// pass it to map, a list function
+val myList = List(1, 2, 5, 9)
+val myListPlus = myList.map(plus1)
+val myListTimes = myList.map(times2)
+
+// create a custom function, which performs an operation on X N times using recursion
+def opN(x: Int, n: Int, op: Int => Int): Int = {
+ if (n <= 0) { x }
+ else { opN(op(x), n-1, op) }
+}
+
+opN(7, 3, plus1)
+opN(7, 3, times2)
+
A possibly confusing situation arises when using functions without arguments. Functions are evaluated every time they are called, while val
s are evaluated at instantiation.
import scala.util.Random
+
+// both x and y call the nextInt function, but x is evaluated immediately and y is a function
+val x = Random.nextInt
+def y = Random.nextInt // def y : Int = Random.nextInt
+
+// x was previously evaluated, so it is a constant
+println(s"x = $x")
+println(s"x = $x")
+
+// y is a function and gets reevaluated at each call, thus these produce different results
+println(s"y = $y")
+println(s"y = $y")
+
+// output:
+x = -2133939285
+x = -2133939285
+y = 1035018406
+y = -968348688
+
val
的函数对象在定义时就初始化了,后面调用不会改变值val myList = List(5, 6, 7, 8)
+
+// add one to every item in the list using an anonymous function
+// arguments get passed to the underscore variable
+// these all do the same thing
+myList.map( (x:Int) => x + 1 )
+myList.map(_ + 1)
+
+// a common situation is to use case statements within an anonymous function
+val myAnyList = List(1, 2, "3", 4L, myList)
+myAnyList.map {
+ case (_:Int|_:Long) => "Number"
+ case _:String => "String"
+ case _ => "error"
+}
+
如果只使用一次的函数对象,就没有必要建立一个 val
了
在 Scala 集合中,scanLeft
/scanRight
、reduceLeft
/reduceRight
和 foldLeft
/foldRight
是重要的函数,用于对集合进行累积运算。理解每个函数的工作方式及其适用场景是很重要的。默认情况下,scan
、reduce
和 fold
的方向是从左到右。
scanLeft
和 scanRight
是累积函数,它们对集合中的元素逐个应用累积函数,并返回一个包含所有中间结果的新集合。scanLeft
从集合的头部开始向尾部递进,而 scanRight
则从尾部开始向头部递进。List(1, 2, 3).scanLeft(0)(_ + _)
会计算 (0, 1, 3, 6)
,其中 0
是初始值,累加过程是 0 + 1 = 1
,1 + 2 = 3
,3 + 3 = 6
。reduceLeft
和 reduceRight
是累积函数,它们对集合中的元素逐个应用累积函数,但只返回最终的累积结果。reduceLeft
从集合的头部开始应用累积函数,直到尾部;reduceRight
则从尾部开始应用,直到头部。reduceLeft
和 reduceRight
不能为空集合使用,因为它们没有初始值。List(1, 2, 3).reduceLeft(_ + _)
会计算 1 + 2 + 3 = 6
。foldLeft
和 foldRight
和 reduceLeft
/reduceRight
类似,但它们接受一个初始值作为累积的起点。foldLeft
从集合的头部开始应用累积函数,而 foldRight
则从尾部开始。foldLeft
和 foldRight
可以在空集合上使用,因为它们有初始值。List(1, 2, 3).foldLeft(0)(_ + _)
会计算 0 + 1 + 2 + 3 = 6
。val exList = List(1, 5, 7, 100)
+
+// write a custom function to add two numbers, then use reduce to find the sum of all values in exList
+def add(a: Int, b: Int): Int = a + b
+val sum = exList.reduce(add)
+
+// find the sum of exList using an anonymous function (hint: you've seen this before!)
+val anon_sum = exList.reduce((a,b) => a + b)
+
+// find the moving average of exList from right to left using scan; make the result (ma2) a list of doubles
+def avg(a: Int, b: Double): Double = (a + b) / 2.0
+val ma2 = exList.scanRight(0.0)(avg)
+
这个 scanRight
调用将执行以下操作:
100
和初始值 0.0
:
+avg(100, 0.0)
得到 (100 + 0.0) / 2 = 50.0
。这是第一个累积值,它将作为下一个右侧元素的累积输入。7
:
+50.0
和当前元素 7
应用 avg
,得到 avg(7, 50.0) = (7 + 50.0) / 2 = 28.5
。这成为下一步的累积输入。5
:
+avg(5, 28.5)
得到 (5 + 28.5) / 2 = 16.75
。1
:
+avg(1, 16.75)
得到 (1 + 16.75) / 2 = 8.875
。scanRight
不仅返回最终的累积结果,它还返回经过每个步骤的中间结果。所以对于这个特定的列表和 avg
函数,scanRight
会返回一个新的列表:List(8.875, 16.75, 28.5, 50.0, 0.0)
。使用函数生成 FIR 的系数:
// get some math functions
+import scala.math.{abs, round, cos, Pi, pow}
+
+// simple triangular window
+val TriangularWindow: (Int, Int) => Seq[Int] = (length, bitwidth) => {
+ val raw_coeffs = (0 until length).map( (x:Int) => 1-abs((x.toDouble-(length-1)/2.0)/((length-1)/2.0)) )
+ val scaled_coeffs = raw_coeffs.map( (x: Double) => round(x * pow(2, bitwidth)).toInt)
+ scaled_coeffs
+}
+
+// Hamming window
+val HammingWindow: (Int, Int) => Seq[Int] = (length, bitwidth) => {
+ val raw_coeffs = (0 until length).map( (x: Int) => 0.54 - 0.46*cos(2*Pi*x/(length-1)))
+ val scaled_coeffs = raw_coeffs.map( (x: Double) => round(x * pow(2, bitwidth)).toInt)
+ scaled_coeffs
+}
+
+// check it out! first argument is the window length, and second argument is the bitwidth
+TriangularWindow(10, 16)
+HammingWindow(10, 16)
+
创建一个接受函数为参数的 Chisel FIR 模块:
// our FIR has parameterized window length, IO bitwidth, and windowing function
+class MyFir(length: Int, bitwidth: Int, window: (Int, Int) => Seq[Int]) extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(bitwidth.W))
+ val out = Output(UInt((bitwidth*2+length-1).W)) // expect bit growth, conservative but lazy
+ })
+
+ // calculate the coefficients using the provided window function, mapping to UInts
+ val coeffs = window(length, bitwidth).map(_.U)
+
+ // create an array holding the output of the delays
+ // note: we avoid using a Vec here since we don't need dynamic indexing
+ val delays = Seq.fill(length)(Wire(UInt(bitwidth.W))).scan(io.in)( (prev: UInt, next: UInt) => {
+ next := RegNext(prev)
+ next
+ })
+
+ // multiply, putting result in "mults"
+ val mults = delays.zip(coeffs).map{ case(delay: UInt, coeff: UInt) => delay * coeff }
+
+ // add up multiplier outputs with bit growth
+ val result = mults.reduce(_+&_)
+
+ // connect output
+ io.out := result
+}
+
+visualize(() => new MyFir(7, 12, TriangularWindow))
+
delays.zip(coeffs)
也可以写成 (delays zip coeffs)
// math imports
+import scala.math.{pow, sin, Pi}
+import breeze.signal.{filter, OptOverhang}
+import breeze.signal.support.{CanFilter, FIRKernel1D}
+import breeze.linalg.DenseVector
+
+// test parameters
+val length = 7
+val bitwidth = 12 // must be less than 15, otherwise Int can't represent the data, need BigInt
+val window = TriangularWindow
+
+// test our FIR
+test(new MyFir(length, bitwidth, window)) { c =>
+
+ // test data
+ val n = 100 // input length
+ val sine_freq = 10
+ val samp_freq = 100
+
+ // sample data, scale to between 0 and 2^bitwidth
+ val max_value = pow(2, bitwidth)-1
+ val sine = (0 until n).map(i => (max_value/2 + max_value/2*sin(2*Pi*sine_freq/samp_freq*i)).toInt)
+ //println(s"input = ${sine.toArray.deep.mkString(", ")}")
+
+ // coefficients
+ val coeffs = window(length, bitwidth)
+ //println(s"coeffs = ${coeffs.toArray.deep.mkString(", ")}")
+
+ // use breeze filter as golden model; need to reverse coefficients
+ val expected = filter(
+ DenseVector(sine.toArray),
+ FIRKernel1D(DenseVector(coeffs.reverse.toArray), 1.0, ""),
+ OptOverhang.None
+ )
+ expected.toArray // seems to be necessary
+ //println(s"exp_out = ${expected.toArray.deep.mkString(", ")}") // this seems to be necessary
+
+ // push data through our FIR and check the result
+ c.reset.poke(true.B)
+ c.clock.step(5)
+ c.reset.poke(false.B)
+ for (i <- 0 until n) {
+ c.io.in.poke(sine(i).U)
+ if (i >= length-1) { // wait for all registers to be initialized since we didn't zero-pad the data
+ val expectValue = expected(i-length+1)
+ //println(s"expected value is $expectValue")
+ c.io.out.expect(expected(i-length+1).U)
+ //println(s"cycle $i, got ${c.io.out.peek()}, expect ${expected(i-length+1)}")
+ }
+ c.clock.step(1)
+ }
+}
+
class Neuron(inputs: Int, act: FixedPoint => FixedPoint) extends Module {
+ val io = IO(new Bundle {
+ val in = Input(Vec(inputs, FixedPoint(16.W, 8.BP)))
+ val weights = Input(Vec(inputs, FixedPoint(16.W, 8.BP)))
+ val out = Output(FixedPoint(16.W, 8.BP))
+ })
+
+ io.out := act((io.in zip io.weights).map{case (a,b) => a*b}.reduce(_+_))
+}
+
在 Chisel 中,FixedPoint
类型用于表示定点数,这是一种在硬件设计中常用的数值表示方法,特别适用于数字信号处理(DSP)等领域。FixedPoint
类型提供了一种方式来指定数值的位宽和小数点位置,这在设计需要精确控制数值精度和范围的硬件时非常有用。
FixedPoint(16.W, 8.BP)
创建了一个定点数,其中:
16.W
指定了该定点数的总位宽为 16 位。这包括了整数部分和小数部分的所有位。8.BP
指定了该定点数的二进制小数点位置(Binary Point)。这里,8.BP
意味着小数部分有 8 位。当您在 Chisel 中使用不同格式的 FixedPoint
数进行运算时,首先需要调整它们,以确保具有相同的位宽和小数点位置。以下是如何进行这种转换和随后的加法运算的示例。
假设您有两个 FixedPoint
数,分别定义如下:
num1
有 16 位宽,其中 8 位是小数位:FixedPoint(16.W, 8.BP)
num2
有 18 位宽,其中 10 位是小数位:FixedPoint(18.W, 10.BP)
要进行加法运算,您首先需要决定一个目标格式,通常是选择两者中更“大”的格式,也就是位宽更宽、小数位更多的那个。在这个例子中,目标格式将是 FixedPoint(18.W, 10.BP)
。
扩展 num1
到更大的格式:
val num1Extended = num1.setBinaryPoint(10).asFixedPoint(18.W)
+
这行代码将 num1
的小数点位置扩展到 10 位,并将整个数扩展到 18 位宽。
确保 num2
也符合目标格式:
虽然 num2
已经是 FixedPoint(18.W, 10.BP)
,我们通常在实际代码中不需要对其进行任何操作,但是为了代码的清晰性和一致性,您可以明确指出这一点(尽管在实践中这不是必需的):
val num2Adjusted = num2.asFixedPoint(18.W)
+
进行加法运算:
一旦两个数都调整到了相同的格式,就可以直接进行加法运算了:
val sum = num1Extended + num2Adjusted
+
这样,sum
将会是两个数的和,也具有相同的格式 FixedPoint(18.W, 10.BP)
。
这个示例说明了如何在 Chisel 中将两个不同格式的 FixedPoint
数调整为相同格式,然后进行加法运算。通过这种方式,您可以确保运算的正确性,并避免由于格式不匹配导致的精度损失。
现在让我们创建一些激活函数!我们将使用零作为阈值。典型的激活函数包括 Sigmoid 函数和修正线性单元(ReLU)。
我们将使用的 Sigmoid 被称为 逻辑函数 (opens new window),其公式为:
其中
第二个函数,ReLU
,由类似的公式给出。
\text{relu}(x) = +\begin{cases} +0 & \text{如果 } x \leq 0 \\ +x & \text{如果 } x > 0 +\end{cases}
在下面实现这两个函数。您可以像这样指定一个固定点数值 -3.14.F(8.BP)
。
val Step: FixedPoint => FixedPoint = (x: FixedPoint) => Mux(x > 0.F(8.BP), 1.F(8.BP), 0.F(8.BP))
+val ReLU: FixedPoint => FixedPoint = (x: FixedPoint) => Mux(x > 0.F(8.BP), x, 0.F(8.BP))
+
Mux
的第一个参数是比较表达式 x > 0.F(0.BP)
,如果这个表达式为真(即 x>0),Mux
返回第二个参数 1.F(0.BP)
;如果为假(即 x<=0),Mux
返回第三个参数 0.F(0.BP)
。
// test our Neuron
+test(new Neuron(2, Step)) { c =>
+ val inputs = Seq(Seq(-1, -1), Seq(-1, 1), Seq(1, -1), Seq(1, 1))
+
+ // make this a sequence of two values
+ val weights = Seq(1, 1)
+
+ // push data through our Neuron and check the result (AND gate)
+ c.reset.poke(true.B)
+ c.clock.step(5)
+ c.reset.poke(false.B)
+ for (i <- inputs) {
+ c.io.in(0).poke(i(0).F(8.BP))
+ c.io.in(1).poke(i(1).F(8.BP))
+ c.io.weights(0).poke(weights(0).F(16.W, 8.BP))
+ c.io.weights(1).poke(weights(1).F(16.W, 8.BP))
+ c.io.out.expect((if (i(0) + i(1) > 0) 1 else 0).F(16.W, 8.BP))
+ c.clock.step(1)
+ }
+
+}
+
在您的 Neuron
测试代码中,改变 weights
中值的类型从整数 1
到浮点数 1.0
是可行的,但这样做后需要确保 weights
的值赋予 FixedPoint
类型时小数点位置(binary point)被正确指定。在这个特定的情况下,由于您定义的权重值是 1.0
,其实并没有小数部分,转换成定点数时小数部分是 0,所以您可以将其直接视为没有小数点的值。
如果您将 weights
定义为:
val weights = Seq(1.0, 1.0)
+
然后在 poke
操作时,您需要将它们转换为适当的 FixedPoint
形式。您的权重值 1.0
实际上在没有小数时与 1
是等价的,但从概念上讲,使用 1.0
表示这是一个可以有小数部分的数值。当您将这些权重值赋给 FixedPoint
信号时,您可以这样操作:
c.io.weights(0).poke(weights(0).F(16.W, 8.BP))
+c.io.weights(1).poke(weights(1).F(16.W, 8.BP))
+
在这个 poke
调用中,虽然 weights
的值是 1.0
,但是由于我们这里用的是 .F(16.W, 8.BP)
转换,它会被认为是没有小数部分的数。在这里,小数点的位置被设置为 8,但由于权重是整数 1
的等价形式 1.0
,其实没有影响。
在决定是否使用小数点时,主要考虑的是您期望的数值范围和精度。如果您的权重值确实需要小数部分,那么您应该使用 1.0
这样的浮点数表示,并在将其赋给 FixedPoint
类型时注意指定正确的小数点位置。在您的案例中,由于 1.0
等价于整数 1
,所以实际上并不涉及小数点处理,而是直接转换为了等效的定点数表示。
abstract class MyAbstractClass {
+ def myFunction(i: Int): Int
+ val myValue: String
+}
+class ConcreteClass extends MyAbstractClass {
+ def myFunction(i: Int): Int = i + 1
+ val myValue = "Hello World!"
+}
+// Uncomment below to test!
+// val abstractClass = new MyAbstractClass() // Illegal! Cannot instantiate an abstract class
+val concreteClass = new ConcreteClass() // Legal!
+
不能实例化抽象类
Traits are very similar to abstract classes in that they can define unimplemented values. However, they differ in two ways:
Traits are how Scala implements multiple inheritance, as shown in the example below. MyClass
extends from both traits HasFunction
and HasValue
trait HasFunction {
+ def myFunction(i: Int): Int
+}
+trait HasValue {
+ val myValue: String
+ val myOtherValue = 100
+}
+class MyClass extends HasFunction with HasValue {
+ override def myFunction(i: Int): Int = i + 1
+ val myValue = "Hello World!"
+}
+// Uncomment below to test!
+// val myTraitFunction = new HasFunction() // Illegal! Cannot instantiate a trait
+// val myTraitValue = new HasValue() // Illegal! Cannot instantiate a trait
+val myClass = new MyClass() // Legal!
+
extends HasFunction with HasValue
: 使用 with
来继承多个类
注:通常都推荐使用 trait 除非特别想强调只能继承一个父类
一类特殊的 Class,不能实例化,可以直接调用
object MyObject {
+ def hi: String = "Hello World!"
+ def apply(msg: String) = msg
+}
+println(MyObject.hi)
+println(MyObject("This message is important!")) // equivalent to MyObject.apply(msg)
+
## Companion Objects<a name="compobj"></a>
+
+When a class and an object share the same name and defined in the same file, the object is called a companion object. When you use `new` before the class/object name, it will instantiate the class. If you don't use `new`, it will reference the object:
+
+<span style="color:blue">Example: Companion Object</span><br>
+
When a class and an object share the same name and defined in the same file, the object is called a companion object. When you use new
before the class/object name, it will instantiate the class. If you don't use new
, it will reference the object.
object Lion {
+ def roar(): Unit = println("I'M AN OBJECT!")
+}
+class Lion {
+ def roar(): Unit = println("I'M A CLASS!")
+}
+new Lion().roar()
+Lion.roar()
+
在 Scala 中,伴生对象是与特定类共享同一名称的单例对象。每个类只能有一个伴生对象,反之亦然。使用伴生对象主要有几个目的,如下所述:
Circle
类,可能会在其伴生对象中定义一个 Pi
常量。apply
方法,可以提供多个构造器。客户端代码可以通过调用这些 apply
方法,而不是直接使用 new
关键字来创建实例,使得代码更加简洁。这种模式在 Scala 集合库中非常常见。以下是一个简单的示例来说明这些用途:
class Circle(radius: Double) {
+ def area: Double = Circle.Pi * radius * radius
+}
+
+object Circle {
+ private val Pi = 3.141592653589793
+
+ // 2. Code execution before/after class constructor
+ def init(): Unit = {
+ println("Circle companion object initialized.")
+ }
+
+ // 3. Multiple constructors
+ def apply(radius: Double): Circle = {
+ init()
+ new Circle(radius)
+ }
+}
+
+
在上面的例子中:
Pi
是作为与 Circle
类相关的常量定义在伴生对象中。init
方法可以在创建类实例之前被调用。apply
方法作为创建 Circle
实例的替代构造器,它允许客户端代码通过 Circle(radius)
而不是 new Circle(radius)
来创建 Circle
的实例,同时确保每次实例化前都调用 init
方法。这样,伴生对象增强了 Scala 类的功能性,同时保持代码的组织性和简洁性。
在 Scala 中,类的构造器和工厂函数是用来创建类实例的,但它们的使用方式和上下文有所不同。
类的构造器:
类的构造器是类定义的一部分,用于初始化新创建的对象。Scala 中的构造器分为两种:主构造器和辅助构造器。
class Person(val name: String, val age: Int) {
+ // 主构造器的代码
+ println("A new person instance is created.")
+}
+
+
def this(...)
定义。每个辅助构造器必须以另一个已经定义好的构造器(主构造器或另一个辅助构造器)的调用开始。class Person(val name: String, val age: Int) {
+ // 辅助构造器
+ def this(name: String) = {
+ this(name, 0) // 调用主构造器
+ }
+}
+
+
工厂函数:
工厂函数通常定义在类的伴生对象中,提供了一种灵活的创建类实例的方式,而不必直接使用 new
关键字。工厂函数可以有不同的名字,但习惯上经常使用 apply
方法。使用工厂函数的好处包括更简洁的语法和更丰富的逻辑处理能力。
object Person {
+ // 工厂方法
+ def apply(name: String, age: Int): Person = {
+ // 可以添加逻辑,例如参数校验或预处理
+ new Person(name, age)
+ }
+}
+
+
使用工厂函数创建实例时,可以省略 new
关键字,直接通过类名加上参数调用:
val p = Person("Alice", 25) // 使用工厂函数而非 new 关键字
+
+
总的来说,类的构造器直接定义了如何构建类的实例,而工厂函数提供了更灵活、更丰富的逻辑来创建和管理类实例。
case class
(样例类)是一种特殊类型的类,它们提供了一些附加功能,使得在函数式编程和模式匹配中非常有用。样例类在 Scala 编程中非常常见,以下是它们的一些有用特性:
new
关键字实例化:通常在创建普通类的实例时需要使用 new
关键字。但对于样例类,Scala 允许您省略 new
关键字直接创建实例。unapply
方法:这使得样例类非常适合用于模式匹配。unapply
方法可以从实例中提取构造器参数作为元组返回,这在模式匹配中特别有用。这里是一个声明三个不同样例类 Nail
、Screw
和 Staple
的例子:
class Nail(length: Int) // Regular class
+val nail = new Nail(10) // Requires the `new` keyword
+// println(nail.length) // Illegal! Class constructor parameters are not by default externally visible
+
+class Screw(val threadSpace: Int) // By using the `val` keyword, threadSpace is now externally visible
+val screw = new Screw(2) // Requires the `new` keyword
+println(screw.threadSpace)
+
+case class Staple(isClosed: Boolean) // Case class constructor parameters are, by default, externally visible
+val staple = Staple(false) // No `new` keyword required
+println(staple.isClosed)
+
+// 使用模式匹配来检查 Staple 实例的 isClosed 属性
+staple match {
+ case Staple(true) => println("The staple is closed.")
+ case Staple(false) => println("The staple is open.")
+}
+
+// output:
+2
+false
+The staple is open.
+
import scala.math.pow
+
+// create a module
+class GrayCoder(bitwidth: Int) extends Module {
+ val io = IO(new Bundle{
+ val in = Input(UInt(bitwidth.W))
+ val out = Output(UInt(bitwidth.W))
+ val encode = Input(Bool()) // decode on false
+ })
+
+ when (io.encode) { //encode
+ io.out := io.in ^ (io.in >> 1.U)
+ } .otherwise { // decode, much more complicated
+ io.out := Seq.fill(log2Ceil(bitwidth))(Wire(UInt(bitwidth.W))).zipWithIndex.fold((io.in, 0)){
+ case ((w1: UInt, i1: Int), (w2: UInt, i2: Int)) => {
+ w2 := w1 ^ (w1 >> pow(2, log2Ceil(bitwidth)-i2-1).toInt.U)
+ (w2, i1)
+ }
+ }._1
+ }
+}
+
定义了一个名为 GrayCoder
的模块,用于执行格雷码的编码和解码。格雷码是一种二进制数码系统,其中两个连续的数值只有一个位数不同。这在某些硬件设计中非常有用,尤其是在减少位切换和错误率方面。
模块 GrayCoder
接受三个输入:
io.in
: 这是要编码或解码的输入值,其位宽由构造器参数 bitwidth
指定。io.out
: 这是编码或解码后的输出值,位宽与输入相同。io.encode
: 这是一个布尔输入,用于指示模块是应执行编码操作(当 encode
为 true
)还是解码操作(当 encode
为 false
)。编码部分 (io.encode
为真时) 相对简单:
io.out := io.in ^ (io.in >> 1.U)
+
+
在这里,进行格雷码编码的方式是将输入 io.in
与其自身右移一位的结果进行异或操作。右移操作 io.in >> 1.U
生成了 io.in
每一位右移一位的结果,然后通过异或操作 ^
与原始输入进行比较,以生成格雷码。
解码部分 (io.encode
为假时) 相对复杂:
io.out := Seq.fill(log2Ceil(bitwidth))(Wire(UInt(bitwidth.W))).zipWithIndex.fold((io.in, 0)){
+ case ((w1: UInt, i1: Int), (w2: UInt, i2: Int)) => {
+ w2 := w1 ^ (w1 >> pow(2, log2Ceil(bitwidth)-i2-1).toInt.U)
+ (w2, i1)
+ }
+}._1
+
+
这部分是格雷码的解码逻辑。解码格雷码比编码更复杂,因为需要迭代地将已解码的部分与右移的值进行异或运算来恢复原始的二进制数:
Seq.fill(log2Ceil(bitwidth))(Wire(UInt(bitwidth.W)))
: 这创建了一个足够长度的 Wire
序列,每个元素的宽度都是 bitwidth
。序列的长度由 log2Ceil(bitwidth)
确定,这是对数的上取整结果,确保能够覆盖所有位。zipWithIndex
: 这将序列中的每个元素与其索引进行配对。fold
: 这在序列上执行折叠操作,用于迭代地计算解码结果。在每一步,都将当前的部分解码结果 w1
与其右移特定位数后的值进行异或运算。移位的位数 pow(2, log2Ceil(bitwidth)-i2-1).toInt
是递减的,这确保了在解码过程中正确地将各个位逐个恢复。._1
用于从 (w2, i1)
这个元组中取出 w2
,即最终的解码值,将其赋给 io.out
。通过这种方式,GrayCoder
模块能够根据 encode
信号的值选择执行格雷码的编码或解码,并输出相应的结果。
// test our gray coder
+val bitwidth = 4
+test(new GrayCoder(bitwidth)) { c =>
+ def toBinary(i: Int, digits: Int = 8) = {
+ String.format("%" + digits + "s", i.toBinaryString).replace(' ', '0')
+ }
+ println("Encoding:")
+ for (i <- 0 until pow(2, bitwidth).toInt) {
+ c.io.in.poke(i.U)
+ c.io.encode.poke(true.B)
+ c.clock.step(1)
+ println(s"In = ${toBinary(i, bitwidth)}, Out = ${toBinary(c.io.out.peek().litValue.toInt, bitwidth)}")
+ }
+
+ println("Decoding:")
+ for (i <- 0 until pow(2, bitwidth).toInt) {
+ c.io.in.poke(i.U)
+ c.io.encode.poke(false.B)
+ c.clock.step(1)
+ println(s"In = ${toBinary(i, bitwidth)}, Out = ${toBinary(c.io.out.peek().litValue.toInt, bitwidth)}")
+ }
+}
+
这段代码是使用 Chisel 测试框架来测试 GrayCoder
模块的一个实例。GrayCoder
模块是一个旨在执行格雷码编码和解码的模块。测试主要分为两个部分:编码和解码。以下是代码的详细解释:
设置测试位宽:
val bitwidth = 4
+
+
这里设定了 bitwidth
为 4,这意味着测试将处理 4 位宽的输入和输出。在格雷码转换中,输入和输出都将具有相同的位宽。
测试实例的创建:
test(new GrayCoder(bitwidth)) { c =>
+
+
这行代码启动了对 GrayCoder
模块的测试,其中 bitwidth
为 4,传递给 GrayCoder
以设置其处理宽度。
定义二进制格式化函数:
def toBinary(i: Int, digits: Int = 8) = {
+ String.format("%" + digits + "s", i.toBinaryString).replace(' ', '0')
+}
+
+
toBinary
函数用于将整数格式化为二进制字符串。digits
参数指定了字符串的长度,未使用位用 '0' 填充。
编码测试:
println("Encoding:")
+for (i <- 0 until pow(2, bitwidth).toInt) {
+ c.io.in.poke(i.U)
+ c.io.encode.poke(true.B)
+ c.clock.step(1)
+ println(s"In = ${toBinary(i, bitwidth)}, Out = ${toBinary(c.io.out.peek().litValue.toInt, bitwidth)}")
+}
+
+
在编码测试部分,循环变量 i
从 0 遍历到 2^bitwidth - 1
。对于每个 i
值:
poke
方法将 i
作为输入写入到 GrayCoder
模块的 in
端口。encode
信号为 true
,指示模块执行编码操作。解码测试:
println("Decoding:")
+for (i <- 0 until pow(2, bitwidth).toInt) {
+ c.io.in.poke(i.U)
+ c.io.encode.poke(false.B)
+ c.clock.step(1)
+ println(s"In = ${toBinary(i, bitwidth)}, Out = ${toBinary(c.io.out.peek().litValue.toInt, bitwidth)}")
+}
+
+
解码测试遵循与编码测试相似的逻辑,不同之处在于:
encode
信号为 false
,指示模块执行解码操作。i
被认为是经过格雷码编码的值,测试检查模块是否能正确地将其解码回原始数值。通过这个测试脚本,可以验证 GrayCoder
模块在给定位宽下对所有可能的输入值进行正确的编码和解码。输出结果提供了一个直观的方式来确认格雷码转换是否按预期执行。
println(10.getClass)
+println(10.0.getClass)
+println("ten".getClass)
+class MyClass {
+ def myMethod = ???
+}
+println(new MyClass().getClass)
+
+// output:
+int
+double
+class java.lang.String
+class ammonite.$sess.cmd4$Helper$MyClass
+
没有返回值的函数,就设置返回值类型为 Unit
var counter = 0
+def increment(): Unit = {
+ counter += 1
+}
+increment()
+
模块 2.2 讨论了 Chisel 类型和 Scala 类型之间的区别。例如,下面的代码是合法的,因为 0.U
是 UInt
类型(一个 Chisel 类型):
val a = Wire(UInt(4.W))
+a := 0.U
+
而下面的代码是非法的,因为 0 是 Int
类型(一个 Scala 类型):
val a = Wire(UInt(4.W))
+a := 0
+
这同样适用于 Bool
,这是一个 Chisel 类型,它与 Scala 的 Boolean
类型不同:
val bool = Wire(Bool())
+val boolean: Boolean = false
+// 合法
+when (bool) { ... }
+if (boolean) { ... }
+// 非法
+if (bool) { ... }
+when (boolean) { ... }
+
如果你错误地混合使用了 UInt
和 Int
或 Bool
和 Boolean
,Scala 编译器通常会为你捕捉到这些错误。这归功于 Scala 的静态类型系统。在编译时,编译器能够区分 Chisel 类型和 Scala 类型,并且能够理解 if ()
期望一个 Boolean
而 when ()
期望一个 Bool
。这种类型检查机制有助于避免类型相关的逻辑错误,确保您的硬件描述代码的正确性和稳定性。
asInstanceOf
x.asInstanceOf[T]
casts the object x
to the type T
. It throws an exception if the given object cannot be cast to type T
.
val x: UInt = 3.U
+try {
+ println(x.asInstanceOf[Int])
+} catch {
+ case e: java.lang.ClassCastException => println("As expected, we can't cast UInt to Int")
+}
+
+// But we can cast UInt to Data since UInt inherits from Data.
+println(x.asInstanceOf[Data])
+
Chisel 提供了一套类型转换函数,可以帮助开发者在不同的 Chisel 类型之间转换数据。其中,asTypeOf()
是最通用的类型转换函数,允许将一个 Chisel 数据类型转换成另一个指定的 Chisel 数据类型,只要这样的转换在逻辑上是有意义的。
除了 asTypeOf()
,还有一些特定的转换函数,如 asUInt()
和 asSInt()
,这些函数分别用于将数据转换为无符号整数(UInt)和有符号整数(SInt)。使用这些函数可以确保类型转换的意图更加明确,同时也使代码更容易理解。
class TypeConvertDemo extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(4.W))
+ val out = Output(SInt(4.W))
+ })
+ io.out := io.in.asTypeOf(io.out)
+}
+
class ConstantSum(in1: chisel3.Data, in2: chisel3.Data) extends Module {
+ val io = IO(new Bundle {
+ val out = Output(chiselTypeOf(in1)) // in case in1 is literal then just get its type
+ })
+ (in1, in2) match {
+ case (x: UInt, y: UInt) => io.out := x + y
+ case (x: SInt, y: SInt) => io.out := x + y
+ case _ => throw new Exception("I give up!")
+ }
+}
+
下面代码会报错:
class InputIsZero extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(16.W))
+ val out = Output(Bool())
+ })
+ io.out := {io.in match {
+ // note that case 0.U is an error
+ case (0.U) => true.B
+ case _ => false.B
+ }}
+}
+println(getVerilog(new InputIsZero))
+
这是因为在硬件描述语言中的 match
语句并不像软件编程语言那样工作。在 Chisel(基于 Scala),match
语句通常用于软件逻辑的模式匹配,而不是硬件逻辑。硬件描述中的条件判断应该使用 when
、.elsewhen
和 .otherwise
等语句来实现。
在您的代码中,您尝试使用 match
语句来为 io.out
赋值,这是不允许的。您应该改用 when
语句来判断 io.in
是否为 0,并据此为 io.out
赋值。以下是修改后的代码:
class InputIsZero extends Module {
+ val io = IO(new Bundle {
+ val in = Input(UInt(16.W))
+ val out = Output(Bool())
+ })
+
+ // 使用when语句代替match进行硬件条件判断
+ io.out := io.in === 0.U
+}
+
+
在这段修正后的代码中,io.out
直接被赋值为 io.in === 0.U
的结果,这是 Chisel 中比较 UInt
值的标准方法。这样的表达式直接评估 io.in
是否等于 0,并将布尔结果赋给 io.out
,无需使用 when-otherwise
语句,因为这里是个直接的等式判断。
unapply
方法在 Scala 中是模式匹配的一种强大特性,它通常与 apply
方法相对应。apply
方法允许你以一种简洁的方式构造对象(即不需要显式地使用 new
关键字),而 unapply
方法则用于在模式匹配中分解对象,提取出关键的信息或属性。对于每个 case 类,Scala 编译器会自动创建一个伴生对象(companion object),其中包含 apply
和 unapply
方法。apply
方法使你能够不用 new
关键字来创建对象,而 unapply
方法则用于模式匹配和提取值。当你在模式匹配中使用 case 类时,unapply
方法会自动被调用。该方法从主构造器接收的对象中提取出数据,并将其包装为一个选项(Option),通常是一个元组。模式匹配会检查这个选项,如果是 Some
,则匹配成功,并允许进一步操作提取的值;如果是 None
,则匹配失败,继续尝试下一个模式。
考虑下面的 Something
case 类:
case class Something(a: String, b: Int)
+
对于这个 case 类,Scala 编译器自动生成 unapply
方法,当你执行类似下面的模式匹配时:
val a = Something("A", 3)
+
+a match {
+ case Something("A", value) => println(s"Matched with value: $value")
+ case Something(str, 3) => println(s"Matched with string: $str")
+}
+
这里发生的事情是:
case Something("A", value)
中,unapply
方法从对象 a
中提取 String
和 Int
,并检查字符串是否等于 "A"
。如果是,value
被赋予 Int
值,然后执行相应的代码块。case Something(str, 3)
中,同样利用 unapply
方法提取,这次是检查 Int
是否等于 3
。如果是,str
被赋予相应的 String
值。unapply
方法的存在让模式匹配变得非常强大和灵活,不仅可以检查类型,还可以提取并操作数据。这使得 Scala 的模式匹配在处理复杂数据结构时非常有用。
case class SomeGeneratorParameters(
+ someWidth: Int,
+ someOtherWidth: Int = 10,
+ pipelineMe: Boolean = false
+) {
+ require(someWidth >= 0)
+ require(someOtherWidth >= 0)
+ val totalWidth = someWidth + someOtherWidth
+}
+
+def delay(p: SomeGeneratorParameters): Int = p match {
+ case SomeGeneratorParameters(_, sw, false) => sw * 2
+ case sg @SomeGeneratorParameters(_, _, true) => sg.totalWidth * 3
+}
+
这个示例演示了如何在 Scala 中使用模式匹配结合 unapply
方法,以及如何在 case 类和普通类中实现和使用这些方法。
SomeGeneratorParameters
case 类SomeGeneratorParameters
是一个 case 类,它接受三个参数,其中两个有默认值。由于这是一个 case 类,Scala 编译器会自动生成 apply
和 unapply
方法:
apply
方法允许你直接使用 SomeGeneratorParameters(10, 10)
来构造实例,而不需要 new
关键字。unapply
方法使你能够在模式匹配中解构 SomeGeneratorParameters
对象。delay
函数delay
函数接受一个 SomeGeneratorParameters
实例 p
并根据其属性计算一个延迟值:
pipelineMe
为 false
的 SomeGeneratorParameters
实例,计算延迟为 someOtherWidth * 2
。@
符号 (sg @ SomeGeneratorParameters(...)
) 来同时绑定整个参数实例到 sg
变量和匹配其属性。如果 pipelineMe
是 true
,则使用该实例的 totalWidth
属性计算延迟为 totalWidth * 3
。直接匹配类型并引用参数值:
case p: SomeGeneratorParameters => p.someOtherWidth * 2
+
这表示匹配 SomeGeneratorParameters
类型的任何实例 p
,并使用其 someOtherWidth
属性。
解构匹配并直接引用内部值:
case SomeGeneratorParameters(_, sw, _) => sw * 2
+
这表示解构 SomeGeneratorParameters
实例,匹配任何值,但只关心 someOtherWidth
(sw
)。
使用 @
保留整个实例的引用同时匹配内部值:
case sg @ SomeGeneratorParameters(_, sw, true) => sw
+
这使得 sg
绑定到整个 SomeGeneratorParameters
实例,同时解构匹配来引用 someOtherWidth
。
嵌入条件检查:
case s: SomeGeneratorParameters if s.pipelineMe => s.someOtherWidth * 2
+
这表示匹配 SomeGeneratorParameters
类型的实例,且只在 pipelineMe
为 true
时匹配。
所有这些语法形式都依赖于 unapply
方法,它在 case 类的伴生对象中自动生成。如果您想为非 case 类实现类似的模式匹配功能,可以手动实现 apply
和 unapply
方法,如最后部分提及,但示例未给出具体实现。
class Boat(val name: String, val length: Int)
+object Boat {
+ def unapply(b: Boat): Option[(String, Int)] = Some((b.name, b.length))
+ def apply(name: String, length: Int): Boat = new Boat(name, length)
+}
+
+def getSmallBoats(seq: Seq[Boat]): Seq[Boat] = seq.filter { b =>
+ b match {
+ case Boat(_, length) if length < 60 => true
+ case Boat(_, _) => false
+ }
+}
+
+val boats = Seq(Boat("Santa Maria", 62), Boat("Pinta", 56), Boat("Nina", 50))
+println(getSmallBoats(boats).map(_.name).mkString(" and ") + " are small boats!")
+
在这个例子中,我们定义了一个名为 Boat
的类,它有两个属性:name
和 length
。为了使 Boat
类能够在模式匹配中使用,我们还定义了一个伴生对象 Boat
,其中实现了 apply
和 unapply
方法。这样,即使 Boat
不是一个 case 类,我们也可以使用模式匹配的功能,就像使用 case 类那样。
Boat
类定义:
class Boat(val name: String, val length: Int)
+
这个定义创建了一个拥有 name
(船名)和 length
(船长)两个属性的 Boat
类。这两个属性是公开的,因此可以在类的外部访问。
Boat
伴生对象定义:
object Boat {
+ def unapply(b: Boat): Option[(String, Int)] = Some((b.name, b.length))
+ def apply(name: String, length: Int): Boat = new Boat(name, length)
+}
+
apply
方法允许我们以 Boat("Santa Maria", 62)
的形式创建 Boat
实例,而不需要使用 new
关键字。unapply
方法用于模式匹配。它接收一个 Boat
实例,并返回一个包含船名和船长的元组的 Option
。如果 Boat
实例不符合期望的格式(虽然在这里总是返回 Some
),也可以返回 None
。getSmallBoats
函数定义:
def getSmallBoats(seq: Seq[Boat]): Seq[Boat] = seq.filter { b =>
+ b match {
+ case Boat(_, length) if length < 60 => true
+ case _ => false
+ }
+}
+
这个函数接收一个 Boat
序列,使用 filter
方法和模式匹配来筛选出长度小于 60 的船。case Boat(_, length) if length < 60 => true
这行代码使用 unapply
方法从 Boat
实例中提取长度,如果长度小于 60,就选择这艘船。
测试和输出:
val boats = Seq(Boat("Santa Maria", 62), Boat("Pinta", 56), Boat("Nina", 50))
+println(getSmallBoats(boats).map(_.name).mkString(" and ") + " are small boats!")
+
这部分代码创建了一个包含三艘船的序列,然后调用 getSmallBoats
函数来筛选出其中的小船,并打印出这些小船的名字。
通过这个示例,你可以看到如何通过实现 unapply
方法在普通类上使用模式匹配,以及如何将这些技术应用于实际问题。这展示了 Scala 在处理数据和模式匹配方面的强大功能。
偏函数(Partial Function)是一种特殊的函数,它只对输入值的一个子集进行定义,并不对所有可能的输入值都给出定义。偏函数在 Scala 中通常用于模式匹配,特别是处理那些只对特定输入感兴趣的情况。
偏函数的类型是 PartialFunction[A, B]
,其中 A
是输入类型,B
是输出类型。你可以使用 { case ... => ... }
语法来定义一个偏函数。在这个语法中,每个 case
语句定义了函数的行为对于特定的输入值或输入模式。
偏函数非常适合用在模式匹配中,特别是当你只需要处理输入数据的一部分,而不关心其他数据时。这样做可以使代码更清晰和简洁。
让我们回顾 getSmallBoats
函数的例子来看偏函数是如何工作的:
def getSmallBoats(seq: Seq[Boat]): Seq[Boat] = seq.filter { b =>
+ b match {
+ case Boat(_, length) if length < 60 => true
+ case _ => false
+ }
+}
+
在这个例子中,filter
方法接受一个函数作为参数,这个函数将每个元素 b
从序列 seq
中传递到一个 match
表达式。match
表达式实际上定义了一个偏函数,它只对长度小于 60 的船只感兴趣。
我们可以将这段代码重构为使用显式的偏函数定义:
val smallBoat: PartialFunction[Boat, Boolean] = {
+ case Boat(_, length) if length < 60 => true
+}
+
+def getSmallBoats(seq: Seq[Boat]): Seq[Boat] = seq.filter(smallBoat.lift)
+
在这个重构后的版本中,smallBoat
是一个偏函数,它仅在船的长度小于 60 时返回 true
。lift
方法将 PartialFunction
转换为一个返回 Option
的普通函数,使得其可以与 filter
一起使用。这里的 lift
将 true
转换为 Some(true)
,并将不匹配的情况转换为 None
,然后 filter
通过这些 Some(true)
和 None
值来决定哪些元素应该被保留。
通过这个例子,你可以看到偏函数如何提供一种强大且表达性很好的方式来处理特定的数据和模式,同时忽略其他不相关的情况。
class Bundle1 extends Bundle {
+ val a = UInt(8.W)
+}
+
+class Bundle2 extends Bundle1 {
+ val b = UInt(16.W)
+}
+
+class BadTypeModule extends Module {
+ val io = IO(new Bundle {
+ val c = Input(Clock())
+ val in = Input(UInt(2.W))
+ val out = Output(Bool())
+
+ val bundleIn = Input(new Bundle2)
+ val bundleOut = Output(new Bundle1)
+ })
+
+ //io.out := io.c // won't work due to different types
+
+ // Okay, but Chisel will truncate the input width to 1 to match the output.
+// io.out := io.in
+
+// // Compiles; Chisel will connect the common subelements of the two Bundles (in this case, 'a').
+// io.bundleOut := io.bundleIn
+}
+
+println(getVerilog(new BadTypeModule))
+
在这个 Chisel 示例中,我们定义了两个 Bundle
类(Bundle1
和 Bundle2
),然后创建了一个名为 BadTypeModule
的模块,用以展示不同类型赋值时的行为。这里同时展示了可以正常工作的代码和会引发问题的代码,让我们逐一进行解释:
Bundle 定义:
Bundle1
和 Bundle2
是 Chisel 中的两个数据包结构(或称为复合类型)。Bundle2
继承了 Bundle1
,所以它包含了 Bundle1
的所有字段,并额外增加了一个字段 b
。
class Bundle1 extends Bundle {
+ val a = UInt(8.W)
+}
+
+class Bundle2 extends Bundle1 {
+ val b = UInt(16.W)
+}
+
BadTypeModule 模块定义:
在 BadTypeModule
模块中,定义了一些输入输出接口,包括标准的 UInt
和特定的 Bundle
类型。
class BadTypeModule extends Module {
+ val io = IO(new Bundle {
+ val c = Input(Clock())
+ val in = Input(UInt(2.W))
+ val out = Output(Bool())
+
+ val bundleIn = Input(new Bundle2)
+ val bundleOut = Output(new Bundle1)
+ })
+}
+
接下来是对 io.out
赋值的不同尝试:
io.out := io.c
这行代码是错误的尝试,因为 io.c
是 Clock()
类型,而 io.out
是 Bool()
类型。在 Chisel 中,不能直接将时钟信号赋给布尔型输出。io.out := io.in
这行是可以正常工作的,尽管 io.in
是 UInt(2.W)
类型,而 io.out
是 Bool()
类型。Chisel 在赋值时会进行类型转换,这里会将 io.in
截断为 1 位以匹配 Bool()
的宽度。io.bundleOut := io.bundleIn
也是可以正常工作的代码。尽管 io.bundleIn
是 Bundle2
类型,而 io.bundleOut
是 Bundle1
类型,Chisel 在赋值时会进行宽松匹配(loose coupling),只连接两个 Bundle 中相同的字段。因为 Bundle2
继承自 Bundle1
,所以 Bundle1
中的字段 a
在两者间会被正确连接。val seq1 = Seq("1", "2", "3") // Type is Seq[String]
+val seq2 = Seq(1, 2, 3) // Type is Seq[Int]
+val seq3 = Seq(1, "2", true) // Type is Seq[Any]
+
有时需要用户指明多态类型
//val default = Seq() // Error!
+val default = Seq[String]() // User must tell compiler that default is of type Seq[String]
+Seq(1, "2", true).foldLeft(default){ (strings, next) =>
+ next match {
+ case s: String => strings ++ Seq(s)
+ case _ => strings
+ }
+}
+
这段 Scala 代码演示了如何使用 foldLeft
方法和模式匹配来从混合类型的序列中筛选出字符串类型的元素。我将逐步解释每一部分的功能和目的。
定义默认值:
初始的尝试定义 default
时出现错误,因为仅写 Seq()
没有足够的信息让编译器推断出序列的具体类型。这在类型推断的上下文中很重要。Scala 编译器需要明确的类型信息来保证类型安全和后续操作的正确性。
// val default = Seq() // Error!
+val default = Seq[String]() // 明确指定 default 为 Seq[String] 类型
+
+
正确的做法是使用 Seq[String]()
明确指定序列中包含的元素类型是 String
。这样,default
被明确为一个空的 String
类型序列。
创建混合类型序列并应用 foldLeft
:
Seq(1, "2", true).foldLeft(default){ (strings, next) =>
+ next match {
+ case s: String => strings ++ Seq(s)
+ case _ => strings
+ }
+}
+
+// output:
+default: Seq[String] = List()
+res17_1: Seq[String] = List("2")
+
这里,我们有一个包含整数、字符串和布尔值的序列 Seq(1, "2", true)
。目标是从中筛选出所有的字符串。
foldLeft
方法从 default
(空的字符串序列)开始,逐个处理原始序列中的元素。(strings, next) => ...
是一个函数,其中 strings
是累积结果(开始时是 default
),next
是当前遍历到的序列元素。next
的类型:
+next
是一个字符串(case s: String
),那么将这个字符串添加到累积结果 strings
中,并返回更新后的序列。next
不是字符串(case _
),仅返回当前的累积结果 strings
,不做任何改动。通过上述步骤,foldLeft
方法最终返回一个只包含原始序列中所有字符串的新序列。在这个例子中,它将构建并返回一个只包含 "2"
的 Seq[String]
。这段代码演示了如何结合使用 foldLeft
和模式匹配来处理和筛选混合类型的数据集合。
def time[T](block: => T): T = {
+ val t0 = System.nanoTime()
+ val result = block
+ val t1 = System.nanoTime()
+ val timeMillis = (t1 - t0) / 1000000.0
+ println(s"Block took $timeMillis milliseconds!")
+ result
+}
+
+// Adds 1 through a million
+val int = time { (1 to 1000000).reduce(_ + _) }
+println(s"Add 1 through a million is $int")
+
+// Finds the largest number under a million that, in hex, contains "beef"
+val string = time {
+ (1 to 1000000).map(_.toHexString).filter(_.contains("beef")).last
+}
+println(s"The largest number under a million that has beef: $string")
+
这段代码定义了一个名为 time
的函数,用于测量任何 Scala 代码块的执行时间。然后,它演示了如何使用 time
函数来测量两个不同操作的执行时间:一个是将一百万个整数相加,另一个是在一百万个整数中查找以十六进制表示时包含字符串 "beef" 的最大数。下面是对每个部分的详细解释:
时间测量函数 time
:
def time[T](block: => T): T = {
+ val t0 = System.nanoTime()
+ val result = block
+ val t1 = System.nanoTime()
+ val timeMillis = (t1 - t0) / 1000000.0
+ println(s"Block took $timeMillis milliseconds!")
+ result
+}
+
+
time
函数接受一个代码块 block
并返回该代码块的执行结果。[T]
表示这是一个泛型函数,可以接受和返回任意类型的结果。
+这个 time
函数的定义利用了 Scala 中的两个高级特性:泛型和按名参数。我将分解这个函数定义,帮助你理解每个部分的含义。
+泛型 [T]
:
在这里,[T]
表示 time
函数是泛型的,即它可以接受并返回任意类型 T
的结果。这种定义方式使 time
函数非常灵活,因为它不限制代码块 block
的返回类型。例如,block
可以返回一个整数、字符串或任何其他类型的值,time
函数将相应地处理并返回相同的类型。
按名参数 (block: => T)
:
(block: => T)
定义了一个按名参数 block
。按名参数与普通(按值)参数不同,因为它不会在传递到函数时立即求值。相反,每次在函数体内访问 block
时,都会执行代码块并计算其结果。
这里的 block: => T
意味着 block
是一个将被延迟执行的代码块,而不是一个已经计算好的值。这允许 time
函数首先记录执行前的时间,然后执行 block
,最后记录执行后的时间,从而测量出 block
的执行时间。
+总结一下,def time[T](block: => T): T = { ... }
的含义是:定义一个名为 time
的泛型函数,它接受一个将被延迟执行的代码块 block
作为输入,并返回 block
执行的结果。block
可以是任意复杂的表达式或操作,time
函数将测量并打印出其执行时间,最终返回 block
的结果。这种函数在性能分析或需要测量某段代码执行时间的场景中非常有用。
System.nanoTime()
用于获取纳秒级的当前时间,从而计算代码块执行前后的时间差。result
变量存储代码块 block
的执行结果。result
。整数累加操作:
val int = time { (1 to 1000000).reduce(_ + _) }
+
+
time
函数测量 (1 to 1000000).reduce(_ + _)
的执行时间,该表达式计算从 1 到 1000000 的整数和。reduce(_ + _)
使用简化操作,将序列中的所有数字累加。查找特定十六进制字符串的操作:
val string = time {
+ (1 to 1000000).map(_.toHexString).filter(_.contains("beef")).last
+}
+
+
time
函数测量查找操作的执行时间。它将从 1 到 1000000 的整数转换为十六进制字符串,过滤出包含 "beef" 的字符串,然后获取满足条件的最大数。map(_.toHexString)
将每个整数转换为其十六进制表示形式。filter(_.contains("beef"))
过滤出包含 "beef" 的字符串。.last
获取满足条件的最后一个元素,即最大的一个。这两个 time
函数调用展示了如何在实际应用中测量不同代码块的执行时间,为性能分析提供了便捷的工具。最后,程序打印出每个操作的结果以及执行时间。
chisel3.Data
是 Chisel 硬件类型的基类。UInt
、SInt
、Vec
、Bundle
等都是 Data
的实例。Data
可以用在 IO 中,并支持 :=
、wires、regs 等操作。寄存器是 Chisel 中多态代码的一个很好的例子。可以查看 RegEnable
(带有 Bool
使能信号的寄存器)的实现这里 (opens new window)。apply 函数为 [T <: Data]
模板化,这意味着 RegEnable
将适用于所有 Chisel 硬件类型。一些操作只在 Bits
的子类型上定义,例如 +
操作。这就是为什么你可以对 UInt
或 SInt
进行加法操作,但不能对 Bundle
或 Vec
进行加法操作的原因。
在本例中,我们希望能够在一个捆绑(Bundle
)中放置各种对象,能够使用 :=
连接它们,并且能够用它们创建寄存器(RegNext
等)。这些操作不能对任意对象执行;例如,wire := 3
是非法的,因为 3
是一个 Scala 的 Int
类型,而不是 Chisel 的 UInt
类型。如果我们使用一个类型约束来指明类型 T
是 Data
的一个子类,那么我们就可以对任何类型为 T
的对象使用 :=
,因为所有的 Data
类型都定义了 :=
操作。
class ShiftRegisterIO[T <: Data](gen: T, n: Int) extends Bundle {
+ require (n >= 0, "Shift register must have non-negative shift")
+
+ val in = Input(gen)
+ val out = Output(Vec(n + 1, gen)) // + 1 because in is included in out
+ override def cloneType: this.type = (new ShiftRegisterIO(gen, n)).asInstanceOf[this.type]
+}
+
+class ShiftRegister[T <: Data](gen: T, n: Int) extends Module {
+ val io = IO(new ShiftRegisterIO(gen, n))
+
+ io.out.foldLeft(io.in) { case (in, out) =>
+ out := in
+ RegNext(in)
+ }
+}
+
这段代码定义了两个类:ShiftRegisterIO
和 ShiftRegister
。这两个类联合实现了一个泛型的移位寄存器,在 Chisel 中移位寄存器是一种常见的硬件结构,用于数据的时序延迟。这里使用了泛型编程来允许这个移位寄存器处理任何类型的 Data
。让我们逐步分析这两个类的定义和功能:
ShiftRegisterIO
类:
ShiftRegisterIO
是一个基于 Bundle
的 IO 接口定义,它被参数化以接受任何继承自 Data
的类型 T
以及整数 n
代表移位级数。
require (n >= 0, "Shift register must have non-negative shift")
: 这行代码确保传入的移位级数 n
是非负的,否则会在运行时抛出异常。val in = Input(gen)
: 定义了一个名为 in
的输入端口,其类型为 T
。val out = Output(Vec(n + 1, gen))
: 定义了一个名为 out
的输出端口,其类型为 Vec
,长度为 n + 1
,向量中的每个元素类型为 T
。长度为 n + 1
是因为输出向量包括输入和所有中间移位寄存器的状态。override def cloneType
: 这个方法确保了 Chisel 在需要复制或实例化 ShiftRegisterIO
对象时能正确处理类型信息。这是 Chisel 的类型系统要求的。ShiftRegister
类:
ShiftRegister
实现了泛型移位寄存器的功能。
val io = IO(new ShiftRegisterIO(gen, n))
: 这行代码实例化了上面定义的 ShiftRegisterIO
,作为模块的 IO 接口。io.out.foldLeft(io.in) { case (in, out) => ... }
: 这是移位寄存器的核心实现。这里使用了 foldLeft
方法来遍历输出向量 io.out
并依次连接寄存器。对于输出向量中的每一项:
+out := in
: 当前输出连接到当前输入。RegNext(in)
: 使用 RegNext
创建一个新的寄存器,其输入是 in
,输出成为下一次迭代的输入。在 foldLeft
中,每次迭代的结果通常用于更新累积值。但在这个特定的情况下,RegNext(in)
的结果(即下一时钟周期的 in
值)并没有直接赋给一个命名的变量;相反,它隐式地成为下一次迭代中 in
参数的值。最终结果是,输入 io.in
通过一系列寄存器传递,每个寄存器的输出连接到下一个寄存器的输入,形成了一个链式结构。io.out
向量包含了这个链上的所有节点,因此它能够提供输入信号的每一步移位结果。
通过这种方式,ShiftRegister
类可以用来创建任何数据类型 T
的移位寄存器,其中 T
必须是 Data
的子类。这样的设计使得 ShiftRegister
非常灵活,能够被用在多种不同的数据路径和时序电路设计中。
+ ← + + 2. 2024.02.21-Chisel +
+ The Missing Semester + + → +
int main(int argc, char *argv[])
+{
+ puts("Hello world.");
+
+ return 0;
+}
+
代码段没有显式包含#include <stdio.h>
,但是仍然能够运行,并成功调用puts
函数输出"Hello world."。这是因为,在某些编译器和开发环境中,即使没有显式地包含标准输入输出头文件stdio.h
,编译器也可能会隐式地包含或者提前编译了这些基本的库函数。
具体来说,puts
函数是 C 语言标准库中负责输出字符串到标准输出的函数,它定义在stdio.h
中。按照 C 语言标准,使用puts
或者其他标准库函数时,应该包含相应的头文件。不这样做是不符合标准的,可能导致编译器警告或错误。
然而,一些编译器为了方便开发者,可能会有以下行为:
stdio.h
,编译器也会处理像puts
这样的标准库函数调用。不过,依赖这种非标准的行为是不推荐的。为了确保代码的可移植性和避免潜在的问题,最佳实践是始终显式包含你在代码中用到的所有标准库头文件。这样做可以保证代码在不同的编译器和环境中都能正常工作。
对于这个程序,打开所有编译警告重新构建它:
$ rm ex1
+$ CFLAGS="-Wall" make ex1
+cc -Wall ex1.c -o ex1
+ex1.c: Infunction 'main':
+ex1.c:3: warning: implicit declaration offunction 'puts'
+$ ./ex1
+Hello world.
+
CFLAGS="-Wall"
:这部分设置了一个环境变量CFLAGS
,它是用于 C 编译器的标志(flags)集合。在这个特定的例子中,-Wall
选项被设置给CFLAGS
。-Wall
是一个编译器标志,告诉编译器生成所有警告信息,帮助开发者发现代码中的潜在问题。这个标志对于提高代码质量和发现潜在的错误非常有用。
现在你会得到一个警告,说puts
函数是隐式声明的。C 语言的编译器很智能,它能够理解你想要什么。但是如果可以的话,你应该去除所有编译器警告。把下面一行添加到ex1.c
文件的最上面,之后重新编译来去除它:
#include <stdio.h>
+
在你的文本编辑器中打开ex1
文件,随机修改或删除一部分,之后运行它看看发生了什么。
再多打印 5 行文本或者其它比"Hello world."
更复杂的东西。
在 C 语言中,char *argv[]
是main
函数的一个参数,它表示传递给程序的命令行参数的数组。让我们分解一下这个声明,以便更好地理解它:
char
:表示数组中的元素是字符类型。*
:这是一个指针符号,表明这个变量是一个指针。argv[]
:这是一个数组符号,表明这个变量是一个数组。结合前面的指针符号,这意味着argv
是一个指向指针的数组,或者更准确地说,是一个指向字符指针的数组。所以,char *argv[]
是一个指向字符指针数组的指针,每个字符指针指向一个字符串。这些字符串是命令行参数,即在命令行中执行程序时输入的参数。
在main
函数的上下文中,argc
(argument count)是一个整型(int
)变量,它表示命令行参数的数量,包括程序名本身。argv
(argument vector)是一个指向字符串数组的指针,存储了所有的命令行参数。argv[0]
通常是程序的名称,argv[1]
是传递给程序的第一个参数,依此类推。argv[argc]
是NULL
,标记数组的结束。
举例来说,如果你在命令行中运行程序如下:
./myprogram arg1 arg2
+
这里,argc
将是3
(因为有三个命令行参数:./myprogram
, arg1
, 和 arg2
),而argv
数组将包含以下内容:
argv[0]
将是字符串 "./myprogram"
,指向程序的名称。argv[1]
将是字符串 "arg1"
,指向第一个命令行参数。argv[2]
将是字符串 "arg2"
,指向第二个命令行参数。argv[3]
将是NULL
,标记数组的结束。通过使用argc
和argv
,C 程序可以接收和处理用户在命令行中输入的参数。
执行man 3 puts
来阅读这个函数和其它函数的文档。
在 UNIX 或类 UNIX 系统中,man
命令用于查看手册页(manual pages),它是系统文档的一个重要组成部分。手册页按照不同的部分组织,每个部分涵盖了特定类型的命令或信息。当你在命令行中输入man 3 puts
时,3
指的是你想要查看第三部分中puts
函数的手册页。
手册页的部分主要包括:
所以,man 3 puts
命令表示你请求查看第三部分(库调用)中关于puts
函数的文档。puts
是标准 C 库的一部分,用于向标准输出写入一个字符串,这就是为什么它位于第三部分。这种组织方法让用户可以快速找到关于不同类型命令和函数的文档,即使它们的名字相同(例如,一个是用户命令,另一个是系统调用)。
make ex1
+CFLAGS="-Wall" make ex1
+
第一个命令中你告诉 make,“我想创建名为 ex1 的文件”。于是 Make 执行下面的动作:
ex1
存在吗?ex1
开头?ex1.c
。我知道如何构建.c
文件吗?cc ex1.c -o ex1
来构建它。cc
从ex1.c
文件来为你构建ex1
。上面列出的第二条命令是一种向 make 命令传递“修改器”的途径。如果你不熟悉 Unix shell 如何工作,你可以创建这些“环境变量”,它们会在程序运行时生效。有时你会用一条类似于export CFLAGS="-Wall"
的命令来执行相同的事情,取决于你所用的 shell。然而你可以仅仅把它们放到你想执行的命令前面,于是环境变量只会在程序运行时有效。
在这个例子中我执行了CFLAGS="-Wall" make ex1
,所以它会给 make 通常使用的cc
命令添加-Wall
选项。这行命令告诉cc
编译器要报告所有的警告(然而实际上不可能报告所有警告)。
实际上你可以深入探索使用 make 的上述方法,但是先让我们来看看Makefile
,以便让你对 make 了解得更多一点。首先,创建文件并写入以下内容:
CFLAGS=-Wall -g
+
+clean:
+ rm -f ex1
+
将文件在你的当前文件夹上保存为Makefile
。Make 会自动假设当前文件夹中有一个叫做Makefile
的文件,并且会执行它。此外,一定要注意:确保你只输入了 TAB 字符,而不是空格和 TAB 的混合。
Makefile
向你展示了 make 的一些新功能。首先我们在文件中设置CFLAGS
,所以之后就不用再设置了。并且,我们添加了-g
标识来获取调试信息。接着我们写了一个叫做clean
的部分,它告诉 make 如何清理我们的小项目。
确保它和你的ex1.c
文件在相同的目录中,之后运行以下命令:
make clean
+make ex1
+
目标 ... : 依赖 ...
+ 命令1
+ 命令2
+ . . .
+
Makefile 的核心规则,类似于一位厨神做菜,目标就是做好一道菜,那么所谓的依赖就是各种食材,各种厨具等等,然后需要厨师好的技术方法类似于命令,才能作出一道好菜。同时这些依赖也有可能此时并不存在,需要现场制作,或者是由其他厨师做好,那么这个依赖就成为了其他规则的目标,该目标也会有他自己的依赖和命令。这样就形成了一层一层递归依赖组成了 Makefile 文件。Makefile 并不会关心命令是如何执行的,仅仅只是会去执行所有定义的命令,和我们平时直接输入命令行是一样的效果。
1、目标即要生成的文件。如果目标文件的更新时间晚于依赖文件更新时间,则说明依赖文件没有改动,目标文件不需要重新编译。否则会进行重新编译并更新目标文件。
2、默认情况下 Makefile 的第一个目标为终极目标。
3、依赖:即目标文件由哪些文件生成。
4、命令:即通过执行命令由依赖文件生成目标文件。注意每条命令之前必须有一个 tab 保持缩进,这是语法要求(会有一些编辑工具默认 tab 为 4 个空格,会造成 Makefile 语法错误)。
5、all:Makefile 文件默认只生成第一个目标文件即完成编译,但是我们可以通过 all 指定所需要生成的目标文件。
$
符号表示取变量的值,当变量名多于一个字符时,使用"( )"
$
符的其他用法
$^
表示所有的依赖文件
$@
表示生成的目标文件
$<
代表第一个依赖文件
SRC = $(wildcard *.c)
+OBJ = $(patsubst %.c, %.o, $(SRC))
+
+ALL: hello.out
+
+hello.out: $(OBJ)
+ gcc $^ -o $@
+
+$(OBJ): $(SRC)
+ gcc -c $^ -o $@
+
1、"="是最普通的等号,在 Makefile 中容易搞错赋值等号,使用 “=”进行赋值,变量的值是整个 Makefile 中最后被指定的值。
VIR_A = A
+VIR_B = $(VIR_A) B
+VIR_A = AA
+
经过上面的赋值后,最后 VIR_B 的值是 AA B,而不是 A B,在 make 时,会把整个 Makefile 展开,来决定变量的值
2、":=" 表示直接赋值,赋予当前位置的值。
VIR_A := A
+VIR_B := $(VIR_A) B
+VIR_A := AA
+
最后 BIR_B 的值是 A B,即根据当前位置进行赋值。因此相当于“=”,“:=”才是真正意义上的直接赋值
3、"?=" 表示如果该变量没有被赋值,赋值予等号后面的值。
VIR ?= new_value
+
如果 VIR 在之前没有被赋值,那么 VIR 的值就为 new_value。
VIR := old_value
+VIR ?= new_value
+
这种情况下,VIR 的值就是 old_value
4、"+="和平时写代码的理解是一样的,表示将符号后面的值添加到前面的变量上
CC:c 编译器的名称,默认值为 cc。cpp c 预编译器的名称默认值为$(CC) -E
CC = gcc
+
回显问题,Makefile 中的命令都会被打印出来。如果不想打印命令部分 可以使用@去除回显
@echo "clean done!"
+
@
符号:在Makefile
中,当你在命令行前加上@
符号,它告诉make
在执行这个命令时不要将命令本身输出到标准输出(即不在控制台显示命令)。通常,make
会打印每个命令到标准输出,然后执行它。通过在命令前加上@
符号,你可以避免显示命令,只显示命令的输出或者执行结果。这可以让你的构建输出看起来更简洁。
通配符 SRC = $(wildcard ./*.c)
匹配目录下所有.c 文件,并将其赋值给 SRC 变量。
OBJ = $(patsubst %.c, %.o, $(SRC))
这个函数有三个参数,意思是取出 SRC 中的所有值,然后将.c 替换为.o 最后赋值给 OBJ 变量。
示例:如果目录下有很多个.c 源文件,就不需要写很多条规则语句了,而是可以像下面这样写
SRC = $(wildcard *.c)
+OBJ = $(patsubst %.c, %.o, $(SRC))
+
+ALL: hello.out
+
+hello.out: $(OBJ)
+ gcc $(OBJ) -o hello.out
+
+$(OBJ): $(SRC)
+ gcc -c $(SRC) -o $(OBJ)
+
这里先将所有.c 文件编译为 .o 文件,这样后面更改某个 .c 文件时,其他的 .c 文件将不在编译,而只是编译有更改的 .c 文件,可以大大提高大项目中的编译速度。
伪目标只是一个标签,clean 是个伪目标没有依赖文件,只有用 make 来调用时才会执行
当目录下有与 make 命令 同名的文件时 执行 make 命令就会出现错误。
解决办法就是使用伪目标
SRC = $(wildcard *.c)
+OBJ = $(patsubst %.c, %.o, $(SRC))
+
+ALL: hello.out
+
+hello.out: $(OBJ)
+ gcc $< -o $@
+
+$(OBJ): $(SRC)
+ gcc -c $< -o $@
+
+clean:
+ rm -rf $(OBJ) hello.out
+
+.PHONY: clean ALL
+
通常也会把 ALL 设置成伪目标
代码清理 clean
我们可以编译一条属于自己的 clean 语句,来清理 make 命令所产生的所有文件,列如
SRC = $(wildcard *.c)
+OBJ = $(patsubst %.c, %.o, $(SRC))
+
+ALL: hello.out
+
+hello.out: $(OBJ)
+ gcc $< -o $@
+
+$(OBJ): $(SRC)
+ gcc -c $< -o $@
+
+clean:
+ rm -rf $(OBJ) hello.out
+
在一些大工程中,会把不同模块或不同功能的源文件放在不同的目录中,我们可以在每个目录中都写一个该目录的 Makefile 这有利于让我们的 Makefile 变的更加简洁,不至于把所有东西全部写在一个 Makefile 中。
列如在子目录 subdir 目录下有个 Makefile 文件,来指明这个目录下文件的编译规则。外部总 Makefile 可以这样写
subsystem:
+ cd subdir && $(MAKE)
+其等价于:
+subsystem:
+ $(MAKE) -C subdir
+
定义$(MAKE)宏变量的意思是,也许我们的 make 需要一些参数,所以定义成一个变量比较有利于维护。两个例子意思都是先进入"subdir"目录,然后执行 make 命令
我们把这个 Makefile 叫做总控 Makefile,总控 Makefile 的变量可以传递到下级的 Makefile 中,但是不会覆盖下层 Makefile 中所定义的变量,除非指定了 "-e"参数。
如果传递变量到下级 Makefile 中,那么可以使用这样的声明 export
如果不想让某些变量传递到下级 Makefile,可以使用 unexport
export variable = value
+等价于
+variable = value
+export variable
+等价于
+export variable := value
+等价于
+variable := value
+export variable
+如果需要传递所有变量,那么只要一个export就行了。后面什么也不用跟,表示传递所有变量
+
一般都是通过"-I"(大写 i)来指定,假设头文件在: /home/develop/include
则可以通过-I 指定: -I/home/develop/include
将该目录添加到头文件搜索路径中
在 Makefile 中则可以这样写:
CFLAGS=-I/home/develop/include
+
然后在编译的时候,引用 CFLAGS 即可,如下
yourapp:*.c
+ gcc $(CFLAGS) -o yourapp
+
all:ex1
,可以以单个命令make
构建ex1
CC=gcc
+
+CFLAGS=-Wall -g
+
+all:ex1
+
+ex1:ex1.o
+ $(CC) $(CFLAGS) ex1.o -o ex1
+
+ex1.o:ex1.c
+ $(CC) $(CFLAGS) -c ex1.c -o ex1.o
+
+clean:
+ rm -f ex1 ex1.o
+.PHONY: all clean
+
man cc
来了解关于-Wall
和-g
行为的更多信息-Wall
Wall
是一个编译器选项,代表“warn all”,告诉编译器产生尽可能多的警告信息。尽管名称暗示它会启用所有警告,实际上它只启用了最常见的一组警告。这些警告可以帮助开发者发现代码中的潜在问题,比如变量未使用、可能的数据类型不匹配、未初始化的变量等。Wall
是一种很好的做法,因为它可以帮助你提前识别潜在的错误或不一致,从而提高代码质量。在开发过程中,尽量解决所有Wall
引发的警告,可以避免未来发生更复杂的问题。-g
g
选项用于在编译时生成调试信息。这些调试信息包括了程序中的变量、函数、类等符号的名称和类型信息,以及它们在源代码中的位置。这使得调试器(如 GDB)能够理解程序的结构,允许开发者进行断点调试、单步执行、查看变量值等调试操作。g
选项的情况下编译的程序仍然可以运行,但如果需要调试,缺少调试信息会让这一过程变得非常困难。因此,开发阶段建议总是加上g
选项来编译程序,以便在遇到问题时能够更容易地调试。g
选项编译的程序会因为包含了额外的调试信息而变得更大。在发布产品时,通常会移除调试信息(不使用g
选项或使用像s
这样的选项来剔除调试信息),以减小程序的大小和提高运行时性能。+ ← + + 2024.04.02-练习 1:启用编译器 + + 2024.04.02-练习3:格式化输出 + + → +
#include <stdio.h>
+
+int main()
+{
+ int age = 10;
+ int height = 72;
+
+ printf("I am %d years old.\n", age);
+ printf("I am %d inches tall.\n", height);
+
+ return 0;
+}
+
这个练习的代码量很小,但是信息量很大,所以让我们逐行分析一下:
stdio.h
。这告诉了编译器你要使用“标准的输入/输出函数”。它们之一就是printf
。age
的变量并且将它设置为10。height
的变量并且设置为72。printf
函数来打印这个星球上最高的十岁的人的年龄和高度。printf
中你会注意到你传入了一个字符串,这就是格式字符串,和其它语言中一样。printf
“替换”进格式字符串中。这些语句的结果就是你用printf
处理了一些变量,并且它会构造出一个新的字符串,之后将它打印在终端上。
常用的%
占位符
%d
或 %i
:用于输出int
类型的整数。%u
:用于输出无符号整型unsigned int
。%f
:用于输出单精度浮点数或双精度浮点数(float
或double
),默认情况下显示六位小数。%lf
:用于输出双精度浮点数(double
),虽然%f
也可以用于double
,但在某些情况下使用%lf
更加明确。%e
或 %E
:用科学记数法输出浮点数。%g
或 %G
:自动选择%f
或%e
(%E
)中较短的一种输出浮点数。%c
:输出单个字符。%s
:输出字符串。%p
:输出指针的地址。%x
或 %X
:以十六进制形式输出无符号整数,%x
产生小写字母,而%X
产生大写字母。%%
:输出%
字符本身。转义字符
转义字符用于表示那些不能直接在源代码中表示的字符,或者具有特殊意义的字符。以下是一些常用的转义字符:
\n
:换行符,移动到下一行的开头。\t
:水平制表符,常用于输出中的对齐。\\
:表示一个反斜杠字符\
。\"
:表示双引号字符"
,允许在字符串常量中使用双引号。\'
:表示单引号字符'
(在字符常量中使用)。\r
:回车符,将光标移动到当前行的开头。\b
:退格符,将光标向左移动一个位置。\0
:空字符,字符串的结束标志。对于浮点数:
%.*f
:对于float
和double
类型,.*
允许你通过参数指定小数点后的位数。例如,printf("%.*f", 3, 3.14159)
会输出3.142
。%.*e
或 %.*E
:以科学记数法格式输出浮点数,其中.*
指定小数点后的位数。%.*g
或 %.*G
:自动选择%f
或%e
(%E
)中较短的一种输出浮点数,.*
指定有效数字的最大位数。对于整数:
%.*d
或 %.*i
:虽然通常用于指定最小宽度,但通过指定精度,可以使得输出的整数在前面用零填充到指定的长度。例如,printf("%.*d", 5, 123)
会输出00123
。对于字符串:
%.*s
:指定字符串的最大输出长度。这对于输出字符串的一个子串非常有用。例如,printf("%.*s", 3, "abcdef")
会输出abc
。CC=gcc
+CFLAGS=-Wall -g
+
+TARGET=ex3
+
+all: $(TARGET)
+
+$(TARGET): $(TARGET).o
+ $(CC) $(CFLAGS) $^ -o $@
+
+$(TARGET).o: $(TARGET).c
+ $(CC) $(CFLAGS) -c $^ -o $@
+
+clean:
+ rm -f $(TARGET) $(TARGET).o
+
+.PHONY: all clean
+
Valgrind是一个编程工具,用于内存调试、内存泄漏检测,以及性能分析。它主要用于帮助开发者找出程序中的内存管理和线程使用错误,是Linux和macOS下常用的工具之一。Valgrind通过一个核心,提供了多种不同的工具,其中最著名的是Memcheck。Memcheck可以检测以下问题:
malloc
和free
等堆管理函数的误用使用方法
使用Valgrind的基本语法很简单。首先,确保你的程序是用调试信息编译的(通常是使用gcc
的-g
选项)。然后,使用下面的命令行格式运行Valgrind:
valgrind [options] your_program [program_options]
+
其中,[options]
是传递给Valgrind的选项(例如,选择不同的工具),your_program
是你的程序的路径,[program_options]
是传递给你的程序的任何选项。
示例:使用Memcheck检测内存泄漏
假设你的可执行文件名为my_program
,要使用Memcheck工具(Valgrind的默认工具),可以这样做:
valgrind --leak-check=full ./my_program
+
-leak-check=full
选项告诉Memcheck提供每个内存泄漏的详细信息。Valgrind运行后,会在终端输出报告,其中包含了内存泄漏的信息、未初始化变量的使用等问题。
#include <stdio.h>
+
+/* Warning: This program is wrong on purpose. */
+
+int main()
+{
+ int age = 10;
+ int height;
+
+ printf("I am %d years old.\n");
+ printf("I am %d inches tall.\n", height);
+
+ return 0;
+}
+
使用 valgrind 运行,结果如下
==431815== Memcheck, a memory error detector
+==431815== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
+==431815== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
+==431815== Command: ./ex4
+==431815==
+==431815== Conditional jump or move depends on uninitialised value(s)
+==431815== at 0x48EAAD6: __vfprintf_internal (vfprintf-internal.c:1516)
+==431815== by 0x48D479E: printf (printf.c:33)
+==431815== by 0x109188: main (ex4.c:8)
+==431815== Uninitialised value was created by a stack allocation
+==431815== at 0x109149: main (ex4.c:3)
+==431815==
+==431815== Use of uninitialised value of size 8
+==431815== at 0x48CE2EB: _itoa_word (_itoa.c:177)
+==431815== by 0x48E9ABD: __vfprintf_internal (vfprintf-internal.c:1516)
+==431815== by 0x48D479E: printf (printf.c:33)
+==431815== by 0x109188: main (ex4.c:8)
+==431815== Uninitialised value was created by a stack allocation
+==431815== at 0x109149: main (ex4.c:3)
+==431815==
+==431815== Conditional jump or move depends on uninitialised value(s)
+==431815== at 0x48CE2FC: _itoa_word (_itoa.c:177)
+==431815== by 0x48E9ABD: __vfprintf_internal (vfprintf-internal.c:1516)
+==431815== by 0x48D479E: printf (printf.c:33)
+==431815== by 0x109188: main (ex4.c:8)
+==431815== Uninitialised value was created by a stack allocation
+==431815== at 0x109149: main (ex4.c:3)
+==431815==
+==431815== Conditional jump or move depends on uninitialised value(s)
+==431815== at 0x48EA5C3: __vfprintf_internal (vfprintf-internal.c:1516)
+==431815== by 0x48D479E: printf (printf.c:33)
+==431815== by 0x109188: main (ex4.c:8)
+==431815== Uninitialised value was created by a stack allocation
+==431815== at 0x109149: main (ex4.c:3)
+==431815==
+==431815== Conditional jump or move depends on uninitialised value(s)
+==431815== at 0x48E9C05: __vfprintf_internal (vfprintf-internal.c:1516)
+==431815== by 0x48D479E: printf (printf.c:33)
+==431815== by 0x109188: main (ex4.c:8)
+==431815== Uninitialised value was created by a stack allocation
+==431815== at 0x109149: main (ex4.c:3)
+==431815==
+I am -16778360 years old.
+I am 0 inches tall.
+==431815==
+==431815== HEAP SUMMARY:
+==431815== in use at exit: 0 bytes in 0 blocks
+==431815== total heap usage: 1 allocs, 1 frees, 4,096 bytes allocated
+==431815==
+==431815== All heap blocks were freed -- no leaks are possible
+==431815==
+==431815== For lists of detected and suppressed errors, rerun with: -s
+==431815== ERROR SUMMARY: 5 errors from 5 contexts (suppressed: 0 from 0)
+
by 0x109188: main (ex4.c:8):第八行 printf("I am %d inches tall.\n", height);
中有未初始化的量
+ ← + + 2024.04.02-练习3:格式化输出 + + 2024.04.03-练习5:一个C程序的结构 + + → +
include <stdio.h>
+
+int main(int argc, char *argv[])
+{
+ int distance = 100;
+ float power = 2.345f;
+ double super_power = 56789.4532;
+ char initial = 'A';
+ char first_name[] = "Zed";
+ char last_name[] = "Shaw";
+
+ printf("You are %d miles away.\n", distance);
+ printf("You have %f levels of power.\n", power);
+ printf("You have %f awesome super powers.\n", super_power);
+ printf("I have an initial %c.\n", initial);
+ printf("I have a first name %s.\n", first_name);
+ printf("I have a last name %s.\n", last_name);
+ printf("My whole name is %s %c. %s.\n",
+ first_name, initial, last_name);
+
+ return 0;
+}
+
整数:使用int
声明,使用%d
来打印。
浮点:使用float
或double
声明,使用%f
来打印。
字符:使用char
来声明,以周围带有'
(单引号)的单个字符来表示,使用%c
来打印。
字符串(字符数组):使用char name[]
来声明,以周围带有"
的一些字符来表示,使用%s
来打印。 你会注意到C语言中区分单引号的char
和双引号的char[]
或字符串。
你可以通过向printf
传递错误的参数来轻易使这个程序崩溃。例如,如果你找到打印我的名字的那行,把initial
放到first_name
前面,你就制造了一个bug。执行上述修改编译器就会向你报错,之后运行的时候你可能会得到一个“段错误”,就像这样:
==453551== Memcheck, a memory error detector
+==453551== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
+==453551== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
+==453551== Command: ./ex6
+==453551==
+==453551== Invalid read of size 1
+==453551== at 0x484ED16: strlen (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
+==453551== by 0x48EAD30: __vfprintf_internal (vfprintf-internal.c:1517)
+==453551== by 0x48D479E: printf (printf.c:33)
+==453551== by 0x109290: main (ex6.c:18)
+==453551== Address 0x41 is not stack'd, malloc'd or (recently) free'd
+==453551==
+==453551==
+==453551== Process terminating with default action of signal 11 (SIGSEGV)
+==453551== Access not within mapped region at address 0x41
+==453551== at 0x484ED16: strlen (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
+==453551== by 0x48EAD30: __vfprintf_internal (vfprintf-internal.c:1517)
+==453551== by 0x48D479E: printf (printf.c:33)
+==453551== by 0x109290: main (ex6.c:18)
+==453551== If you believe this happened as a result of a stack
+==453551== overflow in your program's main thread (unlikely but
+==453551== possible), you can try to increase the size of the
+==453551== main thread stack using the --main-stacksize= flag.
+==453551== The main thread stack size used in this run was 8388608.
+==453551==
+==453551== HEAP SUMMARY:
+==453551== in use at exit: 4,096 bytes in 1 blocks
+==453551== total heap usage: 1 allocs, 0 frees, 4,096 bytes allocated
+==453551==
+==453551== LEAK SUMMARY:
+==453551== definitely lost: 0 bytes in 0 blocks
+==453551== indirectly lost: 0 bytes in 0 blocks
+==453551== possibly lost: 0 bytes in 0 blocks
+==453551== still reachable: 4,096 bytes in 1 blocks
+==453551== suppressed: 0 bytes in 0 blocks
+==453551== Rerun with --leak-check=full to see details of leaked memory
+==453551==
+==453551== For lists of detected and suppressed errors, rerun with: -s
+==453551== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
+
by 0x109290: main (ex6.c:18)
显示 printf("My whole name is %s %c. %s.\n",
这一行有问题
""
char *null = "";
+printf("null is \"%s\"\n", null);
+
int main(int argc, char *argv[])
+{
+ int bugs = 100;
+ double bug_rate = 1.2;
+
+ printf("You have %d bugs at the imaginary rate of %f.\n",
+ bugs, bug_rate);
+
+ long universe_of_defects = 1L * 1024L * 1024L * 1024L;
+ printf("The entire universe has %ld bugs.\n",
+ universe_of_defects);
+
+ double expected_bugs = bugs * bug_rate;
+ printf("You are expected to have %f bugs.\n",
+ expected_bugs);
+
+ double part_of_universe = expected_bugs / universe_of_defects;
+ printf("That is only a %e portion of the universe.\n",
+ part_of_universe);
+
+ // this makes no sense, just a demo of something weird
+ char nul_byte = '\0';
+ int care_percentage = bugs * nul_byte;
+ printf("Which means you should care %d%%.\n",
+ care_percentage);
+
+ return 0;
+}
+
以特殊的语法'\0'
声明了一个字符。这样创建了一个“空字节”字符,实际上是数字0。
会发生溢出,到负的那边循环
unsigned的范围是signed的最大值的两倍加一
因为char字符都有ASCII编码,所以可以被认为是int
+ ← + + 2024.04.03-练习6:变量类型 + + 2024.04.03-练习8:大小和数组 + + → +
sizeof
和数组#include <stdio.h>
+
+int main(int argc, char *argv[])
+{
+ int areas[] = {10, 12, 13, 14, 20};
+ char name[] = "Zed";
+ char full_name[] = {
+ 'Z', 'e', 'd',
+ ' ', 'A', '.', ' ',
+ 'S', 'h', 'a', 'w', '\0'
+ };
+
+ // WARNING: On some systems you may have to change the
+ // %ld in this code to a %u since it will use unsigned ints
+ printf("The size of an int: %ld\n", sizeof(int));
+ printf("The size of areas (int[]): %ld\n",
+ sizeof(areas));
+ printf("The number of ints in areas: %ld\n",
+ sizeof(areas) / sizeof(int));
+ printf("The first area is %d, the 2nd %d.\n",
+ areas[0], areas[1]);
+
+ printf("The size of a char: %ld\n", sizeof(char));
+ printf("The size of name (char[]): %ld\n",
+ sizeof(name));
+ printf("The number of chars: %ld\n",
+ sizeof(name) / sizeof(char));
+
+ printf("The size of full_name (char[]): %ld\n",
+ sizeof(full_name));
+ printf("The number of chars: %ld\n",
+ sizeof(full_name) / sizeof(char));
+
+ printf("name=\"%s\" and full_name=\"%s\"\n",
+ name, full_name);
+
+ return 0;
+}
+
full_name
最后的'\0'
去掉\0
终止符的后果:标准的字符串处理函数(如printf
的%s
格式化输出、strcpy
、strlen
等)期望字符串以\0
终止。如果省略了\0
,这些函数会继续读取内存,直到偶然遇到一个\0
字节。这可能导致读取数组后面的内存,结果是不可预测的,可能会导致错误的输出、内存访问违规、甚至是程序崩溃。areas[0]
改为areas[10]
会打印出一个随机数
areas
的元素赋值name
的元素赋值areas
的一个元素赋值为name
中的字符1. 32位系统(如x86)
在32位系统上,通常遵循ILP32数据模型,其中:
int
通常是32位(4字节)。short
通常是16位(2字节)。long
也通常是32位(4字节)。long long
保证至少是64位(8字节)。2. 64位系统(如x86_64或AMD64)
在64位系统上,最常见的数据模型是LP64,其中:
int
保持32位(4字节)。short
仍然是16位(2字节)。long
和指针的大小增加到64位(8字节)。long long
保证至少是64位(8字节)。另一个在某些64位系统(如Windows的64位版本)上使用的数据模型是LLP64,它保持long
为32位,而只有指针和long long
是64位。
#include <stdio.h>
+
+int main(int argc, char *argv[])
+{
+ int numbers[4] = {0};
+ char name[4] = {'a'};
+
+ // first, print them out raw
+ printf("numbers: %d %d %d %d\n",
+ numbers[0], numbers[1],
+ numbers[2], numbers[3]);
+
+ printf("name each: %c %c %c %c\n",
+ name[0], name[1],
+ name[2], name[3]);
+
+ printf("name: %s\n", name);
+
+ // setup the numbers
+ numbers[0] = 1;
+ numbers[1] = 2;
+ numbers[2] = 3;
+ numbers[3] = 4;
+
+ // setup the name
+ name[0] = 'Z';
+ name[1] = 'e';
+ name[2] = 'd';
+ name[3] = '\0';
+
+ // then print them out initialized
+ printf("numbers: %d %d %d %d\n",
+ numbers[0], numbers[1],
+ numbers[2], numbers[3]);
+
+ printf("name each: %c %c %c %c\n",
+ name[0], name[1],
+ name[2], name[3]);
+
+ // print the name like a string
+ printf("name: %s\n", name);
+
+ // another way to use name
+ char *another = "Zed";
+
+ printf("another: %s\n", another);
+
+ printf("another each: %c %c %c %c\n",
+ another[0], another[1],
+ another[2], another[3]);
+
+ return 0;
+}
+
name
的初始化表达式error
name[3] = 'A'
{'a','a','a','a'}
numbers
的元素当用%c
格式化字符串来打印一个int
值时,编译器通常不会发出警告,因为%c
期望一个int
类型的参数(在大多数情况下,字符在传递给函数时会被提升为int
)。然而,如果数组中的整数值不对应于有效的ASCII字符编码,则打印的结果可能是乱码或不可预测的字符。
names
当成int
数组使用%d
格式化字符串来打印时,如果直接传递char
类型的值给printf
,由于char
到int
的隐式类型提升,这种类型不匹配通常不会导致编译器警告。但是,如果尝试直接以int
数组的方式访问char
数组(如通过类型转换或指针操作),并尝试打印,这将导致未定义的行为,特别是当char
数组的大小小于int
时。
如果一个字符数组占四个字节,一个整数也占4个字节,你可以像整数一样使用整个name吗?你如何用黑魔法实现它?
Zed\0
的ASCII码对应的十六进制分别是5a,65,64,0.然后考虑到计算机是小端序,所以name存储时最低位存的是Z,所以综合起来存储的就是0064655a,十进制就是6579546
+ ← + + 2024.04.03-练习8:大小和数组 + + 2024.04.04-练习10:字符串数组和循环 + + → +
#include <stdio.h>
+
+int main(int argc, char *argv[])
+{
+ int i = 0;
+
+ // go through each string in argv
+ // why am I skipping argv[0]?
+ for(i = 1; i < argc; i++) {
+ printf("arg %d: %s\n", i, argv[i]);
+ }
+
+ // let's make our own array of strings
+ char *states[] = {
+ "California", "Oregon",
+ "Washington", "Texas"
+ };
+ int num_states = 4;
+
+ for(i = 0; i < num_states; i++) {
+ printf("state %d: %s\n", i, states[i]);
+ }
+
+ return 0;
+}
+
argv[0]
是程序的名称
如果 num_states 大于4,会报段错误,因为这些索引超出了数组 states
****的初始化范围,它们没有被初始化指向任何有效的字符串。
for
循环的每一部分可以放置什么代码true
(非零),循环继续;如果为false
(零),循环结束。for
循环中的逗号for (int i = 0, j = 10; i < j; i++, j--) {}
+
NULL
在C语言中,NULL
是一个宏,通常用来表示指针不指向任何有效的对象或位置。它在多个头文件中被定义,如<stddef.h>
、<stdio.h>
、<stdlib.h>
等,通常被定义为((void *)0)
,即一个类型为void*
的空指针。
将NULL
用作字符指针数组states
的一个元素,相当于在数组中插入一个不指向任何有效字符串的指针。在打印时,尝试访问这个NULL
指针所指向的字符串将触发未定义行为(Undefined Behavior, UB),因为试图访问一个不存在的内存位置。在大多数情况下,这可能会导致程序崩溃,因为printf
的%s
格式化选项期望一个指向有效C字符串的指针。
while
#include <stdio.h>
+
+int main(int argc, char *argv[])
+{
+ // go through each string in argv
+
+ int i = 0;
+ while(i < argc) {
+ printf("arg %d: %s\n", i, argv[i]);
+ i++;
+ }
+
+ // let's make our own array of strings
+ char *states[] = {
+ "California", "Oregon",
+ "Washington", "Texas"
+ };
+
+ int num_states = 4;
+ i = 0; // watch for this
+ while(i < num_states) {
+ printf("state %d: %s\n", i, states[i]);
+ i++;
+ }
+
+ return 0;
+}
+
int i = argc;
+ while(i > 0) {
+ printf("arg %d: %s\n", i-1, argv[i-1]);
+ i--;
+ }
+
while
循环将argv
中的值复制到states
char *states[] = {
+ "California", "Oregon",
+ "Washington", "Texas"
+ };
+
+ int num_states = 4;
+ i = 0; // watch for this
+ // 让这个复制循环不会执行失败,即使argv之中有很多元素也不会全部放进states。
+ while(i < num_states && i < argc - 1) {
+ states[i] = argv[i+1];
+ i++;
+ }
+
当在C语言中将一个字符串数组(例如argv
或states
)的元素赋值给另一个数组的元素时,你实际上并没有复制字符串的内容。相反,你只是复制了字符串的指针,这意味着两个数组中的对应元素现在共享同一个字符串实例(即指向同一个内存地址)。这是因为在C中,字符串是通过字符指针表示的,赋值操作只涉及指针的复制,而非指针所指向的数据(字符串内容)的复制。
#include <stdio.h>
+
+int main(int argc, char *argv[])
+{
+ int i = 0;
+
+ if(argc == 1) {
+ printf("You only have one argument. You suck.\n");
+ } else if(argc > 1 && argc < 4) {
+ printf("Here's your arguments:\n");
+
+ for(i = 0; i < argc; i++) {
+ printf("%s ", argv[i]);
+ }
+ printf("\n");
+ } else {
+ printf("You have too many arguments. You suck.\n");
+ }
+
+ return 0;
+}
+
在C语言中,常用的布尔运算符包括"与"(&&
)、"或"(||
)和"非"(!
)。这些运算符用于构建逻辑表达式,从而控制程序流程如条件语句和循环。&&
运算符用于判断两个条件是否同时满足,||
检查至少一个条件是否满足,而!
用于反转一个条件的布尔值。由于C语言中没有内置的布尔类型,在使用这些运算符时,非零值被视为真(true
),而零值被视为假(false
)。
#include <stdio.h>
+
+int main(int argc, char *argv[])
+{
+ if(argc != 2) {
+ printf("ERROR: You need one argument.\n");
+ // this is how you abort a program
+ return 1;
+ }
+
+ int i = 0;
+ for(i = 0; argv[1][i] != '\0'; i++) {
+ char letter = argv[1][i];
+
+ switch(letter) {
+ case 'a':
+ case 'A':
+ printf("%d: 'A'\n", i);
+ break;
+
+ case 'e':
+ case 'E':
+ printf("%d: 'E'\n", i);
+ break;
+
+ case 'i':
+ case 'I':
+ printf("%d: 'I'\n", i);
+ break;
+
+ case 'o':
+ case 'O':
+ printf("%d: 'O'\n", i);
+ break;
+
+ case 'u':
+ case 'U':
+ printf("%d: 'U'\n", i);
+ break;
+
+ case 'y':
+ case 'Y':
+ if(i > 2) {
+ // it's only sometimes Y
+ printf("%d: 'Y'\n", i);
+ }
+ break;
+
+ default:
+ printf("%d: %c is not a vowel\n", i, letter);
+ }
+ }
+
+ return 0;
+}
+
在这个程序中我们接受了单一的命令行参数,并且用一种极其复杂的方式打印出所有原因,来向你演示switch
语句。下面是swicth
语句的工作原理:
swicth
语句的顶端,我们先把它记为地址Y。switch
中的表达式求值,产生一个数字。在上面的例子中,数字为argv[1]
中字母的原始的ASCLL码。case 'A'
的case
代码块翻译成这个程序中距离语句顶端的地址,所以case 'A'
就在Y + 'A'
处。Y+letter
位于switch
语句中,如果距离太远则会将其调整为Y+Default
。case
代码块中有break
而另外一些没有的原因。'a'
,那它就会跳到case 'a'
,它里面没有break
语句,所以它会贯穿执行底下带有代码和break
的case 'A'
。break
完全跳出switch
语句块。更常见的情况是,gcc会在空白处单独构建一张跳转表,各个偏移处存放对应的case
语句的地址。Y不是switch
语句的起始地址,而是这张表的起始地址。程序会跳转到*(Y + 'A')
而不是Y + 'A'
处。
default
在C语言中,如果switch
语句中没有写default
子句,那么当没有任何case
标签与switch
表达式的值匹配时,程序将跳过整个switch
块,继续执行switch
语句之后的代码。简而言之,没有匹配的case
且缺少default
时,switch
语句不执行任何操作。
switch
中这个值如果没有在任何case
标签中明确匹配到,且没有default
分支处理这种情况,switch
语句将不会执行任何操作,程序将跳过整个switch
块,继续执行下面的代码。
switch
中移除所有额外的大写字母if(letter >= 'A' && letter <= 'Z') {
+ letter += 32; // Convert to lowercase
+}
+
#include <stdio.h>
+#include <ctype.h>
+
+// forward declarations
+int can_print_it(char ch);
+void print_letters(char arg[]);
+
+void print_arguments(int argc, char *argv[])
+{
+ int i = 0;
+
+ for(i = 0; i < argc; i++) {
+ print_letters(argv[i]);
+ }
+}
+
+void print_letters(char arg[])
+{
+ int i = 0;
+
+ for(i = 0; arg[i] != '\0'; i++) {
+ char ch = arg[i];
+
+ if(can_print_it(ch)) {
+ printf("'%c' == %d ", ch, ch);
+ }
+ }
+
+ printf("\n");
+}
+
+// 属于“字母”和“空白”的字符
+int can_print_it(char ch)
+{
+ return isalpha(ch) || isblank(ch);
+}
+
+int main(int argc, char *argv[])
+{
+ print_arguments(argc, argv);
+ return 0;
+}
+
strlen
函数isdigit()
用于检查字符是否为数字(0-9),isspace()
用于检查空白字符(如空格、制表符、换行符),islower()
和isupper()
分别用于检查字符是否为小写或大写字母,isalnum()
用于检查字符是否为字母或数字。
#include <stdio.h>
+
+int main(int argc, char *argv[])
+{
+ // create two arrays we care about
+ int ages[] = {23, 43, 12, 89, 2};
+ char *names[] = {
+ "Alan", "Frank",
+ "Mary", "John", "Lisa"
+ };
+
+ // safely get the size of ages
+ int count = sizeof(ages) / sizeof(int);
+ int i = 0;
+
+ // first way using indexing
+ for(i = 0; i < count; i++) {
+ printf("%s has %d years alive.\n",
+ names[i], ages[i]);
+ }
+
+ printf("---\n");
+
+ // setup the pointers to the start of the arrays
+ int *cur_age = ages;
+ char **cur_name = names;
+
+ // second way using pointers
+ for(i = 0; i < count; i++) {
+ printf("%s is %d years old.\n",
+ *(cur_name+i), *(cur_age+i));
+ }
+
+ printf("---\n");
+
+ // third way, pointers are just arrays
+ for(i = 0; i < count; i++) {
+ printf("%s is %d years old again.\n",
+ cur_name[i], cur_age[i]);
+ }
+
+ printf("---\n");
+
+ // fourth way with pointers in a stupid complex way
+ for(cur_name = names, cur_age = ages;
+ (cur_age - ages) < count;
+ cur_name++, cur_age++)
+ {
+ printf("%s lived %d years so far.\n",
+ *cur_name, *cur_age);
+ }
+
+ return 0;
+}
+
char ** list;
那么list
是指向第一个字符串指针的指针。访问第一个字符串的第二个字符,可以使用表达式 *(*(list) + 1)
。
你可以用指针做下面四个最基本的操作:
对于你看到的其它所有情况,实际上应当使用数组。在早期,由于编译器不擅长优化数组,人们使用指针来加速它们的程序。然而,现在访问数组和指针的语法都会翻译成相同的机器码,并且表现一致。由此,你应该每次尽可能使用数组,并且按需将指针用作提升性能的手段。
现在我打算向你提供一个词库,用于读写指针。当你遇到复杂的指针语句时,试着参考它并且逐字拆分语句(或者不要使用这个语句,因为有可能并不好):
type *ptr
: type
类型的指针,名为ptr
。
*ptr
: ptr
所指向位置的值。*(ptr + i)
: (ptr
所指向位置加上i
)的值。译者注:以字节为单位的话,应该是ptr所指向的位置再加上
sizeof(type) * i
。
&thing
: thing
的地址。
type *ptr = &thing
: 名为ptr
,type
类型的指针,值设置为thing
的地址。 ptr++
自增ptr
指向的位置。
无论怎么样,你都不应该把指针和数组混为一谈。它们并不是相同的东西,即使C让你以一些相同的方法来使用它们。例如,如果你访问上面代码中的sizeof(cur_age)
,你会得到指针的大小,而不是它指向数组的大小。如果你想得到整个数组的大小,你应该使用数组的名称age
,就像第12行那样。
译者注,除了sizeof
、&
操作和声明之外,数组名称都会被编译器推导为指向其首个元素的指针。对于这些情况,不要用“是”这个词,而是要用“推导”。
#include <stdio.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+struct Person {
+ char *name;
+ int age;
+ int height;
+ int weight;
+};
+
+struct Person *Person_create(char *name, int age, int height, int weight)
+{
+ struct Person *who = malloc(sizeof(struct Person));
+ assert(who != NULL);
+
+ who->name = strdup(name);
+ who->age = age;
+ who->height = height;
+ who->weight = weight;
+
+ return who;
+}
+
+void Person_destroy(struct Person *who)
+{
+ assert(who != NULL);
+
+ free(who->name);
+ free(who);
+}
+
+void Person_print(struct Person *who)
+{
+ printf("Name: %s\n", who->name);
+ printf("\tAge: %d\n", who->age);
+ printf("\tHeight: %d\n", who->height);
+ printf("\tWeight: %d\n", who->weight);
+}
+
+int main(int argc, char *argv[])
+{
+ // make two people structures
+ struct Person *joe = Person_create(
+ "Joe Alex", 32, 64, 140);
+
+ struct Person *frank = Person_create(
+ "Frank Blank", 20, 72, 180);
+
+ // print them out and where they are in memory
+ printf("Joe is at memory location %p:\n", joe);
+ Person_print(joe);
+
+ printf("Frank is at memory location %p:\n", frank);
+ Person_print(frank);
+
+ // make everyone age 20 years and print them again
+ joe->age += 20;
+ joe->height -= 2;
+ joe->weight += 40;
+ Person_print(joe);
+
+ frank->age += 20;
+ frank->weight += 20;
+ Person_print(frank);
+
+ // destroy them both so we clean up
+ Person_destroy(joe);
+ Person_destroy(frank);
+
+ return 0;
+}
+
使用strdup
来复制字符串name
,是为了确保结构体真正拥有它。strdup
的行为实际上类似malloc
但是它同时会将原来的字符串复制到新创建的内存。
!!!如果结构体有指针类型成员,同时结构体在堆中创建的,那么释放堆中结构体之前需要提前释放结构体中的指针成员指向的内存,然后再释放结构体自身的。
assert(who != NULL);
试着传递NULL
给Person_destroy
来看看会发生什么。如果它没有崩溃,你必须移除Makefile的CFLAGS
中的g
选项。
在结尾处忘记调用Person_destroy
,在Valgrind
下运行程序,你会看到它报告出你忘记释放内存。弄清楚你应该向valgrind
传递什么参数来让它向你报告内存如何泄露。
忘记在Person_destroy
中释放who->name
,并且对比两次的输出。同时,使用正确的选项来让Valgrind
告诉你哪里错了。
这一次,向Person_print
传递NULL
,并且观察Valgrind
会输出什么。
#include <stdio.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+
+#define MAX_DATA 512
+#define MAX_ROWS 100
+
+struct Address {
+ int id;
+ int set;
+ char name[MAX_DATA];
+ char email[MAX_DATA];
+};
+
+struct Database {
+ struct Address rows[MAX_ROWS];
+};
+
+struct Connection {
+ FILE *file;
+ struct Database *db;
+};
+
+void die(const char *message)
+{
+ if(errno) {
+ perror(message);
+ } else {
+ printf("ERROR: %s\n", message);
+ }
+
+ exit(1);
+}
+
+void Address_print(struct Address *addr)
+{
+ printf("%d %s %s\n",
+ addr->id, addr->name, addr->email);
+}
+
+void Database_load(struct Connection *conn)
+{
+ int rc = fread(conn->db, sizeof(struct Database), 1, conn->file);
+ if(rc != 1) die("Failed to load database.");
+}
+
+struct Connection *Database_open(const char *filename, char mode)
+{
+ struct Connection *conn = malloc(sizeof(struct Connection));
+ if(!conn) die("Memory error");
+
+ conn->db = malloc(sizeof(struct Database));
+ if(!conn->db) die("Memory error");
+
+ if(mode == 'c') {
+ conn->file = fopen(filename, "w");
+ } else {
+ conn->file = fopen(filename, "r+");
+
+ if(conn->file) {
+ Database_load(conn);
+ }
+ }
+
+ if(!conn->file) die("Failed to open the file");
+
+ return conn;
+}
+
+void Database_close(struct Connection *conn)
+{
+ if(conn) {
+ if(conn->file) fclose(conn->file);
+ if(conn->db) free(conn->db);
+ free(conn);
+ }
+}
+
+void Database_write(struct Connection *conn)
+{
+ rewind(conn->file);
+
+ int rc = fwrite(conn->db, sizeof(struct Database), 1, conn->file);
+ if(rc != 1) die("Failed to write database.");
+
+ rc = fflush(conn->file);
+ if(rc == -1) die("Cannot flush database.");
+}
+
+void Database_create(struct Connection *conn)
+{
+ int i = 0;
+
+ for(i = 0; i < MAX_ROWS; i++) {
+ // make a prototype to initialize it
+ struct Address addr = {.id = i, .set = 0};
+ // then just assign it
+ conn->db->rows[i] = addr;
+ }
+}
+
+void Database_set(struct Connection *conn, int id, const char *name, const char *email)
+{
+ struct Address *addr = &conn->db->rows[id];
+ if(addr->set) die("Already set, delete it first");
+
+ addr->set = 1;
+ // WARNING: bug, read the "How To Break It" and fix this
+ char *res = strncpy(addr->name, name, MAX_DATA);
+ // demonstrate the strncpy bug
+ if(!res) die("Name copy failed");
+
+ res = strncpy(addr->email, email, MAX_DATA);
+ if(!res) die("Email copy failed");
+}
+
+void Database_get(struct Connection *conn, int id)
+{
+ struct Address *addr = &conn->db->rows[id];
+
+ if(addr->set) {
+ Address_print(addr);
+ } else {
+ die("ID is not set");
+ }
+}
+
+void Database_delete(struct Connection *conn, int id)
+{
+ struct Address addr = {.id = id, .set = 0};
+ conn->db->rows[id] = addr;
+}
+
+void Database_list(struct Connection *conn)
+{
+ int i = 0;
+ struct Database *db = conn->db;
+
+ for(i = 0; i < MAX_ROWS; i++) {
+ struct Address *cur = &db->rows[i];
+
+ if(cur->set) {
+ Address_print(cur);
+ }
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ if(argc < 3) die("USAGE: ex17 <dbfile> <action> [action params]");
+
+ char *filename = argv[1];
+ char action = argv[2][0];
+ struct Connection *conn = Database_open(filename, action);
+ int id = 0;
+
+ if(argc > 3) id = atoi(argv[3]);
+ if(id >= MAX_ROWS) die("There's not that many records.");
+
+ switch(action) {
+ case 'c':
+ Database_create(conn);
+ Database_write(conn);
+ break;
+
+ case 'g':
+ if(argc != 4) die("Need an id to get");
+
+ Database_get(conn, id);
+ break;
+
+ case 's':
+ if(argc != 6) die("Need id, name, email to set");
+
+ Database_set(conn, id, argv[4], argv[5]);
+ Database_write(conn);
+ break;
+
+ case 'd':
+ if(argc != 4) die("Need id to delete");
+
+ Database_delete(conn, id);
+ Database_write(conn);
+ break;
+
+ case 'l':
+ Database_list(conn);
+ break;
+ default:
+ die("Invalid action, only: c=create, g=get, s=set, d=del, l=list");
+ }
+
+ Database_close(conn);
+
+ return 0;
+}
+
#define
常量
我使用了“C预处理器”的另外一部分,来创建MAX_DATA
和MAX_ROWS
的设置常量。我之后会更多地讲解预处理器的功能,不过这是一个创建可靠的常量的简易方法。除此之外还有另一种方法,但是在特定场景下并不适用。
定长结构体
Address
结构体接着使用这些常量来创建数据,这些数据是定长的,它们并不高效,但是便于存储和读取。Database
结构体也是定长的,因为它有一个定长的Address
结构体数组。这样你就可以稍后把整个数据一步写到磁盘。
出现错误时终止的die
函数
在像这样的小型程序中,你可以编写一个单个函数在出现错误时杀掉程序。我把它叫做die
。而且在任何失败的函数调用,或错误输出之后,它会调用exit
带着错误退出程序。
用于错误报告的 errno
和perror
当函数返回了一个错误时,它通常设置一个叫做errno
的“外部”变量,来描述发生了什么错误。它们只是数字,所以你可以使用perror
来“打印出错误信息”。
文件函数
我使用了一些新的函数,比如fopen
,fread
,fclose
,和rewind
来处理文件。这些函数中每个都作用于FILE
结构体上,就像你的结构体似的,但是它由C标准库定义。
嵌套结构体指针
你应该学习这里的嵌套结构器和获取数组元素地址的用法,它读作“读取db
中的conn
中的rows
的第i
个元素,并返回地址(&
)”。
译者注:这里有个更简便的写法是db->conn->row + i。
结构体原型的复制
它在Database_delete
中体现得最清楚,你可以看到我是用了临时的局部Address
变量,初始化了它的id
和set
字段,接着通过把它赋值给rows
数组中的元素,简单地复制到数组中。这个小技巧确保了所有除了set
和id
的字段都初始化为0,而且很容易编写。顺便说一句,你不应该在这种数组复制操作中使用memcpy
。现代C语言中你可以只是将一个赋值给另一个,它会自动帮你处理复制。
处理复杂参数
我执行了一些更复杂的参数解析,但是这不是处理它们的最好方法。在这本书的后面我们将会了解一些用于解析的更好方法。
将字符串转换为整数
我使用了atoi
函数在命令行中接受作为id的字符串并把它转换为int id
变量。去查询这个函数以及相似的函数。
在堆上分配大块数据
这个程序的要点就是在我创建Database
的时候,我使用了malloc
来向OS请求一块大容量的内存。稍后我会讲得更细致一些。
NULL
就是0,所以可转成布尔值
在许多检查中,我简单地通过if(!ptr) die("fail!")
检测了一个指针是不是NULL
。这是有效的,因为NULL
会被计算成假。在一些少见的系统中,NULL
会储存在计算机中,并且表示为一些不是0的东西。但在C标准中,你可以把它当成0来编写代码。到目前为止,当我说“NULL
就是0”的时候,我都是对一些迂腐的人说的。
C使用了CPU真实的机制来完成工作,这涉及到RAM中的一块叫做栈的区域,以及另外一块叫做堆的区域。它们的差异取决于取得储存空间的位置。
堆更容易解释,因为它就是你电脑中的剩余内存,你可以通过malloc
访问它来获取更多内存,OS会使用内部函数为你注册一块内存区域,并且返回指向它的指针。当你使用完这片区域时,你应该使用free
把它交还给OS,使之能被其它程序复用。如果你不这样做就会导致程序“泄露”内存,但是Valgrind
会帮你监测这些内存泄露。
栈是一个特殊的内存区域,它储存了每个函数的创建的临时变量,它们对于该函数为局部变量。它的工作机制是,函数的每个c参数都会“压入”栈中,并且可在函数内部使用。它是一个真正的栈数据结构,所以是后进先出的。这对于main
中所有类似char section
和int id
的局部变量也是相同的。使用栈的优点是,当函数退出时C编译器会从栈中“弹出”所有变量来清理。这非常简单,也防止了栈上变量的内存泄露。
理清内存的最简单的方式是遵守这条原则:如果你的变量并不是从malloc
中获取的,也不是从一个从malloc
获取的函数中获取的,那么它在栈上。
下面是三个值得关注的关于栈和堆的主要问题:
malloc
获取了一块内存,并且把指针放在了栈上,那么当函数退出时,指针会被弹出而丢失。malloc
放在堆上。这就是我在程序中使用Database_open
来分配内存或退出的原因,相应的Database_close
用于释放内存。如果你创建了一个“创建”函数,它创建了一些东西,那么一个“销毁”函数可以安全地清理这些东西。这样会更容易理清内存。
最后,当一个程序退出时,OS会为你清理所有的资源,但是有时不会立即执行。一个惯用法(也是本次练习中用到的)是立即终止并且让OS清理错误。
strncpy
可能不会在字符串的末尾自动添加空字符(\0
),如果源字符串的长度等于或超过目标缓冲区的大小。这可能会导致字符串没有正确终止,从而引发安全漏洞或程序错误。为了修复这个问题,确保在strncpy
后手动将目标字符串的最后一个字符设置为\\0
,以确保字符串总是被正确终止。例如,修改代码为:
strncpy(addr->name, name, MAX_DATA - 1);
+addr->name[MAX_DATA - 1] = '\\0';
+
+strncpy(addr->email, email, MAX_DATA - 1);
+addr->email[MAX_DATA - 1] = '\\0';
+
这样可以保证即使源字符串长度达到或超过MAX_DATA
,addr->name
和addr->email
也会被正确终止。
void die(const char *message, struct Connection *conn)
+{
+ if(errno) {
+ perror(message);
+ } else {
+ printf("ERROR: %s\n", message);
+ }
+
+ if(conn) {
+ Database_close(conn); // 清理内存
+ }
+
+ exit(1);
+}
+
#include <stdio.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+
+// #define MAX_DATA 512
+// #define MAX_ROWS 100
+
+struct Address
+{
+ int id;
+ int set;
+ char *name;
+ char *email;
+};
+
+struct Database
+{
+ int MAX_DATA;
+ int MAX_ROWS;
+ struct Address *rows;
+};
+
+struct Connection
+{
+ FILE *file;
+ struct Database *db;
+};
+
+void Database_close(struct Connection *conn);
+
+void die(const char *message, struct Connection *conn)
+{
+ if (errno)
+ {
+ perror(message);
+ }
+ else
+ {
+ printf("ERROR: %s\n", message);
+ }
+
+ if (conn)
+ {
+ Database_close(conn); // 清理内存
+ }
+
+ exit(1);
+}
+
+void Address_print(struct Address *addr)
+{
+ printf("%d %s %s\n",
+ addr->id, addr->name, addr->email);
+}
+
+void Database_load(struct Connection *conn)
+{
+ // 确保文件指针在首个Address记录的开始位置
+ fseek(conn->file, sizeof(int) * 2, SEEK_SET);
+
+ for (int i = 0; i < conn->db->MAX_ROWS; i++)
+ {
+ struct Address *addr = &conn->db->rows[i];
+ if (addr->set)
+ Address_print(addr);
+
+ fread(&addr->id, sizeof(addr->id), 1, conn->file);
+ fread(&addr->set, sizeof(addr->set), 1, conn->file);
+ if (addr->set)
+ {
+ fread(addr->name, sizeof(char), conn->db->MAX_DATA, conn->file);
+ fread(addr->email, sizeof(char), conn->db->MAX_DATA, conn->file);
+ }
+ }
+ // printf("Database loaded.\n");
+}
+
+struct Connection *Database_open(const char *filename, char mode, int MAX_DATA, int MAX_ROWS)
+{
+ struct Connection *conn = malloc(sizeof(struct Connection));
+ if (!conn)
+ die("Memory error", conn);
+
+ if (mode == 'c')
+ {
+ conn->db = malloc(sizeof(struct Database));
+ if (!conn->db)
+ die("Memory error", conn);
+
+ conn->file = fopen(filename, "w");
+ if (!conn->file)
+ die("Failed to open the file", conn);
+
+ // 将MAX_DATA和MAX_ROWS写入文件
+ fwrite(&MAX_DATA, sizeof(int), 1, conn->file);
+ fwrite(&MAX_ROWS, sizeof(int), 1, conn->file);
+
+ // 初始化Database结构体
+ conn->db->MAX_DATA = MAX_DATA;
+ conn->db->MAX_ROWS = MAX_ROWS;
+ conn->db->rows = malloc(sizeof(struct Address) * MAX_ROWS);
+ for (int i = 0; i < MAX_ROWS; i++)
+ {
+ conn->db->rows[i].name = malloc(MAX_DATA);
+ conn->db->rows[i].email = malloc(MAX_DATA);
+ }
+ }
+ else
+ {
+ conn->file = fopen(filename, "r+");
+ if (!conn->file)
+ die("Failed to open the file", conn);
+
+ // 从文件读取MAX_DATA和MAX_ROWS
+ fread(&MAX_DATA, sizeof(int), 1, conn->file);
+ fread(&MAX_ROWS, sizeof(int), 1, conn->file);
+
+ conn->db = malloc(sizeof(struct Database));
+ if (!conn->db)
+ die("Memory error", conn);
+
+ conn->db->MAX_DATA = MAX_DATA;
+ conn->db->MAX_ROWS = MAX_ROWS;
+ conn->db->rows = malloc(sizeof(struct Address) * MAX_ROWS);
+ for (int i = 0; i < MAX_ROWS; i++)
+ {
+ conn->db->rows[i].name = malloc(MAX_DATA);
+ conn->db->rows[i].email = malloc(MAX_DATA);
+ }
+ // printf("conn->db->MAX_DATA: %d, conn->db->MAX_ROWS: %d\n", conn->db->MAX_DATA, conn->db->MAX_ROWS);
+
+ Database_load(conn);
+ }
+
+ return conn;
+}
+
+void Database_close(struct Connection *conn)
+{
+ if (conn)
+ {
+ if (conn->file)
+ fclose(conn->file);
+ if (conn->db)
+ free(conn->db);
+ free(conn);
+ }
+}
+
+void Database_write(struct Connection *conn)
+{
+ rewind(conn->file);
+
+ // 首先写入MAX_DATA和MAX_ROWS
+ if (fwrite(&conn->db->MAX_DATA, sizeof(int), 1, conn->file) != 1)
+ die("Failed to write MAX_DATA.", conn);
+ if (fwrite(&conn->db->MAX_ROWS, sizeof(int), 1, conn->file) != 1)
+ die("Failed to write MAX_ROWS.", conn);
+
+ // 然后逐个写入Address项
+ for (int i = 0; i < conn->db->MAX_ROWS; i++)
+ {
+ struct Address *addr = &conn->db->rows[i];
+
+ // 写入Address的id和set
+ if (fwrite(&addr->id, sizeof(addr->id), 1, conn->file) != 1)
+ die("Failed to write id.", conn);
+ if (fwrite(&addr->set, sizeof(addr->set), 1, conn->file) != 1)
+ die("Failed to write set.", conn);
+
+ // printf("addr->id: %d\n", addr->id);
+ // printf("addr->set: %d\n", addr->set);
+
+ // 只有当set为真时才写入name和email
+ if (addr->set)
+ {
+ // 确保字符串不超过MAX_DATA长度,最后一个字符保留为'\0'
+ addr->name[conn->db->MAX_DATA - 1] = '\0';
+ addr->email[conn->db->MAX_DATA - 1] = '\0';
+
+ // 使用memset填充剩余的空间
+ memset(addr->name + strlen(addr->name), '\0', conn->db->MAX_DATA - strlen(addr->name) - 1);
+ memset(addr->email + strlen(addr->email), '\0', conn->db->MAX_DATA - strlen(addr->email) - 1);
+
+ // 写入name和email到文件
+ size_t written = fwrite(addr->name, sizeof(char), conn->db->MAX_DATA, conn->file);
+ if (written < conn->db->MAX_DATA)
+ {
+ die("Failed to write name.", conn);
+ }
+ written = fwrite(addr->email, sizeof(char), conn->db->MAX_DATA, conn->file);
+ if (written < conn->db->MAX_DATA)
+ {
+ die("Failed to write email.", conn);
+ }
+ }
+ }
+
+ // 刷新文件以确保写入完成
+ if (fflush(conn->file) == -1)
+ die("Cannot flush database.", conn);
+}
+
+void Database_create(struct Connection *conn)
+{
+ for (int i = 0; i < conn->db->MAX_ROWS; i++)
+ {
+ // make a prototype to initialize it
+ struct Address addr = {.id = i, .set = 0};
+ // then just assign it
+ conn->db->rows[i] = addr;
+ }
+}
+
+void Database_set(struct Connection *conn, int id, const char *name, const char *email)
+{
+ struct Address *addr = &(conn->db->rows[id]);
+ // printf("id: %d\n", id);
+ // printf("addr->set: %d\n", addr->set);
+ if (addr->set)
+ die("Already set, delete it first", conn);
+
+ addr->set = 1;
+ addr->id = id;
+ char *res = strncpy(addr->name, name, conn->db->MAX_DATA - 1);
+ addr->name[conn->db->MAX_DATA - 1] = '\0';
+ // printf("addr->name: %s\n", addr->name);
+ if (!res)
+ die("Name copy failed", conn);
+
+ res = strncpy(addr->email, email, conn->db->MAX_DATA - 1);
+ addr->email[conn->db->MAX_DATA - 1] = '\0';
+ if (!res)
+ die("Email copy failed", conn);
+ // Address_print(addr);
+}
+
+void Database_get(struct Connection *conn, int id)
+{
+ struct Address *addr = &conn->db->rows[id];
+
+ if (addr->set)
+ {
+ Address_print(addr);
+ }
+ else
+ {
+ die("ID is not set", conn);
+ }
+}
+
+void Database_delete(struct Connection *conn, int id)
+{
+ // struct Address addr = {.id = id, .set = 0};
+ conn->db->rows[id].set = 0;
+ // printf("id: %d\n", id);
+ // printf("addr->set: %d\n", conn->db->rows[id].set);
+}
+
+void Database_list(struct Connection *conn)
+{
+ int i = 0;
+ // printf("conn->db->rows[3].set: %d\n", conn->db->rows[3].set);
+
+ for (i = 0; i < conn->db->MAX_ROWS; i++)
+ {
+ struct Address *cur = &conn->db->rows[i];
+ // printf("cur->id: %d\n", cur->id);
+ // printf("cur->set: %d\n", cur->set);
+
+ if (cur->set)
+ {
+ // printf("i: %d\n", i);
+ Address_print(cur);
+ }
+ }
+}
+
+void Database_find(struct Connection *conn, const char *search_term)
+{
+ for (int i = 0; i < conn->db->MAX_ROWS; i++)
+ {
+ struct Address *cur = &conn->db->rows[i];
+ if ((cur->set) && (strcmp(cur->name, search_term) == 0 || strcmp(cur->email, search_term) == 0))
+ {
+ Address_print(cur);
+ return;
+ }
+ }
+ printf("Search term not found.\n");
+}
+
+int main(int argc, char *argv[])
+{
+ // 至少需要文件名和操作类型
+ if (argc < 3)
+ die("USAGE: ex17 <dbfile> <action> [action params]", NULL);
+
+ char *filename = argv[1];
+ char action = argv[2][0];
+ struct Connection *conn = NULL;
+ int id = 0;
+
+ // 如果是创建操作,确保有足够的参数
+ if (action == 'c')
+ {
+ if (argc != 5)
+ die("USAGE: ex17 <dbfile> c <MAX_DATA> <MAX_ROWS>", NULL);
+
+ int _MAX_DATA = atoi(argv[3]);
+ int _MAX_ROWS = atoi(argv[4]);
+ conn = Database_open(filename, action, _MAX_DATA, _MAX_ROWS);
+ Database_create(conn);
+ Database_write(conn);
+ }
+ else
+ {
+ // 对于其他操作,不需要MAX_DATA和MAX_ROWS,但需要检查数据库文件是否存在
+ // printf("filename: %s\n", filename);
+ conn = Database_open(filename, action, 0, 0); // 0s表示这些值将被忽略
+ if (!conn)
+ die("Database not found. Use 'c' option to create one.", NULL);
+ }
+
+ if (argc > 3 && action != 'c')
+ {
+ id = atoi(argv[3]);
+ if (id >= conn->db->MAX_ROWS)
+ die("There's not that many records.", conn);
+ }
+
+ if (action != 'c')
+ {
+ switch (action)
+ {
+ case 'f':
+ if (argc != 4)
+ die("Need a search term to find", conn);
+ Database_find(conn, argv[3]);
+ break;
+
+ case 'g':
+ if (argc != 4)
+ die("Need an id to get", conn);
+
+ Database_get(conn, id);
+ break;
+
+ case 's':
+ if (argc != 6)
+ die("Need id, name, email to set", conn);
+
+ Database_set(conn, id, argv[4], argv[5]);
+ Database_write(conn);
+ break;
+
+ case 'd':
+ if (argc != 4)
+ die("Need id to delete", conn);
+
+ Database_delete(conn, id);
+ Database_write(conn);
+ break;
+
+ case 'l':
+ Database_list(conn);
+ break;
+ default:
+ die("Invalid action, only: c=create, g=get, s=set, d=del, l=list", conn);
+ }
+
+ Database_close(conn);
+
+ return 0;
+ }
+}
+
+
在结构体中,为了提高访问速度,编译器可能会按照特定的对齐规则(alignment rules)来安排成员的存储。这通常依赖于目标平台(如处理器架构)和编译器的具体实现。
int
(假设为4字节)紧跟着一个char
(1字节),那么为了使下一个int
成员对齐,编译器可能会在char
后面插入3字节的填充。注:成员的排列顺序可能导致更多或更少的填充字节被插入。例如,将较小的数据类型(如char
)放在较大的数据类型(如int
)之间可能会导致额外的填充,以保持较大类型的对齐。
#include <stdio.h>
+#include <stdlib.h>
+#define MAX_SIZE 10
+
+typedef struct
+{
+ int items[MAX_SIZE];
+ int top;
+} Stack;
+
+// 栈的初始化
+void initializeStack(Stack *s)
+{
+ s->top = -1;
+}
+
+// 检查栈是否为空
+int isEmpty(Stack *s)
+{
+ return s->top == -1;
+}
+
+// 检查栈是否已满
+int isFull(Stack *s)
+{
+ return s->top == MAX_SIZE - 1;
+}
+
+// 入栈
+void push(Stack *s, int item)
+{
+ if (isFull(s))
+ {
+ printf("Stack is full!\n");
+ }
+ else
+ {
+ s->items[++s->top] = item;
+ }
+}
+
+// 出栈
+int pop(Stack *s)
+{
+ if (isEmpty(s))
+ {
+ printf("Stack is empty!\n");
+ return -1; // 返回一个标识值,表示栈空
+ }
+ else
+ {
+ return s->items[s->top--];
+ }
+}
+
+// 查看栈顶元素
+int peek(Stack *s)
+{
+ if (isEmpty(s))
+ {
+ printf("Stack is empty!\n");
+ return -1; // 返回一个标识值,表示栈空
+ }
+ else
+ {
+ return s->items[s->top];
+ }
+}
+
+// 主函数,演示栈操作
+int main()
+{
+ Stack s;
+ initializeStack(&s);
+
+ // 入栈操作
+ push(&s, 10);
+ push(&s, 20);
+ push(&s, 30);
+
+ // 查看栈顶元素
+ printf("Top element is %d\n", peek(&s));
+
+ // 出栈操作,并打印出栈元素
+ printf("Popped %d from the stack\n", pop(&s));
+ printf("Popped %d from the stack\n", pop(&s));
+
+ // 再次查看栈顶元素
+ if (!isEmpty(&s))
+ {
+ printf("Top element is %d\n", peek(&s));
+ }
+
+ // 清空栈
+ while (!isEmpty(&s))
+ {
+ pop(&s);
+ }
+
+ if (isEmpty(&s))
+ {
+ printf("Stack is empty now.\n");
+ }
+
+ return 0;
+}
+
在C中,文件操作主要通过以下几个函数实现:
fopen(const char *filename, const char *mode)
:打开文件,filename
是文件名,mode
是打开模式,如"r"
(只读)、"w"
(只写)、"a"
(追加)等。fclose(FILE *stream)
:关闭一个已打开的文件。fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
和fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
:分别用于从文件读取数据和向文件写入数据,ptr
是数据存储的内存地址,size
是每个数据项的大小,nmemb
是数据项的数量。fprintf(FILE *stream, const char *format, ...)
和fscanf(FILE *stream, const char *format, ...)
:分别用于向文件写入格式化文本和从文件读取格式化文本。fseek(FILE *stream, long int offset, int whence)
:移动文件指针到指定位置,offset
是相对于whence
指定的位置偏移量,whence
可以是SEEK_SET
(文件开头)、SEEK_CUR
(当前位置)、SEEK_END
(文件末尾)。这些函数提供了基础的文件打开、读写、关闭等操作,是进行文件处理的基石。
在C++中,文件操作是通过标准库中的fstream库进行的,它提供了读取、写入文件的能力。fstream库中包含了三个主要的类:ifstream、ofstream和fstream,分别用于文件输入(读取)、文件输出(写入)和同时支持文件输入输出。
open()
, close()
, read()
, getline()
, eof()
等。#include <fstream>
+#include <iostream>
+#include <string>
+using namespace std;
+
+int main() {
+ ifstream file("example.txt");
+ if (file.is_open()) {
+ string line;
+ while (getline(file, line)) {
+ cout << line << '\\n';
+ }
+ file.close();
+ } else cout << "Unable to open file";
+ return 0;
+}
+
open()
, close()
, write()
, <<
操作符等。#include <fstream>
+#include <iostream>
+using namespace std;
+
+int main() {
+ ofstream file("example.txt");
+ if (file.is_open()) {
+ file << "Hello, World!\\n";
+ file.close();
+ } else cout << "Unable to open file";
+ return 0;
+}
+
#include <fstream>
+#include <iostream>
+using namespace std;
+
+int main() {
+ // 打开文件,同时用于读写
+ fstream file("example.txt", ios::in | ios::out | ios::app);
+ if (file.is_open()) {
+ // 写入文件
+ file << "Hello, file!\\n";
+ // 设置读取位置到文件开头
+ file.seekg(0, ios::beg);
+ // 读取文件
+ string line;
+ while (getline(file, line)) {
+ cout << line << '\\n';
+ }
+ file.close();
+ } else cout << "Unable to open file";
+ return 0;
+}
+
在使用fstream、ifstream或ofstream打开文件时,可以指定文件的打开模式。这是通过在构造函数或open()
方法中使用特定的标志完成的,如下所示:
ios::in
:打开文件进行读取。ios::out
:打开文件进行写入。ios::binary
:以二进制方式打开文件。ios::app
:写入数据时追加到文件末尾。ios::ate
:打开文件后立即移动到文件末尾。ios::trunc
:如果文件已存在,先删除文件再创建。ios::nocreate
:打开文件时,如果文件不存在,则打开失败。(在某些编译器中不可用)ios::noreplace
:打开文件时,如果文件已存在,则打开失败。(在某些编译器中不可用)可以组合使用这些模式,例如,ios::in | ios::out
会打开文件以进行读写操作。
通过这种方式,C++提供了强大且灵活的文件操作能力,支持多种文件处理场景。
函数在C中实际上只是指向程序中某一个代码存在位置的指针。就像你创建过的结构体指针、字符串和数组那样,你也可以创建指向函数的指针。函数指针的主要用途是向其他函数传递“回调”,或者模拟类和对象。在这个练习中我们会创建一些回调,并且下一节我们会制作一个简单的对象系统。
函数指针的格式类似这样:
int (*POINTER_NAME)(int a,int b)
+
记住如何编写它的一个方法是:
int callme(int a, int b)
int (*callme)(int a, int b)
int (*compare_cb)(int a, int b)
这个方法的关键是,当你完成这些之后,指针的变量名称为compare_cb
,而你可以将它用作函数。这类似于指向数组的指针可以表示所指向的数组。指向函数的指针也可以用作表示所指向的函数,只不过是不同的名字。
int (*tester)(int a,int b) = sorted_order;
+printf("TEST: %d is same as %d\n", tester(2, 3), sorted_order(2, 3));
+
即使是对于返回指针的函数指针,上述方法依然有效:
char *make_coolness(int awesome_levels)
char *(*make_coolness)(int awesome_levels)
char *(*coolness_cb)(int awesome_levels)
需要解决的下一个问题是使用函数指针向其它函数提供参数比较困难,比如当你打算向其它函数传递回调函数的时候。解决方法是使用typedef
,它是C的一个关键字,可以给其它更复杂的类型起个新的名字。你需要记住的事情是,将typedef
添加到相同的指针语法之前,然后你就可以将那个名字用作类型了。
#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+
+/** Our old friend die from ex17. */
+void die(const char *message)
+{
+ if(errno) {
+ perror(message);
+ } else {
+ printf("ERROR: %s\n", message);
+ }
+
+ exit(1);
+}
+
+// a typedef creates a fake type, in this
+// case for a function pointer
+typedef int (*compare_cb)(int a, int b);
+
+/**
+ * A classic bubble sort function that uses the
+ * compare_cb to do the sorting.
+ */
+int *bubble_sort(int *numbers, int count, compare_cb cmp)
+{
+ int temp = 0;
+ int i = 0;
+ int j = 0;
+ int *target = malloc(count * sizeof(int));
+
+ if(!target) die("Memory error.");
+
+ memcpy(target, numbers, count * sizeof(int));
+
+ for(i = 0; i < count; i++) {
+ for(j = 0; j < count - 1; j++) {
+ if(cmp(target[j], target[j+1]) > 0) {
+ temp = target[j+1];
+ target[j+1] = target[j];
+ target[j] = temp;
+ }
+ }
+ }
+
+ return target;
+}
+
+int sorted_order(int a, int b)
+{
+ return a - b;
+}
+
+int reverse_order(int a, int b)
+{
+ return b - a;
+}
+
+int strange_order(int a, int b)
+{
+ if(a == 0 || b == 0) {
+ return 0;
+ } else {
+ return a % b;
+ }
+}
+
+/**
+ * Used to test that we are sorting things correctly
+ * by doing the sort and printing it out.
+ */
+void test_sorting(int *numbers, int count, compare_cb cmp)
+{
+ int i = 0;
+ int *sorted = bubble_sort(numbers, count, cmp);
+
+ if(!sorted) die("Failed to sort as requested.");
+
+ for(i = 0; i < count; i++) {
+ printf("%d ", sorted[i]);
+ }
+ printf("\n");
+
+ free(sorted);
+}
+
+int main(int argc, char *argv[])
+{
+ if(argc < 2) die("USAGE: ex18 4 3 1 5 6");
+
+ int count = argc - 1;
+ int i = 0;
+ char **inputs = argv + 1;
+
+ int *numbers = malloc(count * sizeof(int));
+ if(!numbers) die("Memory error.");
+
+ for(i = 0; i < count; i++) {
+ numbers[i] = atoi(inputs[i]);
+ }
+
+ test_sorting(numbers, count, sorted_order);
+ test_sorting(numbers, count, reverse_order);
+ test_sorting(numbers, count, strange_order);
+
+ free(numbers);
+
+ return 0;
+}
+
typedef int (*compare_cb)(int a, int b);
+compare_cb cmp;
+unsigned char *data = (unsigned char *)cmp;
+
+for(i = 0; i < 25; i++) {
+ printf("%02x:", data[i]);
+}
+
+// output:
+55:48:89:e5:89:7d:fc:89:75:f8:8b:55:fc:8b:45:f8:29:d0:c9:c3:55:48:89:e5:89:
+
这段代码试图以十六进制的形式打印出一个函数指针(cmp
)所指向的函数的前25个字节。compare_cb
是一个函数指针的类型定义,指向一个接受两个int
类型参数并返回int
类型结果的函数。unsigned char *data = (unsigned char *)cmp;
这行代码将函数指针cmp
转换为一个unsigned char
类型的指针,这样就可以逐字节地访问函数指针所指向的函数的机器代码。
在循环中,程序逐个字节地访问并打印出这个函数的机器代码,直到打印了25个字节。每个字节以两位十六进制数的形式打印,后面跟着一个冒号。
输出的具体内容取决于cmp
所指向的函数的机器代码,这又依赖于具体的函数实现、编译器以及目标平台的架构。不同的编译器优化设置可能产生不同的机器代码。此外,不同的处理器架构(如x86、ARM等)有其独特的指令集,导致即使是相同的高级语言函数,其编译成的机器代码也会不同。
由于这段代码直接访问和解释函数的机器码,它涉及到底层的、平台特定的行为。在大多数现代操作系统和处理器架构中,试图访问函数的机器码是不推荐的,因为:
总之,这段代码的目的是展示如何以字节为单位访问和打印出一个函数的机器码,但实际应用场景有限,且在不同环境中的行为可能会有很大差异。
用十六进制编辑器打开ex18
,接着找到函数起始处的十六进制代码序列,看看是否能在原始程序中找到函数。
修改了上述函数的汇编机器码,结果就提示不合法的硬件指令
void swap(int *a, int *b) {
+ int temp = *a;
+ *a = *b;
+ *b = temp;
+}
+
+int partition(int *array, int low, int high, compare_cb cmp) {
+ int pivot = array[high];
+ int i = (low - 1);
+
+ for (int j = low; j <= high - 1; j++) {
+ if (cmp(array[j], pivot) < 0) {
+ i++;
+ swap(&array[i], &array[j]);
+ }
+ }
+ swap(&array[i + 1], &array[high]);
+ return (i + 1);
+}
+
+void quick_sort(int *array, int low, int high, compare_cb cmp) {
+ if (low < high) {
+ int pi = partition(array, low, high, cmp);
+
+ quick_sort(array, low, pi - 1, cmp);
+ quick_sort(array, pi + 1, high, cmp);
+ }
+}
+
+int *quick_sort_wrapper(int *numbers, int count, compare_cb cmp) {
+ int *target = malloc(count * sizeof(int));
+ if (!target) die("Memory error.");
+
+ memcpy(target, numbers, count * sizeof(int));
+ quick_sort(target, 0, count - 1, cmp);
+
+ return target;
+}
+
+ ← + + 2024.04.08-练习17:堆和栈的内存分配 + + 2024.04.10-练习32:双向链表 + + → +
#ifndef lcthw_List_h
+#define lcthw_List_h
+
+#include <stdlib.h>
+
+struct ListNode;
+
+typedef struct ListNode {
+ struct ListNode *next;
+ struct ListNode *prev;
+ void *value;
+} ListNode;
+
+typedef struct List {
+ int count;
+ ListNode *first;
+ ListNode *last;
+} List;
+
+List *List_create();
+void List_destroy(List *list);
+void List_clear(List *list);
+void List_clear_destroy(List *list);
+
+#define List_count(A) ((A)->count)
+#define List_first(A) ((A)->first != NULL ? (A)->first->value : NULL)
+#define List_last(A) ((A)->last != NULL ? (A)->last->value : NULL)
+
+void List_push(List *list, void *value);
+void *List_pop(List *list);
+
+void List_unshift(List *list, void *value);
+void *List_shift(List *list);
+
+void *List_remove(List *list, ListNode *node);
+
+#define LIST_FOREACH(L, S, M, V) ListNode *_node = NULL;\
+ ListNode *V = NULL;\
+ for(V = _node = L->S; _node != NULL; V = _node = _node->M)
+
+#endif
+
#include <lcthw/list.h>
+#include <lcthw/dbg.h>
+
+List *List_create()
+{
+ return calloc(1, sizeof(List));
+}
+
+void List_destroy(List *list)
+{
+ LIST_FOREACH(list, first, next, cur) {
+ if(cur->prev) {
+ free(cur->prev);
+ }
+ }
+
+ free(list->last);
+ free(list);
+}
+
+void List_clear(List *list)
+{
+ LIST_FOREACH(list, first, next, cur) {
+ free(cur->value);
+ }
+}
+
+void List_clear_destroy(List *list)
+{
+ List_clear(list);
+ List_destroy(list);
+}
+
+void List_push(List *list, void *value)
+{
+ ListNode *node = calloc(1, sizeof(ListNode));
+ check_mem(node);
+
+ node->value = value;
+
+ if(list->last == NULL) {
+ list->first = node;
+ list->last = node;
+ } else {
+ list->last->next = node;
+ node->prev = list->last;
+ list->last = node;
+ }
+
+ list->count++;
+
+error:
+ return;
+}
+
+void *List_pop(List *list)
+{
+ ListNode *node = list->last;
+ return node != NULL ? List_remove(list, node) : NULL;
+}
+
+void List_unshift(List *list, void *value)
+{
+ ListNode *node = calloc(1, sizeof(ListNode));
+ check_mem(node);
+
+ node->value = value;
+
+ if(list->first == NULL) {
+ list->first = node;
+ list->last = node;
+ } else {
+ node->next = list->first;
+ list->first->prev = node;
+ list->first = node;
+ }
+
+ list->count++;
+
+error:
+ return;
+}
+
+void *List_shift(List *list)
+{
+ ListNode *node = list->first;
+ return node != NULL ? List_remove(list, node) : NULL;
+}
+
+void *List_remove(List *list, ListNode *node)
+{
+ void *result = NULL;
+
+ check(list->first && list->last, "List is empty.");
+ check(node, "node can't be NULL");
+
+ if(node == list->first && node == list->last) {
+ list->first = NULL;
+ list->last = NULL;
+ } else if(node == list->first) {
+ list->first = node->next;
+ check(list->first != NULL, "Invalid list, somehow got a first that is NULL.");
+ list->first->prev = NULL;
+ } else if (node == list->last) {
+ list->last = node->prev;
+ check(list->last != NULL, "Invalid list, somehow got a next that is NULL.");
+ list->last->next = NULL;
+ } else {
+ ListNode *after = node->next;
+ ListNode *before = node->prev;
+ after->prev = before;
+ before->next = after;
+ }
+
+ list->count--;
+ result = node->value;
+ free(node);
+
+error:
+ return result;
+}
+
calloc
calloc
(Contiguous Allocation,连续分配)和malloc
(Memory Allocation,内存分配)都是C语言标准库中用于动态内存分配的函数,但它们之间有一些关键的区别:
malloc
分配的内存块内容是未初始化的。这意味着分配后的内存块中的数据是不确定的,可能包含垃圾值。使用malloc
分配内存后,通常需要手动初始化内存块,以避免潜在的安全问题或逻辑错误。calloc
分配的内存块会自动初始化为零。这意味着分配后的内存块中的每个字节都被设置为0
,这可以直接用于程序中而无需额外的初始化步骤。malloc
只需要一个参数,即所需分配的字节数。calloc
需要两个参数:需要分配的元素数量和每个元素的大小。calloc
会计算总共需要的字节数(两个参数的乘积),并分配相应的内存。malloc
或calloc
通常取决于是否需要自动将内存初始化为零。如果程序逻辑要求新分配的内存必须全部为零,那么calloc
是一个更好的选择,因为它可以保证这一点并简化代码。如果不需要初始化,或者分配后会立即被其他值覆盖,那么使用malloc
可能更有效率,因为它避免了额外的初始化开销。calloc
会初始化内存,所以相比malloc
可能有额外的性能开销。如果不需要内存内容初始化为零,直接使用malloc
可能更加高效。示例
使用malloc
分配内存:
int *array = (int*)malloc(10 * sizeof(int));
+// 需要手动初始化
+for(int i = 0; i < 10; i++) {
+ array[i] = 0;
+}
+
使用calloc
分配内存:
int *array = (int*)calloc(10, sizeof(int));
+// 已经自动初始化为零,无需手动初始化
+
总之,选择malloc
还是calloc
主要取决于是否需要自动初始化内存和对性能的考虑。在某些情况下,显式地使用malloc
和手动初始化可能更能清晰地表达程序员的意图,尤其是在初始化为非零值时。然而,calloc
提供了一个方便的方式来确保新分配的内存区域安全地初始化为零。
#define LIST_FOREACH(L, S, M, V) \
+ ListNode *_node = NULL;\
+ ListNode *V = NULL;\
+ for(V = _node = L->S; _node != NULL; V = _node = _node->M)
+
这个宏LIST_FOREACH
是一个用于遍历链表的宏,设计得非常巧妙。它利用了C语言宏的能力,提供了一个通用的、易于阅读的方式来遍历任何类型的链表。这个宏的参数有四个:
L
:指向链表的指针。S
:链表中的起始节点。M
:节点中指向下一个节点的成员的名称。V
:在遍历过程中用于引用当前节点的变量。这个宏定义了两个局部变量(_node
和V
),并初始化一个for
循环,用于遍历链表。以下是该宏各部分的详细解释:
ListNode *_node = NULL;
定义了一个名为_node
的局部变量,类型为ListNode*
,并将其初始化为NULL
。这个变量用于在循环中跟踪当前节点。ListNode *V = NULL;
定义了一个名为V
的局部变量(V
是宏的参数之一),类型为ListNode*
,并将其初始化为NULL
。这个变量用于在循环体中引用当前节点,让使用者能够访问或操作当前的链表节点。for(V = _node = L->S; _node != NULL; V = _node = _node->M)
初始化一个for
循环,开始于链表的起始节点(L->S
),并持续到链表的末尾(_node != NULL
)。循环的每一步都将_node
移动到下一个节点(_node->M
),同时也将V
设置为当前节点,以便于使用。这个宏的设计允许在不知道链表具体实现细节的情况下,通过一个简洁的方式遍历链表。举个例子,如果你有一个链表list
,它的起始节点为head
,每个节点都有一个名为next
的指针指向下一个节点,你可以使用LIST_FOREACH
宏来遍历这个链表,如下所示:
LIST_FOREACH(list, head, next, cur) {
+ // 在这里可以访问 cur 来操作当前节点
+}
+
这里,cur
就是循环中用来引用当前节点的变量,你可以通过它访问或修改当前节点的数据。
需要注意的是,这种宏的使用方式可能在某些编译器或代码分析工具中导致警告,因为它在宏内部定义了局部变量。此外,由于它使用了宏,调试可能会比较困难,错误信息可能不会直接指向问题的根源。不过,这种方式在提高代码可读性和减少重复代码方面是非常有效的。
void
类型的指针在C语言中扮演着非常重要的角色。void
指针可以指向任何类型的数据,这使得它成为一种非常灵活的工具,特别是在需要处理多种数据类型但又不想在编码时指定具体类型的情况下。以下是void
指针的一些主要用途:
void
指针的一个主要用途是实现泛型编程。由于void
指针可以指向任何类型的数据,它可以用来编写可以处理多种数据类型的函数和数据结构。例如,标准库函数qsort
就使用void
指针来排序各种不同类型的数组。
C语言的标准库中有几个用于内存操作的函数,如malloc
、calloc
、realloc
和free
,这些函数都使用void
指针。这使得它们可以用于分配和释放任何类型的数据的内存空间。
在使用回调函数时,void
指针常被用来传递用户自定义的数据给回调函数。这些数据可以是任何类型,使用者在回调函数中可以根据需要将void
指针转换回原始类型的指针来使用。
在设计需要抽象化处理的接口时,void
指针提供了一种方便的方式来隐藏具体的实现细节。这种方式常见于各种库的设计中,特别是那些需要处理多种数据类型或提供高度抽象化的库。
注意事项
虽然void
指针非常灵活和有用,但使用它们时也需要格外小心:
void
指针之前,必须将它转换为合适的类型的指针。void
指针本身不能直接被解引用,因为编译器不知道它指向的对象的类型和大小。void
指针时需要确保转换的目标类型正确,错误的类型转换可能导致未定义行为。void
指针可能会降低代码的可读性和安全性。尽可能使用具体的类型可以帮助编译器更好地进行类型检查。综上所述,void
类型的指针是C语言中一个非常强大的特性,它为编程提供了极大的灵活性,但也需要谨慎使用。
List_clear_destroy
void List_clear_destroy(List *list)
+{
+ check(list, "List is NULL.");
+ LIST_FOREACH(list, first, next, cur)
+ {
+ // printf("freeing: %s\n", (char *)cur->value);
+ free(cur->value);
+ if (cur->prev)
+ {
+ free(cur->prev);
+ }
+ }
+ free(list->last);
+ free(list);
+error:
+ return;
+}
+
双向链表和单向链表是两种基本的数据结构,它们在数据组织和操作上各有特点。理解它们的优势和劣势可以帮助在特定场景下做出更合适的选择。
单向链表是由一系列节点组成的链表,每个节点都包含数据部分和一个指向下一个节点的指针。它的特点和优势包括:
单向链表的主要劣势是:
双向链表中的每个节点除了包含数据部分和指向下一个节点的指针外,还包含一个指向前一个节点的指针。它的优势包括:
双向链表的主要劣势是:
总的来说,没有绝对的“最佳”选择,只有最适合特定需求和场景的选择。理解双向链表和单向链表的优缺点可以帮助你根据实际情况做出明智的决策。
双向链表确实提供了在节点的插入和删除操作上的高效性,特别是在你已经定位到目标节点的情况下。这种数据结构允许每个节点直接链接到其前后节点,从而使得添加或移除节点成为相对简单的操作。然而,双向链表并非没有限制,以下是它们的一些主要限制和缺点:
使用strdup初始化字符串而不是直接赋值(这样可以free,而不是const)
test1 = strdup("test1 data");
+test2 = strdup("test2 data");
+test3 = strdup("test3 data");
+
实现新功能源代码:
List *List_copy(List *list)
+{
+ check(list, "List is NULL.");
+
+ List *new_list = List_create();
+ check_mem(new_list);
+
+ LIST_FOREACH(list, first, next, cur)
+ {
+ if (cur->value)
+ {
+ // 复制字符串。注意:strdup在内存分配失败时返回NULL
+ char *copy_of_value = strdup((char *)cur->value);
+ if (copy_of_value == NULL)
+ {
+ // 处理内存分配失败的情况
+ List_destroy(new_list);
+ return NULL;
+ }
+ List_push(new_list, copy_of_value);
+ }
+ else
+ {
+ // 如果当前节点的value为NULL,也可以选择将NULL推入新列表
+ List_push(new_list, NULL);
+ }
+ }
+
+ return new_list;
+
+error:
+ return NULL;
+}
+
+void List_join(List *first, List *second)
+{
+ check(first && second, "One of the lists is NULL.");
+
+ if (second->first == NULL)
+ {
+ return; // 第二个链表为空,无需操作。
+ }
+
+ if (first->last != NULL)
+ {
+ first->last->next = second->first; // 连接两个链表。
+ second->first->prev = first->last;
+ }
+ else
+ {
+ first->first = second->first; // 如果第一个链表为空,直接设置首尾指针。
+ }
+
+ first->last = second->last;
+ first->count += second->count;
+
+error:
+ return;
+}
+
+List *List_split(List *list, ListNode *node)
+{
+ check(list && node, "List or node is NULL.");
+
+ List *new_list = List_create();
+ check_mem(new_list);
+
+ ListNode *cur = node;
+ while (cur != NULL)
+ {
+ ListNode *next = cur->next;
+ List_push(new_list, cur->value);
+
+ if (cur == list->last)
+ {
+ list->last = cur->prev;
+ }
+
+ if (cur->prev)
+ {
+ cur->prev->next = cur->next;
+ }
+
+ if (cur->next)
+ {
+ cur->next->prev = cur->prev;
+ }
+
+ free(cur);
+ cur = next;
+ list->count--;
+ }
+
+ if (list->last)
+ {
+ list->last->next = NULL;
+ }
+
+ if (list->count == 0)
+ {
+ list->first = NULL;
+ }
+
+ return new_list;
+
+error:
+ return NULL;
+}
+
测试代码:
char *test_copy()
+{
+ // 保证有数据
+ List_push(list, test1);
+ List_push(list, test2);
+ List_push(list, test3);
+ list_copy = List_copy(list);
+ mu_assert(list_copy != NULL, "Failed to copy list.");
+ mu_assert(List_count(list_copy) == List_count(list), "Copy list has wrong count.");
+
+ List_clear_destroy(list_copy);
+
+ return NULL;
+}
+
+char *test_join()
+{
+ list_second = List_create();
+ List_push(list_second, test3);
+ int count1 = List_count(list);
+
+ List_join(list, list_second);
+ int count2 = List_count(list_second);
+
+ mu_assert(List_count(list) == (count1 + count2), "Join list has wrong count.");
+ mu_assert(List_last(list) == test3, "Joined list has wrong last element.");
+
+ return NULL;
+}
+
+char *test_split()
+{
+ ListNode *node = list->first->next; // test2
+ int count1 = List_count(list);
+ list_split = List_split(list, node);
+ mu_assert(List_count(list) == 1, "Original list has wrong count after split.");
+ mu_assert(List_count(list_split) == count1 - 1, "New list has wrong count after split.");
+ mu_assert(List_first(list_split) == test2, "Split list has wrong first element.");
+
+ return NULL;
+}
+
+ ← + + 2024.04.10-练习18:函数指针 + + 2024.04.11-练习33:链表算法 + + → +
归并排序是一种高效、稳定的排序算法,使用分治法(Divide and Conquer)的一个非常典型的应用。它的基本思想是将两个或两个以上的有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并排序的过程可以分为两个主要部分:
归并排序的时间复杂度为
归并排序的稳定性来源于合并过程中,相等的元素会保持原有的先后顺序。这使得归并排序非常适合用于需要稳定排序算法的场景,比如数据库的排序等。
这个算法的空间复杂度是
归并排序虽然在时间复杂度上表现良好,但由于其空间复杂度较高,因此在对空间使用有严格要求的环境下需要慎用。
#ifndef lcthw_List_algos_h
+#define lcthw_List_algos_h
+
+#include <lcthw/list.h>
+
+typedef int (*List_compare)(const void *a, const void *b);
+
+int List_bubble_sort(List *list, List_compare cmp);
+
+List *List_merge_sort(List *list, List_compare cmp);
+
+static inline void ListNode_swap(ListNode *a, ListNode *b)
+{
+ void *temp = a->value;
+ a->value = b->value;
+ b->value = temp;
+}
+
+List *List_merge(List *left, List *right, List_compare cmp);
+
+#endif
+
#include <lcthw/list_algos.h>
+#include <lcthw/dbg.h>
+
+int List_bubble_sort(List *list, List_compare cmp)
+{
+ int sorted = 1;
+
+ if(List_count(list) <= 1) {
+ return 0; // already sorted
+ }
+
+ do {
+ sorted = 1;
+ LIST_FOREACH(list, first, next, cur) {
+ if(cur->next) {
+ if(cmp(cur->value, cur->next->value) > 0) {
+ ListNode_swap(cur, cur->next);
+ sorted = 0;
+ }
+ }
+ }
+ } while(!sorted);
+
+ return 0;
+}
+
+List *List_merge(List *left, List *right, List_compare cmp)
+{
+ List *result = List_create();
+ void *val = NULL;
+
+ while(List_count(left) > 0 || List_count(right) > 0) {
+ if(List_count(left) > 0 && List_count(right) > 0) {
+ if(cmp(List_first(left), List_first(right)) <= 0) {
+ val = List_shift(left);
+ } else {
+ val = List_shift(right);
+ }
+
+ List_push(result, val);
+ } else if(List_count(left) > 0) {
+ val = List_shift(left);
+ List_push(result, val);
+ } else if(List_count(right) > 0) {
+ val = List_shift(right);
+ List_push(result, val);
+ }
+ }
+
+ return result;
+}
+
+List *List_merge_sort(List *list, List_compare cmp)
+{
+ if(List_count(list) <= 1) {
+ return list;
+ }
+
+ List *left = List_create();
+ List *right = List_create();
+ int middle = List_count(list) / 2;
+
+ LIST_FOREACH(list, first, next, cur) {
+ if(middle > 0) {
+ List_push(left, cur->value);
+ } else {
+ List_push(right, cur->value);
+ }
+
+ middle--;
+ }
+
+ List *sort_left = List_merge_sort(left, cmp);
+ List *sort_right = List_merge_sort(right, cmp);
+
+ if(sort_left != left) List_destroy(left);
+ if(sort_right != right) List_destroy(right);
+
+ return List_merge(sort_left, sort_right, cmp);
+}
+
#include "minunit.h"
+#include <lcthw/list_algos.h>
+#include <assert.h>
+#include <string.h>
+
+char *values[] = {"XXXX", "1234", "abcd", "xjvef", "NDSS"};
+#define NUM_VALUES 5
+
+List *create_words()
+{
+ int i = 0;
+ List *words = List_create();
+
+ for(i = 0; i < NUM_VALUES; i++) {
+ List_push(words, values[i]);
+ }
+
+ return words;
+}
+
+int is_sorted(List *words)
+{
+ LIST_FOREACH(words, first, next, cur) {
+ if(cur->next && strcmp(cur->value, cur->next->value) > 0) {
+ debug("%s %s", (char *)cur->value, (char *)cur->next->value);
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+char *test_bubble_sort()
+{
+ List *words = create_words();
+
+ // should work on a list that needs sorting
+ int rc = List_bubble_sort(words, (List_compare)strcmp);
+ mu_assert(rc == 0, "Bubble sort failed.");
+ mu_assert(is_sorted(words), "Words are not sorted after bubble sort.");
+
+ // should work on an already sorted list
+ rc = List_bubble_sort(words, (List_compare)strcmp);
+ mu_assert(rc == 0, "Bubble sort of already sorted failed.");
+ mu_assert(is_sorted(words), "Words should be sort if already bubble sorted.");
+
+ List_destroy(words);
+
+ // should work on an empty list
+ words = List_create(words);
+ rc = List_bubble_sort(words, (List_compare)strcmp);
+ mu_assert(rc == 0, "Bubble sort failed on empty list.");
+ mu_assert(is_sorted(words), "Words should be sorted if empty.");
+
+ List_destroy(words);
+
+ return NULL;
+}
+
+char *test_merge_sort()
+{
+ List *words = create_words();
+
+ // should work on a list that needs sorting
+ List *res = List_merge_sort(words, (List_compare)strcmp);
+ mu_assert(is_sorted(res), "Words are not sorted after merge sort.");
+
+ List *res2 = List_merge_sort(res, (List_compare)strcmp);
+ mu_assert(is_sorted(res), "Should still be sorted after merge sort.");
+ List_destroy(res2);
+ List_destroy(res);
+
+ List_destroy(words);
+ return NULL;
+}
+
+char *all_tests()
+{
+ mu_suite_start();
+
+ mu_run_test(test_bubble_sort);
+ mu_run_test(test_merge_sort);
+
+ return NULL;
+}
+
+RUN_TESTS(all_tests);
+
inline
函数的使用当你在头文件中使用inline
关键字声明和定义一个函数时,你告诉编译器这个函数的调用可以被替换为其函数体,即内联展开。这可以减少函数调用的开销,特别是对于那些非常小且频繁调用的函数。
static inline
通过在头文件中将函数定义为static inline
,你可以避免链接问题。这样做的结果是,每个包含此头文件的.c
文件都会得到函数的一个私有副本。这种方法很简单,适用于大多数情况,特别是当函数非常短小时。
#ifndef EXAMPLE_H
+#define EXAMPLE_H
+
+// Inline function declaration
+static inline void myFunction(int x) {
+ // Function implementation
+}
+
+#endif // EXAMPLE_H
+
这种方式不需要在源文件中额外定义函数。
如果你的目的是确保有一个非内联的版本可用,你可能不需要在源文件中使用inline
或extern
关键字,因为static inline
的使用通常已经足够。我之前的建议在这方面可能不完全准确,因为在C中使用inline
函数的最佳实践可能因编译器和具体情况而异。如果确实需要在某个地方提供一个明确的外部定义,那么你应该在源文件中提供一个普通的函数定义(无inline
关键字),但这在使用了static inline
定义的情况下通常是不必要的。
归并排序做了大量的链表复制和创建操作,寻找减少它们的办法。
List *List_merge(List *left, List *right, List_compare cmp)
+{
+ List *result = List_create();
+
+ while (left->first && right->first)
+ {
+ if (cmp(left->first->value, right->first->value) <= 0)
+ {
+ List_push(result, List_shift(left));
+ }
+ else
+ {
+ List_push(result, List_shift(right));
+ }
+ }
+ while (left->first)
+ {
+ List_push(result, List_shift(left));
+ }
+ while (right->first)
+ {
+ List_push(result, List_shift(right));
+ }
+
+ return result;
+}
+
+List *List_merge_sort(List *list, List_compare cmp)
+{
+ if (List_count(list) <= 1)
+ {
+ return list;
+ }
+
+ List *secondHalf = List_split_mid(list);
+
+ List *sort_left = List_merge_sort(list, cmp);
+ List *sort_right = List_merge_sort(secondHalf, cmp);
+
+ if (sort_left != list)
+ List_destroy(list);
+ if (sort_right != secondHalf)
+ List_destroy(secondHalf);
+
+ return List_merge(sort_left, sort_right, cmp);
+}
+
+List *List_split_mid(List *list)
+{
+ if (!list || !list->first || !list->first->next)
+ {
+ return NULL;
+ }
+
+ int cnt = List_count(list);
+ int c1 = 1, c2 = 0;
+ ListNode *slow = list->first;
+ ListNode *fast = list->first->next;
+ while (fast && fast->next)
+ {
+ slow = slow->next;
+ fast = fast->next->next;
+ c1++;
+ }
+ c2 = cnt - c1;
+
+ List *listsecond = List_create();
+ listsecond->first = slow->next;
+ listsecond->first->prev = NULL;
+ listsecond->last = list->last;
+ listsecond->count = c2;
+ list->last = slow;
+ slow->next = NULL;
+ list->count = c1;
+
+ return listsecond;
+}
+
double start = clock();
+mu_run_test(test_bubble_sort);
+double end = clock();
+double cpu_time_used = ((double)(end - start)) / CLOCKS_PER_SEC;
+printf("test_bubble_sort took %f seconds to execute \n", cpu_time_used);
+start = clock();
+mu_run_test(test_merge_sort);
+end = clock();
+cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
+printf("test_merge_sort took %f seconds to execute \n", cpu_time_used);
+
List_insert_sorted
实现List_insert_sorted
(有序链表),它使用List_compare
,接收一个值,将其插入到正确的位置,使链表有序。
void List_insert_sorted(List *list, void *value, List_compare cmp){
+ ListNode *node = malloc(sizeof(ListNode));
+ node->value = value;
+ node->next = NULL;
+
+ if(list->first == NULL || cmp(value, list->first->value) < 0) {
+ // 插入到链表头部
+ node->next = list->first;
+ list->first = node;
+ if(list->last == NULL) {
+ list->last = node;
+ }
+ } else {
+ // 在链表中找到正确的插入位置
+ ListNode *current = list->first;
+ while(current->next != NULL && cmp(value, current->next->value) > 0) {
+ current = current->next;
+ }
+ // 插入到current节点之后
+ node->next = current->next;
+ current->next = node;
+ if(current->next == NULL) {
+ list->last = node;
+ }
+ }
+ list->count++;
+}
+
+ ← + + 2024.04.10-练习32:双向链表 + + 2024.04.11-练习42:栈和队列 + + → +
#ifndef STACK_H
+#define STACK_H
+#include "list.h"
+
+typedef List Stack;
+
+#define Stack_create() List_create()
+#define Stack_destroy(A) List_destroy(A)
+#define Stack_push(A, B) List_push(A, B)
+#define Stack_peek(A) A->last->value
+#define Stack_count(A) List_count(A)
+#define STACK_FOREACH(S, V) LIST_FOREACH(S, last, prev, V)
+#define Stack_pop(A) List_remove(A, A->last)
+
+#endif
+
#include "minunit.h"
+#include <lcthw/stack.h>
+#include <assert.h>
+
+static Stack *stack = NULL;
+char *tests[] = {"test1 data", "test2 data", "test3 data"};
+#define NUM_TESTS 3
+
+char *test_create()
+{
+ stack = Stack_create();
+ mu_assert(stack != NULL, "Failed to create stack.");
+
+ return NULL;
+}
+
+char *test_destroy()
+{
+ mu_assert(stack != NULL, "Failed to make stack #2");
+ Stack_destroy(stack);
+
+ return NULL;
+}
+
+char *test_push_pop()
+{
+ int i = 0;
+ for(i = 0; i < NUM_TESTS; i++) {
+ Stack_push(stack, tests[i]);
+ mu_assert(Stack_peek(stack) == tests[i], "Wrong next value.");
+ }
+
+ mu_assert(Stack_count(stack) == NUM_TESTS, "Wrong count on push.");
+
+ STACK_FOREACH(stack, cur) {
+ debug("VAL: %s", (char *)cur->value);
+ }
+
+ for(i = NUM_TESTS - 1; i >= 0; i--) {
+ char *val = Stack_pop(stack);
+ mu_assert(val == tests[i], "Wrong value on pop.");
+ }
+
+ mu_assert(Stack_count(stack) == 0, "Wrong count after pop.");
+
+ return NULL;
+}
+
+char *all_tests() {
+ mu_suite_start();
+
+ mu_run_test(test_create);
+ mu_run_test(test_push_pop);
+ mu_run_test(test_destroy);
+
+ return NULL;
+}
+
+RUN_TESTS(all_tests);
+
#ifndef QUEUE_H
+#define QUEUE_H
+#include "list.h"
+
+typedef List Queue;
+
+#define Queue_create() List_create()
+#define Queue_destroy(A) List_destroy(A)
+#define Queue_send(A, B) List_push(A, B)
+#define Queue_peek(A) A->first->value
+#define Queue_count(A) List_count(A)
+#define QUEUE_FOREACH(S, V) LIST_FOREACH(S, first, next, V)
+#define Queue_recv(A) List_remove(A, A->first)
+
+#endif
+
+ ← + + 2024.04.11-练习33:链表算法 + + 2024.04.12-练习44:环形缓冲区 + + → +
#ifndef _lcthw_RingBuffer_h
+#define _lcthw_RingBuffer_h
+
+#include <lcthw/bstrlib.h>
+
+typedef struct {
+ char *buffer;
+ int length;
+ int start;
+ int end;
+} RingBuffer;
+
+RingBuffer *RingBuffer_create(int length);
+
+void RingBuffer_destroy(RingBuffer *buffer);
+
+int RingBuffer_read(RingBuffer *buffer, char *target, int amount);
+
+int RingBuffer_write(RingBuffer *buffer, char *data, int length);
+
+int RingBuffer_empty(RingBuffer *buffer);
+
+int RingBuffer_full(RingBuffer *buffer);
+
+int RingBuffer_available_data(RingBuffer *buffer);
+
+int RingBuffer_available_space(RingBuffer *buffer);
+
+bstring RingBuffer_gets(RingBuffer *buffer, int amount);
+
+#define RingBuffer_available_data(B) (((B)->end + 1) % (B)->length - (B)->start - 1)
+
+#define RingBuffer_available_space(B) ((B)->length - (B)->end - 1)
+
+#define RingBuffer_full(B) (RingBuffer_available_data((B)) - (B)->length == 0)
+
+#define RingBuffer_empty(B) (RingBuffer_available_data((B)) == 0)
+
+#define RingBuffer_puts(B, D) RingBuffer_write((B), bdata((D)), blength((D)))
+
+#define RingBuffer_get_all(B) RingBuffer_gets((B), RingBuffer_available_data((B)))
+
+#define RingBuffer_starts_at(B) ((B)->buffer + (B)->start)
+
+#define RingBuffer_ends_at(B) ((B)->buffer + (B)->end)
+
+#define RingBuffer_commit_read(B, A) ((B)->start = ((B)->start + (A)) % (B)->length)
+
+#define RingBuffer_commit_write(B, A) ((B)->end = ((B)->end + (A)) % (B)->length)
+
+#endif
+
这段代码定义了一个环形缓冲区(Ring Buffer),它是一种数据结构,用于在有限的缓冲区空间中高效地处理数据流。这种结构非常适合于需要循环利用空间的场景,比如网络通信或文件流处理。以下是代码中各部分的详细解释:
#ifndef _lcthw_RingBuffer_h
+#define _lcthw_RingBuffer_h
+#include <lcthw/bstrlib.h>
+
这部分代码使用预处理命令来防止头文件被重复包含。#include <lcthw/bstrlib.h>
是引入一个字符串库,这个库可能提供了一些额外的字符串处理功能。
typedef struct {
+ char *buffer;
+ int length;
+ int start;
+ int end;
+} RingBuffer;
+
这是环形缓冲区的基础结构,包含了以下字段:
char *buffer
:指向实际存储数据的内存区域。int length
:缓冲区的总长度。int start
:指示数据开始的位置。int end
:指示数据结束的位置。环形缓冲区的操作包括创建、销毁、读取、写入以及检查缓冲区的状态:
RingBuffer_create(int length)
:创建一个指定长度的环形缓冲区。RingBuffer_destroy(RingBuffer *buffer)
:销毁缓冲区,释放资源。RingBuffer_read(RingBuffer *buffer, char *target, int amount)
:从缓冲区读取数据到target
数组。RingBuffer_write(RingBuffer *buffer, char *data, int length)
:将数据写入缓冲区。bstring RingBuffer_gets(RingBuffer *buffer, int amount)
:读取指定数量的数据为一个bstring
。这些宏定义提供了一种快速访问缓冲区状态的方式,比如检查缓冲区是否空或满:
RingBuffer_available_data(B)
:计算缓冲区中已存储的数据量。RingBuffer_available_space(B)
:计算缓冲区中剩余的空间量。RingBuffer_full(B)
:检查缓冲区是否已满。RingBuffer_empty(B)
:检查缓冲区是否为空。RingBuffer_puts(B, D)
:便捷函数,用于将bstring
类型的数据写入缓冲区。RingBuffer_get_all(B)
:获取缓冲区中的所有数据。RingBuffer_commit_read(B, A)
:在读取操作后更新start
位置。RingBuffer_commit_write(B, A)
:在写入操作后更新end
位置。#undef NDEBUG
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <lcthw/dbg.h>
+#include <lcthw/ringbuffer.h>
+
+RingBuffer *RingBuffer_create(int length)
+{
+ RingBuffer *buffer = calloc(1, sizeof(RingBuffer));
+ buffer->length = length + 1;
+ buffer->start = 0;
+ buffer->end = 0;
+ buffer->buffer = calloc(buffer->length, 1);
+
+ return buffer;
+}
+
+void RingBuffer_destroy(RingBuffer *buffer)
+{
+ if(buffer) {
+ free(buffer->buffer);
+ free(buffer);
+ }
+}
+
+int RingBuffer_write(RingBuffer *buffer, char *data, int length)
+{
+ if(RingBuffer_available_data(buffer) == 0) {
+ buffer->start = buffer->end = 0;
+ }
+
+ check(length <= RingBuffer_available_space(buffer),
+ "Not enough space: %d request, %d available",
+ RingBuffer_available_data(buffer), length);
+
+ void *result = memcpy(RingBuffer_ends_at(buffer), data, length);
+ check(result != NULL, "Failed to write data into buffer.");
+
+ RingBuffer_commit_write(buffer, length);
+
+ return length;
+error:
+ return -1;
+}
+
+int RingBuffer_read(RingBuffer *buffer, char *target, int amount)
+{
+ check_debug(amount <= RingBuffer_available_data(buffer),
+ "Not enough in the buffer: has %d, needs %d",
+ RingBuffer_available_data(buffer), amount);
+
+ void *result = memcpy(target, RingBuffer_starts_at(buffer), amount);
+ check(result != NULL, "Failed to write buffer into data.");
+
+ RingBuffer_commit_read(buffer, amount);
+
+ if(buffer->end == buffer->start) {
+ buffer->start = buffer->end = 0;
+ }
+
+ return amount;
+error:
+ return -1;
+}
+
+bstring RingBuffer_gets(RingBuffer *buffer, int amount)
+{
+ check(amount > 0, "Need more than 0 for gets, you gave: %d ", amount);
+ check_debug(amount <= RingBuffer_available_data(buffer),
+ "Not enough in the buffer.");
+
+ bstring result = blk2bstr(RingBuffer_starts_at(buffer), amount);
+ check(result != NULL, "Failed to create gets result.");
+ check(blength(result) == amount, "Wrong result length.");
+
+ RingBuffer_commit_read(buffer, amount);
+ assert(RingBuffer_available_data(buffer) >= 0 && "Error in read commit.");
+
+ return result;
+error:
+ return NULL;
+}
+
这个 RingBuffer_write
函数是一个环形缓冲区的写操作函数,其作用是将数据从一个指定的源(data
参数)写入到环形缓冲区中。这段代码涉及错误检查、内存复制以及更新缓冲区指针的操作。下面我将逐步解析这个函数的每一部分。
RingBuffer_write
函数签名int RingBuffer_write(RingBuffer *buffer, char *data, int length)
+
这个函数接收三个参数:
buffer
:指向 RingBuffer
类型的指针,代表目标环形缓冲区。data
:指向需要写入缓冲区的数据的指针。length
:指定要写入的数据长度。函数返回写入的字节数,如果出现错误,则返回 -1
。
if(RingBuffer_available_data(buffer) == 0) {
+ buffer->start = buffer->end = 0;
+}
+
这里检查缓冲区是否为空(没有数据)。如果是空的,将 start
和 end
指针都重置为 0
。这种重置是为了优化写入性能,确保从缓冲区的开始位置写入数据。
check(length <= RingBuffer_available_space(buffer),
+ "Not enough space: %d request, %d available",
+ RingBuffer_available_data(buffer), length);
+
使用 check
宏(这通常是一个错误处理宏)来确保要写入的数据长度不会超过缓冲区的可用空间。如果超出可用空间,则打印错误信息并跳转到错误处理代码(error:
标签)。
void *result = memcpy(RingBuffer_ends_at(buffer), data, length);
+check(result != NULL, "Failed to write data into buffer.");
+
通过 memcpy
函数将数据从 data
指针复制到缓冲区中由 RingBuffer_ends_at(buffer)
返回的位置。memcpy
应该总是返回其目的地址,除非发生异常。这里的检查是为了确保数据确实被写入了。
RingBuffer_commit_write(buffer, length);
+
更新缓冲区的 end
指针,以反映新写入的数据量。这通过 RingBuffer_commit_write
宏实现,它会适当地调整 end
指针,包括处理可能的环形回绕。
return length;
+
如果一切正常,函数返回写入的字节数。
error:
+ return -1;
+
如果在检查或写操作中遇到任何问题,函数跳转到这里并返回 -1
,表示发生了错误。
RingBuffer_write
总结RingBuffer_write
是一个典型的环形缓冲区数据写入函数,它涵盖了错误处理、空间检查、数据复制和指针更新等关键操作。这种实现保证了数据的安全写入,并且能有效地利用缓冲区的空间。通过适当的指针和边界管理,环形缓冲区可以在有限的内存空间内循环使用,非常适用于如 I/O 数据流处理等场景。
RingBuffer_read
和 RingBuffer_gets
都是环形缓冲区中用于从缓冲区读取数据的函数,但它们在处理数据的方式和返回类型上有所不同。这两个函数各有其特定的用途和行为,下面详细解释这两者之间的区别:
RingBuffer_read
buffer
:指向 RingBuffer
结构的指针。target
:目标缓冲区,用于接收从环形缓冲区复制的数据。amount
:要从环形缓冲区读取的数据字节数。memcpy
从 RingBuffer_starts_at(buffer)
的位置开始,将 amount
字节的数据复制到 target
中。RingBuffer_commit_read
更新 start
指针。start
和 end
指针相等,表示缓冲区变为空,将它们重置为0。1
。RingBuffer_gets
bstring
对象。buffer
:指向 RingBuffer
结构的指针。amount
:要从环形缓冲区读取的数据字节数。blk2bstr
从环形缓冲区的 start
位置创建一个新的 bstring
对象,其中包含 amount
字节的数据。RingBuffer_read
,读取操作后通过 RingBuffer_commit_read
更新 start
指针。bstring
的长度是否正确,并确保没有数据处理错误。bstring
对象,失败时返回 NULL
。RingBuffer_read
返回一个整数(读取的字节数),并将数据直接写入提供的目标缓冲区。RingBuffer_gets
返回一个 bstring
对象,这是一个封装了数据和长度的结构体,用于更方便的字符串处理。RingBuffer_read
直接操作原始字节数据。RingBuffer_gets
则可能更适用于处理文本数据,因为它提供了一种易于管理和操作的字符串形式。RingBuffer_gets
在创建 bstring
时进行了额外的检查,如长度验证和空值检查,这可以提供更高的数据完整性保证。RingBuffer_read
更多地用于需要直接处理字节数据的场景,如文件读写、网络数据传输等。RingBuffer_gets
适用于需要字符串操作的场景,比如解析文本命令或读取配置文件等。总之,选择 RingBuffer_read
还是 RingBuffer_gets
取决于你的应用场景和对数据类型的需求。如果你需要直接处理原始数据,RingBuffer_read
是合适的;如果你需要处理和存储字符串数据,并可能在之后进行进一步的字符串操作,RingBuffer_gets
会是更好的选择。
#include "minunit.h" // 引入MinUnit头文件
+#include "RingBuffer.h" // 引入环形缓冲区的实现
+#include <assert.h>
+
+#define NUM_TESTS 5
+static RingBuffer *rb = NULL;
+
+char *test_create()
+{
+ rb = RingBuffer_create(1024);
+ mu_assert(rb != NULL, "Failed to create RingBuffer");
+ mu_assert(rb->length == 1025, "RingBuffer length incorrect");
+ mu_assert(RingBuffer_empty(rb), "RingBuffer should be empty after creation");
+
+ return NULL;
+}
+
+char *test_write_read()
+{
+ char *data = "test";
+ int result = RingBuffer_write(rb, data, 4);
+ mu_assert(result == 4, "RingBuffer_write should write 4 bytes");
+
+ char output[5] = {0}; // 确保有足够的空间和零初始化
+ result = RingBuffer_read(rb, output, 4);
+ mu_assert(result == 4, "RingBuffer_read should read 4 bytes");
+ mu_assert(strcmp(output, "test") == 0, "RingBuffer_read did not read the correct data");
+
+ return NULL;
+}
+
+char *test_puts_get_all()
+{
+ bstring data = bfromcstr("test");
+ RingBuffer_puts(rb, data);
+ bstring result = RingBuffer_get_all(rb);
+ mu_assert(biseq(data, result), "RingBuffer_get_all did not return the correct data");
+ bdestroy(data);
+ bdestroy(result);
+
+ return NULL;
+}
+
+char *test_puts_gets()
+{
+ bstring data = bfromcstr("test");
+ RingBuffer_puts(rb, data);
+ bstring result = RingBuffer_gets(rb, 4);
+ mu_assert(biseq(data, result), "RingBuffer_gets did not return the correct data");
+ bdestroy(data);
+ bdestroy(result);
+
+ return NULL;
+}
+
+char *test_destroy()
+{
+ mu_assert(rb != NULL, "Failed to create RingBuffer#2");
+ RingBuffer_destroy(rb);
+
+ return NULL;
+}
+
+static char *all_tests()
+{
+ mu_suite_start(); // 初始化测试套件
+ mu_run_test(test_create);
+ mu_run_test(test_write_read);
+ mu_run_test(test_puts_get_all);
+ mu_run_test(test_puts_gets);
+ mu_run_test(test_destroy);
+ return NULL;
+}
+
+// 使用RUN_TESTS宏运行所有测试
+RUN_TESTS(all_tests);
+
//#define RingBuffer_available_data(B) (((B)->end + 1) % (B)->length - (B)->start - 1)
+#define RingBuffer_available_data(B) (((B)->end >= (B)->start) ? ((B)->end - (B)->start) : ((B)->length - (B)->start + (B)->end))
+
+//#define RingBuffer_available_space(B) ((B)->length - (B)->end - 1)
+#define RingBuffer_available_space(B) ((B)->length - RingBuffer_available_data(B) - 1)
+
+//#define RingBuffer_full(B) (RingBuffer_available_data((B)) - (B)->length == 0)
+#define RingBuffer_full(B) (RingBuffer_available_space(B) == 0)
+
原来的宏并没有判断end与start的前后位置。
+ ← + + 2024.04.11-练习42:栈和队列 +
echo
程序将该参数打印出来。 shell 基于空格分割命令并进行解析,然后执行第一个单词代表的程序,并将后续的单词作为程序可以访问的参数。如果您希望传递的参数中包含空格(例如一个名为 My Photos 的文件夹),您要么用使用单引号,双引号将其包裹起来,要么使用转义符号 \
进行处理(My\ Photos
)。
切换到上次访问的目录:使用 cd - 可以切换到上次访问的目录。
cd -
+
在 shell 中,程序有两个主要的“流”:它们的输入流和输出流。 当程序尝试读取信息时,它们会从输入流中进行读取,当程序打印信息时,它们会将信息输出到输出流中。 通常,一个程序的输入输出流都是您的终端。也就是,您的键盘作为输入,显示器作为输出。 但是,我们也可以重定向这些流!
最简单的重定向是 < file
和 > file
。这两个命令可以将程序的输入输出流分别重定向到文件:
missing:~$ echo hello > hello.txt
+missing:~$ cat hello.txt
+hello
+missing:~$ cat < hello.txt
+hello
+missing:~$ cat < hello.txt > hello2.txt
+missing:~$ cat hello2.txt
+hello
+
您还可以使用 >>
来向一个文件追加内容。
使用管道( pipes ),我们能够更好的利用文件重定向。 |
操作符允许我们将一个程序的输出和另外一个程序的输入连接起来:
missing:~$ ls -l / | tail -n1
+drwxr-xr-x 1 root root 4096 Jun 20 2019 var
+missing:~$ curl --head --silent google.com | grep --ignore-case content-length | cut --delimiter=' ' -f2
+219
+
ctrl+L
例如,您笔记本电脑的屏幕亮度写在 brightness
文件中,它位于 /sys/class/backlight
通过将数值写入该文件,我们可以改变屏幕的亮度。现在,蹦到您脑袋里的第一个想法可能是:
$ sudo find -L /sys/class/backlight -maxdepth 2 -name '*brightness*'
+/sys/class/backlight/thinkpad_screen/brightness
+$ cd /sys/class/backlight/thinkpad_screen
+$ sudo echo 3 > brightness
+An error occurred while redirecting file 'brightness'
+open: Permission denied
+
出乎意料的是,我们还是得到了一个错误信息。毕竟,我们已经使用了 sudo
命令!关于 shell,有件事我们必须要知道。|
、>
、和 <
是通过 shell 执行的,而不是被各个程序单独执行。 echo
等程序并不知道 |
的存在,它们只知道从自己的输入输出流中进行读写。 对于上面这种情况,shell (权限为您的当前用户) 在设置 sudo echo
前尝试打开 brightness 文件并写入,但是系统拒绝了 shell 的操作因为此时 shell 不是根用户。
明白这一点后,我们可以这样操作:
$ echo 3 | sudo tee brightness
因为打开 /sys
文件的是 tee
这个程序,并且该程序以 root
权限在运行,因此操作可以进行。 这样您就可以在 /sys
中愉快地玩耍了,例如修改系统中各种 LED 的状态(路径可能会有所不同):
$ echo 1 | sudo tee /sys/class/leds/input6::scrolllock/brightness
+
单引号是全引用,被单引号括起的内容不管是常量还是变量都不会发生替换。
也就是说单引号定义字符串所见即所得,将单引号内的内容输出,看到的是什么就会输出什么。
双引号引用的内容,如果内容中有命令、变量等,会先把变量、命令解析出结果,然后在输出最终内容。双引号是部分引用,被双引号括起的内容常量还是常量,变量则会发生替换,替换成变量内容。
+ 2024.03.19-2. Script + + → +
在 bash 中为变量赋值的语法是 foo=bar
,访问变量中存储的数值,其语法为 $foo
。 需要注意的是,foo = bar
(使用空格隔开)是不能正确工作的,因为解释器会调用程序foo
并将 =
和 bar
作为参数。 总的来说,在 shell 脚本中使用空格会起到分割参数的作用,有时候可能会造成混淆,请务必多加检查。
Bash 中的字符串通过'
和 "
分隔符来定义,但是它们的含义并不相同。以'
定义的字符串为原义字符串,其中的变量不会被转义,而 "
定义的字符串会将变量值进行替换。
// mcd.sh
+// 创建一个文件夹并使用cd进入该文件夹。
+mcd () {
+ mkdir -p "$1"
+ cd "$1"
+}
+
这里 $1
是脚本的第一个参数。与其他脚本语言不同的是,bash 使用了很多特殊的变量来表示参数、错误代码和相关变量。下面列举了其中一些变量,更完整的列表可以参考 这里 (opens new window)。
$0
- 脚本名$1
到 $9
- 脚本的参数。 $1
是第一个参数,依此类推。$@
- 所有参数$#
- 参数个数$?
- 前一个命令的返回值$$
- 当前脚本的进程识别码!!
- 完整的上一条命令,包括参数。常见应用:当你因为权限不足执行命令失败时,可以使用 sudo !!
再尝试一次。$_
- 上一条命令的最后一个参数。如果你正在使用的是交互式 shell,你可以通过按下 Esc
之后键入 . 来获取这个值。使用 source mcd.sh
会将 mcd 函数加载进来,后面可以直接调用
命令通常使用 STDOUT
来返回输出值,使用STDERR
来返回错误及错误码,便于脚本以更加友好的方式报告错误。 返回码或退出状态是脚本/命令之间交流执行状态的方式。返回值 0 表示正常执行,其他所有非 0 的返回值都表示有错误发生。
退出码可以搭配 &&
(与操作符)和 ||
(或操作符)使用,用来进行条件判断,决定是否执行其他程序。它们都属于短路运算符 (opens new window)(short-circuiting) 同一行的多个命令可以用 ;
分隔。程序 true
的返回码永远是0
,false
的返回码永远是1
。
false || echo "Oops, fail"
+# Oops, fail
+
+true || echo "Will not be printed"
+#
+
+true && echo "Things went well"
+# Things went well
+
+false && echo "Will not be printed"
+#
+
+false ; echo "This will always run"
+# This will always run
+
另一个常见的模式是以变量的形式获取一个命令的输出,这可以通过 命令替换(command substitution)实现。
当您通过 $( CMD )
这样的方式来执行CMD
这个命令时,它的输出结果会替换掉 $( CMD )
。例如,如果执行 for file in $(ls)
,shell 首先将调用ls
,然后遍历得到的这些返回值。还有一个冷门的类似特性是 进程替换(process substitution), <( CMD )
会执行 CMD
并将结果输出到一个临时文件中,并将 <( CMD )
替换成临时文件名。这在我们希望返回值通过文件而不是 STDIN 传递时很有用。例如, diff <(ls foo) <(ls bar)
会显示文件夹 foo
和 bar
中文件的区别。
说了很多,现在该看例子了,下面这个例子展示了一部分上面提到的特性。这段脚本会遍历我们提供的参数,使用grep
搜索字符串 foobar
,如果没有找到,则将其作为注释追加到文件中。
#!/bin/bash
+
+echo "Starting program at $(date)" # date会被替换成日期和时间
+
+echo "Running program $0 with $# arguments with pid $$"
+
+for file in "$@"; do
+ grep foobar "$file" > /dev/null 2> /dev/null
+ # 如果模式没有找到,则grep退出状态为 1
+ # 我们将标准输出流和标准错误流重定向到Null,因为我们并不关心这些信息
+ if [[ $? -ne 0 ]]; then
+ echo "File $file does not have any foobar, adding one"
+ echo "# foobar" >> "$file"
+ fi
+done
+
在条件语句中,我们比较 $?
是否等于 0。 Bash 实现了许多类似的比较操作,您可以查看 [test 手册](https://man7.org/linux/man-pages/man1/test.1.html)
。 在 bash 中进行比较时,尽量使用双方括号 [[ ]]
而不是单方括号 [ ]
,这样会降低犯错的几率,尽管这样并不能兼容 sh
。 更详细的说明参见这里 (opens new window)。
当执行脚本时,我们经常需要提供形式类似的参数。bash 使我们可以轻松的实现这一操作,它可以基于文件扩展名展开表达式。这一技术被称为 shell 的 通配(globbing)
?
和 *
来匹配一个或任意个字符。例如,对于文件foo
, foo1
, foo2
, foo10
和 bar
, rm foo?
这条命令会删除foo1
和 foo2
,而rm foo*
则会删除除了bar
之外的所有文件。{}
- 当你有一系列的指令,其中包含一段公共子串时,可以用花括号来自动展开这些命令。这在批量移动或转换文件时非常方便。convert image.{png,jpg}
+# 会展开为
+convert image.png image.jpg
+
+cp /path/to/project/{foo,bar,baz}.sh /newpath
+# 会展开为
+cp /path/to/project/foo.sh /path/to/project/bar.sh /path/to/project/baz.sh /newpath
+
+# 也可以结合通配使用
+mv *{.py,.sh} folder
+# 会移动所有 *.py 和 *.sh 文件
+
+mkdir foo bar
+
+# 下面命令会创建foo/a, foo/b, ... foo/h, bar/a, bar/b, ... bar/h这些文件
+touch {foo,bar}/{a..h}
+touch foo/x bar/y
+# 比较文件夹 foo 和 bar 中包含文件的不同
+diff <(ls foo) <(ls bar)
+# 输出
+# < x
+# ---
+# > y
+
脚本并不一定只有用 bash 写才能在终端里调用。比如说,这是一段 Python 脚本,作用是将输入的参数倒序输出:
*#!/usr/local/bin/python //* #!/usr/bin/env python
+import sys
+for arg in reversed(sys.argv[1:]):
+ print(arg)
+
内核知道去用 python 解释器而不是 shell 命令来运行这段脚本,是因为脚本的开头第一行的 shebang (opens new window)。
在 shebang
行中使用 [env](https://man7.org/linux/man-pages/man1/env.1.html)
命令是一种好的实践,它会利用环境变量中的程序来解析该脚本,这样就提高了您的脚本的可移植性。env
会利用我们第一节讲座中介绍过的PATH
环境变量来进行定位。 例如,使用了env
的 shebang 看上去是这样的#!/usr/bin/env python
。
shell 函数和脚本有如下一些不同点:
shebang
是很重要的。[export](https://man7.org/linux/man-pages/man1/export.1p.html)
将环境变量导出,并将值传递给环境变量。编写 bash
脚本有时候会很别扭和反直觉。例如 shellcheck (opens new window) 这样的工具可以帮助你定位 sh/bash 脚本中的错误。
看到这里,您可能会有疑问,我们应该如何为特定的命令找到合适的标记呢?例如 ls -l
, mv -i
和 mkdir -p
。更普遍的是,给您一个命令行,您应该怎样了解如何使用这个命令行并找出它的不同的选项呢? 一般来说,您可能会先去网上搜索答案,但是,UNIX 可比 StackOverflow 出现的早,因此我们的系统里其实早就包含了可以获取相关信息的方法。
在上一节中我们介绍过,最常用的方法是为对应的命令行添加-h
或 --help
标记。另外一个更详细的方法则是使用man
命令。[man](https://man7.org/linux/man-pages/man1/man.1.html)
命令是手册(manual)的缩写,它提供了命令的用户手册。
例如,man rm
会输出命令 rm
的说明,同时还有其标记列表,包括之前我们介绍过的-i
。 事实上,目前我们给出的所有命令的说明链接,都是网页版的 Linux 命令手册。即使是您安装的第三方命令,前提是开发者编写了手册并将其包含在了安装包中。在交互式的、基于字符处理的终端窗口中,一般也可以通过 :help
命令或键入 ?
来获取帮助。
有时候手册内容太过详实,让我们难以在其中查找哪些最常用的标记和语法。 TLDR pages (opens new window) 是一个很不错的替代品,它提供了一些案例,可以帮助您快速找到正确的选项。
snap install tldr # in Ubuntu
+
程序员们面对的最常见的重复任务就是查找文件或目录。所有的类 UNIX 系统都包含一个名为 [find](https://man7.org/linux/man-pages/man1/find.1.html)
的工具,它是 shell 上用于查找文件的绝佳工具。find
命令会递归地搜索符合条件的文件,例如:
# 查找所有名称为src的文件夹
+find . -name src -type d
+# 查找前一天修改的所有文件
+find . -mtime -1
+# 查找所有大小在500k至10M的tar.gz文件
+find . -size +500k -size -10M -name '*.tar.gz'
+
# 查找所有文件夹路径中包含test的python文件
+find . -path '/test/*.py' -type f
+
find .
:这个命令从当前目录(.
代表当前目录)开始递归搜索。path '/test/*.py'
:这部分指定了搜索的路径模式:
+/test/\*.py
中,它表示可以匹配任何深度的目录层级,直到遇到test
目录。test
是一个具体的目录名,表示在任何可能的位置中寻找名为test
的目录。.py
:这个模式匹配所有以.py
结尾的文件,即 Python 脚本文件。type f
:这个参数告诉find
命令只关心文件(f
代表文件),不要在结果中包括目录或其他类型的文件系统对象。除了列出所寻找的文件之外,find 还能对所有查找到的文件进行操作。这能极大地简化一些单调的任务。
*# 删除全部扩展名为.tmp 的文件*
+find . -name '*.tmp' -exec rm {} \;
+*# 查找全部的 PNG 文件并将其转换为 JPG*
+find . -name '*.png' -exec convert {} {}.jpg \;
+
这段代码是一个在 Unix 或类 Unix 系统中使用的 shell 命令,用于查找当前目录及其子目录中所有的.png
文件,并将每个找到的文件转换为.jpg
格式。这里一步一步解释这个命令:
find . -name '*.png'
:这个命令从当前目录(.
代表当前目录)开始递归搜索所有扩展名为.png
的文件。name '*.png'
指定了要匹配的文件名模式,星号``是一个通配符,表示任意数量的任意字符。exec convert {} {}.jpg \;
:这部分指定了find
命令找到每个文件后要执行的操作。exec
后面跟的是要执行的命令,这里使用convert
命令来转换图像格式。
+convert
:这是 ImageMagick 工具集中的一个命令,用于转换图像格式。{}
:这是一个特殊的占位符,对于每个匹配的文件,find
命令都会在这个位置插入文件的路径。{}.jpg
:这指定了输出文件的名称。对于每个输入文件,这会在原文件名(即{}
)后添加.jpg
扩展名,创建一个新的文件名。\;
:这个分号表示exec
参数的结束,并且需要转义(\;
)来避免被 shell 解释。尽管 find
用途广泛,它的语法却比较难以记忆。例如,为了查找满足模式 PATTERN
的文件,您需要执行 find -name '*PATTERN*'
(如果您希望模式匹配时是不区分大小写,可以使用-iname
选项)
您当然可以使用 alias 设置别名来简化上述操作,但 shell 的哲学之一便是寻找(更好用的)替代方案。 记住,shell 最好的特性就是您只是在调用程序,因此您只要找到合适的替代程序即可(甚至自己编写)。
例如,[fd](https://github.com/sharkdp/fd)
就是一个更简单、更快速、更友好的程序,它可以用来作为find
的替代品。它有很多不错的默认设置,例如输出着色、默认支持正则匹配、支持 unicode 并且我认为它的语法更符合直觉。以模式PATTERN
搜索的语法是 fd PATTERN
。
sudo apt install fdfind
+
下面是关于如何使用 fd
以及一些实际的例子:
fd
基本用法fd
后跟你要搜索的文件名或模式,它会在当前目录及其子目录下查找匹配的文件。例如,要查找所有的 .txt
文件,你可以使用:fd '\.txt$'
+
fd
在其中搜索。例如,要在 ~/Documents
目录中搜索 .pdf
文件:fd '\.pdf$' ~/Documents
+
fd
高级用法fd
默认使用智能大小写搜索。如果你想强制执行不区分大小写的搜索,可以使用 i
选项。例如,查找所有的 README
文件(不区分大小写):fd -i 'readme'
+
fd
支持正则表达式,让你可以进行更复杂的搜索。例如,查找所有以 a
开头,以 z
结尾的文件:fd '^a.*z$'
+
E
选项。例如,搜索所有 .js
文件,但排除掉 node_modules
目录:fd '\.js$' -E node_modules
+
fd
的输出可以通过管道传递给其他命令。例如,你可以使用 xargs
结合 fd
来对找到的文件执行操作。下面的命令将找到所有 .tmp
文件并删除它们:fd '\.tmp$' | xargs rm
+
fd
实用示例快速查找特定文件:如果你想找到所有的 JPEG 图片文件,可以使用如下命令:
fd '\.jpg$'
+
在特定目录下搜索:如果你需要在 /var/log
目录下查找扩展名为 .log
的文件,可以使用:
fd '\.log$' /var/lo
+
大多数人都认为 find
和 fd
已经很好用了,但是有的人可能想知道,我们是不是可以有更高效的方法,例如不要每次都搜索文件而是通过编译索引或建立数据库的方式来实现更加快速地搜索。
这就要靠 [locate](https://man7.org/linux/man-pages/man1/locate.1.html)
了。 locate
使用一个由 [updatedb](https://man7.org/linux/man-pages/man1/updatedb.1.html)
负责更新的数据库,在大多数系统中 updatedb
都会通过 [cron](https://man7.org/linux/man-pages/man8/cron.8.html)
每日更新。这便需要我们在速度和时效性之间作出权衡。而且,find
和类似的工具可以通过别的属性比如文件大小、修改时间或是权限来查找文件,locate
则只能通过文件名。 这里 (opens new window)有一个更详细的对比。
查找文件是很有用的技能,但是很多时候您的目标其实是查看文件的内容。一个最常见的场景是您希望查找具有某种模式的全部文件,并找它们的位置。
为了实现这一点,很多类 UNIX 的系统都提供了[grep](https://man7.org/linux/man-pages/man1/grep.1.html)
命令,它是用于对输入文本进行匹配的通用工具。它是一个非常重要的 shell 工具,我们会在后续的数据清理课程中深入的探讨它。
grep
有很多选项,这也使它成为一个非常全能的工具。其中我经常使用的有 -C
:获取查找结果的上下文(Context);-v
将对结果进行反选(Invert),也就是输出不匹配的结果。举例来说, grep -C 5
会输出匹配结果前后五行。当需要搜索大量文件的时候,使用 -R
会递归地进入子目录并搜索所有的文本文件。
但是,我们有很多办法可以对 grep -R
进行改进,例如使其忽略.git
文件夹,使用多 CPU 等等。
因此也出现了很多它的替代品,包括 ack (opens new window), ag (opens new window) 和 rg (opens new window)。它们都特别好用,但是功能也都差不多,我比较常用的是 ripgrep (rg
) ,因为它速度快,而且用法非常符合直觉。例子如下:
# 查找所有使用了 requests 库的文件
+rg -t py 'import requests'
+# 查找所有没有写 shebang 的文件(包含隐藏文件)
+rg -u --files-without-match "^#!"
+# 查找所有的foo字符串,并打印其之后的5行
+rg foo -A 5
+# 打印匹配的统计信息(匹配的行和文件的数量)
+rg --stats PATTERN
+
首先,按向上的方向键会显示你使用过的上一条命令,继续按上键则会遍历整个历史记录。
history
命令允许您以程序员的方式来访问 shell 中输入的历史命令。这个命令会在标准输出中打印 shell 中的历史命令。如果我们要搜索历史记录,则可以利用管道将输出结果传递给 grep
进行模式搜索。 history | grep find
会打印包含 find 子串的命令。
对于大多数的 shell 来说,您可以使用 Ctrl+R
对命令历史记录进行回溯搜索。敲 Ctrl+R
后您可以输入子串来进行匹配,查找历史命令行。
反复按下就会在所有搜索结果中循环。在 zsh (opens new window) 中,使用方向键上或下也可以完成这项工作。
Ctrl+R
可以配合 fzf (opens new window) 使用。fzf
是一个通用的模糊查找工具,它可以和很多命令一起使用。这里我们可以对历史命令进行模糊查找并将结果以赏心悦目的格式输出。
搜索历史命令:
你可以通过管道将 history
命令的输出送入 fzf
,然后 fzf
会提供一个交互式界面让你模糊搜索历史命令:
history | fzf
+
这将展示一个交互式列表,你可以开始输入来过滤历史命令。当你找到需要的命令时,按 Enter 键,fzf
将把这个命令输出到标准输出。
执行选中的历史命令:
如果你想直接执行选择的命令,可以使用以下技巧结合 xargs
:
history | fzf | awk '{print $2}' | xargs -I {} bash -c "{}"
+
这条命令的工作流程是:
history | fzf
:从历史中选择一个命令。awk '{print $2}'
:假设你的历史格式是编号后跟命令,这将抽取命令部分(根据你的 shell 历史格式可能需要调整)。xargs -I {} bash -c "{}"
:执行选中的命令。你可以修改 shell history 的行为,例如,如果在命令的开头加上一个空格,它就不会被加进 shell 记录中。当你输入包含密码或是其他敏感信息的命令时会用到这一特性。 为此你需要在.bashrc
中添加HISTCONTROL=ignorespace
或者向.zshrc
添加 setopt HIST_IGNORE_SPACE
。 如果你不小心忘了在前面加空格,可以通过编辑 .bash_history
或 .zhistory
来手动地从历史记录中移除那一项。
之前对所有操作我们都默认一个前提,即您已经位于想要执行命令的目录下,但是如何才能高效地在目录间随意切换呢?有很多简便的方法可以做到,比如设置 alias,使用 ln -s (opens new window) 创建符号连接等。而开发者们已经想到了很多更为精妙的解决方案。
由于本课程的目的是尽可能对你的日常习惯进行优化。因此,我们可以使用[fasd](https://github.com/clvv/fasd)
和 autojump
(opens new window) 这两个工具来查找最常用或最近使用的文件和目录。
Fasd 基于 *frecency* (opens new window) 对文件和文件排序,也就是说它会同时针对频率(frequency)和时效(recency)进行排序。默认情况下,fasd
使用命令 z
帮助我们快速切换到最常访问的目录。例如, 如果您经常访问/home/user/files/cool_project
目录,那么可以直接使用 z cool
跳转到该目录。对于 autojump,则使用j cool
代替即可。
还有一些更复杂的工具可以用来概览目录结构,例如 [tree](https://linux.die.net/man/1/tree)
, [broot](https://github.com/Canop/broot)
或更加完整的文件管理器,例如 [nnn](https://github.com/jarun/nnn)
或 [ranger](https://github.com/ranger/ranger)
。
阅读 [man ls](https://man7.org/linux/man-pages/man1/ls.1.html)
,然后使用ls
命令进行如下操作:
典型输出如下:
-rw-r--r-- 1 user group 1.1M Jan 14 09:53 baz
+ drwxr-xr-x 5 user group 160 Jan 14 09:53 .
+ -rw-r--r-- 1 user group 514 Jan 14 06:42 bar
+ -rw-r--r-- 1 user group 106M Jan 13 12:12 foo
+ drwx------+ 47 user group 1.5K Jan 12 18:08 ..
+
编写两个 bash 函数 marco
和 polo
执行下面的操作。 每当你执行 marco
时,当前的工作目录应当以某种形式保存,当执行 polo
时,无论现在处在什么目录下,都应当 cd
回到当时执行 marco
的目录。 为了方便 debug,你可以把代码写在单独的文件 marco.sh
中,并通过 source marco.sh
命令,(重新)加载函数。
#!/usr/bin/env zsh
+ marco(){
+ echo "$(pwd)" > $HOME/marco_history.log
+ echo "save pwd $(pwd)"
+ }
+ polo(){
+ cd "$(cat "$HOME/marco_history.log")"
+ }
+
#!/usr/bin/env zsh
+marco() {
+ export MARCO=$(pwd)
+}
+polo() {
+ cd "$MARCO"
+}
+
Answer:
marco(){
+ current_dir=$(pwd)
+}
+polo(){
+ cd $current_dir
+}
+
假设您有一个命令,它很少出错。因此为了在出错时能够对其进行调试,需要花费大量的时间重现错误并捕获输出。 编写一段 bash 脚本,运行如下的脚本直到它出错,将它的标准输出和标准错误流记录到文件,并在最后输出所有内容。
加分项:报告脚本在失败前共运行了多少次。
#!/usr/bin/env zsh
+
+n=$(( RANDOM % 100 ))
+
+if [[ n -eq 42 ]]; then
+ echo "Something went wrong"
+ >&2 echo "The error was using magic numbers"
+ exit 1
+fi
+
+echo "Everything went according to plan"
+
Answer:
#!/usr/bin/env zsh
+
+chmod 777 3script.sh
+normCnt=0
+while true; do
+ ./3script.sh >> 3log.txt 2> 3err.txt
+ if [[ $? -eq 1 ]]; then
+ echo "The script failed"
+ echo "The script succeeded $normCnt times before failed"
+ break
+ fi
+ ((normCnt++))
+done
+
本节课我们讲解的 find
命令中的 exec
参数非常强大,它可以对我们查找的文件进行操作。但是,如果我们要对所有文件进行操作呢?例如创建一个 zip 压缩文件?我们已经知道,命令行可以从参数或标准输入接受输入。在用管道连接命令时,我们将标准输出和标准输入连接起来,但是有些命令,例如tar
则需要从参数接受输入。这里我们可以使用[xargs](https://man7.org/linux/man-pages/man1/xargs.1.html)
命令,它可以使用标准输入中的内容作为参数。 例如 ls | xargs rm
会删除当前目录中的所有文件。
您的任务是编写一个命令,它可以递归地查找文件夹中所有的 HTML 文件,并将它们压缩成 zip 文件。注意,即使文件名中包含空格,您的命令也应该能够正确执行(提示:查看 xargs
的参数-d
,译注:MacOS 上的 xargs
没有-d
,查看这个 issue (opens new window))
如果您使用的是 MacOS,请注意默认的 BSD find
与 GNU coreutils (opens new window) 中的是不一样的。你可以为find
添加-print0
选项,并为xargs
添加-0
选项。作为 Mac 用户,您需要注意 mac 系统自带的命令行工具和 GNU 中对应的工具是有区别的;如果你想使用 GNU 版本的工具,也可以使用 brew 来安装 (opens new window)。
(进阶)编写一个命令或脚本递归的查找文件夹中最近使用的文件。更通用的做法,你可以按照最近的使用时间列出文件吗?
管道实现的是将前面的输出stdout
作为后面的输入stdin
,但是有些命令不接受管道的传递方式。例如:ls
,这是为什么呢?
因为有些命令希望管道传递过来的是参数,但是直接使用管道有时无法传递到命令的参数位。这时候就需要xargs
,xargs
实现的是将管道传递过来的stdin
进行处理然后传递到命令的参数位置上。
xargs -0
可以处理接收到的stdin
中的 null 字符(\0)
。如果不使用-0
选项或-null
选项,检测到\0
后会给出警告提醒,并只向命令传递非\0
段。
+ ← + + 2024.03.14-1. The Shell + + 2024.03.20-3. Vim + + → +
Vim 的设计以大多数时间都花在阅读、浏览和进行少量编辑改动为基础,因此它具有多种操作模式:
在不同的操作模式下,键盘敲击的含义也不同。比如,x
在插入模式会插入字母 x
,但是在正常模式 会删除当前光标所在的字母,在可视模式下则会删除选中文块。
在默认设置下,Vim 会在左下角显示当前的模式。Vim 启动时的默认模式是正常模式。通常你会把大部分 时间花在正常模式和插入模式。
你可以按下 <ESC>
(退出键)从任何其他模式返回正常模式。在正常模式,键入 i
进入插入 模式,R
进入替换模式,v
进入可视(一般)模式,V
进入可视(行)模式,<C-v>
(Ctrl-V, 有时也写作 ^V
)进入可视(块)模式,:
进入命令模式。
因为你会在使用 Vim 时大量使用 <ESC>
键,所以可以考虑把大小写锁定键重定义成 <ESC>
键(MacOS 教程 (opens new window))。
在正常模式,键入 i
进入插入模式。现在 Vim 跟很多其他的编辑器一样,直到你键入 <ESC>
返回正常模式。你只需要掌握这一点和上面介绍的所有基础知识就可以使用 Vim 来编辑文件了 (虽然如果你一直停留在插入模式内不一定高效)。
Vim 会维护一系列打开的文件,称为“缓存”。一个 Vim 会话包含一系列标签页,每个标签页包含 一系列窗口(分隔面板)。每个窗口显示一个缓存。跟网页浏览器等其他你熟悉的程序不一样的是, 缓存和窗口不是一一对应的关系;窗口只是视角。一个缓存可以在多个窗口打开,甚至在同一 个标签页内的多个窗口打开。这个功能其实很好用,比如在查看同一个文件的不同部分的时候。
Vim 默认打开一个标签页,这个标签也包含一个窗口。
在正常模式下键入 :
进入命令行模式。 在键入 :
后,你的光标会立即跳到屏幕下方的命令行。 这个模式有很多功能,包括打开,保存,关闭文件,以及 退出 Vim (opens new window)。
:q
退出(关闭窗口):w
保存(写):wq
保存然后退出:e {文件名}
打开要编辑的文件:ls
显示打开的缓存:help {标题}
打开帮助文档
+:help :w
打开 :w
命令的帮助文档:help w
打开 w
移动的帮助文档多数时候你会在正常模式下,使用移动命令在缓存中导航。在 Vim 里面移动也被称为 “名词”, 因为它们指向文字块。
hjkl
(左, 下, 上, 右)w
(下一个词), b
(词初), e
(词尾)0
(行初), ^
(第一个非空格字符), $
(行尾)H
(屏幕首行), M
(屏幕中间), L
(屏幕底部)Ctrl-u
(上翻), Ctrl-d
(下翻)gg
(文件头), G
(文件尾):{行数}<CR>
或者 {行数}G
({行数}为行数)%
(找到配对,比如括号或者 /* */ 之类的注释对)f{字符}
, t{字符}
, F{字符}
, T{字符}
,
/ ;
用于导航匹配/{正则表达式}
, n
/ N
用于导航匹配所有你需要用鼠标做的事, 你现在都可以用键盘:采用编辑命令和移动命令的组合来完成。 这就是 Vim 的界面开始看起来像一个程序语言的时候。Vim 的编辑命令也被称为 “动词”, 因为动词可以施动于名词。
i
进入插入模式
+O
/ o
在之上/之下插入行d{移动命令}
删除 {移动命令}
+dw
删除词, d$
删除到行尾, d0
删除到行头。c{移动命令}
改变 {移动命令}
+cw
改变词d{移动命令}
再 i
x
删除字符(等同于 dl
)s
替换字符(等同于 xi
)d
删除 或者 c
改变u
撤销, <C-r>
重做y
复制 / “yank” (其他一些命令比如 d
也会复制)yy
复制一行p
粘贴~
改变字符的大小写你可以用一个计数来结合“名词”和“动词”,这会执行指定操作若干次。
3w
向后移动三个词5j
向下移动 5 行7dw
删除 7 个词你可以用修饰语改变“名词”的意义。修饰语有 i
,表示“内部”或者“在内”,和 a
, 表示“周围”。
ci(
改变当前括号内的内容ci[
改变当前方括号内的内容da'
删除一个单引号字符串, 包括周围的单引号Vim 由一个位于 ~/.vimrc
的文本配置文件(包含 Vim 脚本命令)。你可能会启用很多基本 设置。
我们提供一个文档详细的基本设置,你可以用它当作你的初始设置。我们推荐使用这个设置因为 它修复了一些 Vim 默认设置奇怪行为。 在这儿 (opens new window) 下载我们的设置,然后将它保存成 ~/.vimrc
.
Vim 能够被重度自定义,花时间探索自定义选项是值得的。你可以参考其他人的在 GitHub 上共享的设置文件,比如,你的授课人的 Vim 设置 (Anish (opens new window), Jon (opens new window) (uses neovim (opens new window)), Jose (opens new window))。 有很多好的博客文章也聊到了这个话题。尽量不要复制粘贴别人的整个设置文件, 而是阅读和理解它,然后采用对你有用的部分。
Vim 有很多扩展插件。跟很多互联网上已经过时的建议相反,你不需要在 Vim 使用一个插件 管理器(从 Vim 8.0 开始)。你可以使用内置的插件管理系统。只需要创建一个 ~/.vim/pack/vendor/start/
的文件夹,然后把插件放到这里(比如通过 git clone
)。
以下是一些我们最爱的插件:
我们尽量避免在这里提供一份冗长的插件列表。你可以查看讲师们的开源的配置文件 (Anish (opens new window), Jon (opens new window), Jose (opens new window)) 来看看我们使用的其他插件。 浏览 Vim Awesome (opens new window) 来了解一些很棒的插件。 这个话题也有很多博客文章:搜索 “best Vim plugins”。
这里我们提供了一些展示这个编辑器能力的例子。我们无法把所有的这样的事情都教给你,但是你可以在使用中学习。一个好的对策是: 当你在使用你的编辑器的时候感觉 “一定有更好的方法来做这个”, 那么很可能真的有:上网搜寻一下。
:s
(替换)命令(文档 (opens new window))。
%s/foo/bar/g
%s/\[.*\](\(.*\))/\1/g
:sp
/ :vsp
来分割窗口q{字符}
来开始在寄存器{字符}
中录制宏q
停止录制@{字符}
重放宏{计数}@{字符}
执行一个宏{计数}次q{字符}q
清除宏@{字符}
来递归调用该宏 (在录制完成之前不会有任何操作)g/people/d
%s/<person>/{/g
%s/<name>\(.*\)<\/name>/"name": "\1",/g
ggdd
, Gdd
删除第一行和最后一行e
)
+<name>
的行qe^r"f>s": "<ESC>f<C"<ESC>q
<person>
的行qpS{<ESC>j@eA,<ESC>j@ejS},<ESC>q
<person>
的行qq@pjq
999@q
,
然后加上 [
和 ]
分隔符vimtutor
是一个 Vim 安装时自带的教程查看所有日志(默认情况下 ,只保存本次启动的日志)
journalctl
查看内核日志(不显示应用日志)
journalctl -k
查看系统本次启动的日志
journalctl -b
journalctl -b -0
查看上一次启动的日志(需更改设置)
journalctl -b -1
ssh myserver journalctl | grep sshd
+
这里我们使用管道将一个远程服务器上的文件传递给本机的 grep
程序! ssh
太牛了,下一节课我们会讲授命令行环境,届时我们会详细讨论 ssh
的相关内容。此时我们打印出的内容,仍然比我们需要的要多得多,读起来也非常费劲。我们来改进一下:
ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' | less
+
多出来的引号是什么作用呢?这么说吧,我们的日志是一个非常大的文件,把这么大的文件流直接传输到我们本地的电脑上再进行过滤是对流量的一种浪费。因此我们采取另外一种方式,我们先在远端机器上过滤文本内容,然后再将结果传输到本机。 less
为我们创建来一个文件分页器,使我们可以通过翻页的方式浏览较长的文本。
为了进一步节省流量,我们甚至可以将当前过滤出的日志保存到文件中,这样后续就不需要再次通过网络访问该文件了:
ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' > ssh.log
+less ssh.log
+
过滤结果中仍然包含不少没用的数据。我们有很多办法可以删除这些无用的数据,但是让我们先研究一下 sed
这个非常强大的工具。
sed
是一个基于文本编辑器ed
构建的”流编辑器” 。在 sed
中,您基本上是利用一些简短的命令来修改文件,而不是直接操作文件的内容(尽管您也可以选择这样做)。相关的命令行非常多,但是最常用的是 s
,即替换命令,例如我们可以这样写:
ssh myserver journalctl
+ | grep sshd
+ | grep "Disconnected from"
+ | sed 's/.*Disconnected from //'
+
上面这段命令中,我们使用了一段简单的正则表达式。正则表达式是一种非常强大的工具,可以让我们基于某种模式来对字符串进行匹配。s
命令的语法如下:s/REGEX/SUBSTITUTION/
, 其中 REGEX
部分是我们需要使用的正则表达式,而 SUBSTITUTION
是用于替换匹配结果的文本。
正则表达式通常以(尽管并不总是) /
开始和结束。大多数的 ASCII 字符都表示它们本来的含义,但是有一些字符确实具有表示匹配行为的“特殊”含义。不同字符所表示的含义,根据正则表达式的实现方式不同,也会有所变化,这一点确实令人沮丧。常见的模式有:
.
除换行符之外的”任意单个字符”+
匹配前面字符一次或多次[abc]
匹配 a
, b
和 c
中的任意一个(RX1|RX2)
任何能够匹配RX1
或 RX2
的结果^
行首$
行尾正则表达式会如何匹配?*
和 +
在默认情况下是贪婪模式,也就是说,它们会尽可能多的匹配文本。对于某些正则表达式的实现来说,您可以给 *
或 +
增加一个?
后缀使其变成非贪婪模式
sed -E 's/.*Disconnected from (invalid |authenticating )?user .* [^ ]+ port [0-9]+( \[preauth\])?$//'
+
IP 地址正则表达式:
(?:(?:1[0-9][0-9]\.)|(?:2[0-4][0-9]\.)|(?:25[0-5]\.)|(?:[1-9][0-9]\.)|(?:[0-9]\.)){3}(?:(?:1[0-9][0-9])|(?:2[0-4][0-9])|(?:25[0-5])|(?:[1-9][0-9])|(?:[0-9]))
+
我们实际上希望能够将用户名保留下来。对此,我们可以使用“捕获组(capture groups)”来完成。被圆括号内的正则表达式匹配到的文本,都会被存入一系列以编号区分的捕获组中。捕获组的内容可以在替换字符串时使用(有些正则表达式的引擎甚至支持替换表达式本身),例如\1
、 \2
、\3
等等,因此可以使用如下命令:
sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
+
在练习之前,需要大家知道一些基本知识,如果有一定基础的可以跳过该步骤,直接往下看。
[abc]:代表a或者b,或者c字符中的一个。
+[^abc]:代表除a,b,c以外的任何字符。
+[a-z]:代表a-z的所有小写字符中的一个。
+[A-Z]:代表A-Z的所有大写字符中的一个。
+[0-9]:代表0-9之间的某一个数字字符。
+[a-zA-Z0-9]:代表a-z或者A-Z或者0-9之间的任意一个字符。
+[a-dm-p]:a 到 d 或 m 到 p之间的任意一个字符。
+
&&:并且
+| :或者(可以省略)
+
“.” : 匹配任何字符。
+“\d”:任何数字[0-9]的简写;
+“\D”:任何非数字[^0-9]的简写;
+“\s”: 空白字符:[ \t\n\x0B\f\r] 的简写
+“\S”: 非空白字符:[^\s] 的简写
+“\w”:单词字符:[a-zA-Z_0-9]的简写
+“\W”:非单词字符:[^\w]
+
x? : 0次或1次
+x* : 0次到多次
+x+ : 1次或多次
+X{n} : 恰好n次
+X{n,} : 至少n次
+X{n,m}: n到m次(n和m都是包含的,最少n次,最多m次。
+
ssh myserver journalctl
+ | grep sshd
+ | grep "Disconnected from"
+ | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
+ | sort | uniq -c
+
sort
会对其输入数据进行排序。uniq -c
会把连续出现的行折叠为一行并使用出现次数作为前缀。我们希望按照出现次数排序,过滤出最常出现的用户名:
ssh myserver journalctl
+ | grep sshd
+ | grep "Disconnected from"
+ | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
+ | sort | uniq -c
+ | sort -nk1,1 | tail -n10
+
sort -n
会按照数字顺序对输入进行排序(默认情况下是按照字典序排序) -k1,1
则表示“仅基于以空格分割的第一列进行排序”。,n
部分表示“仅排序到第 n 个部分”,默认情况是到行尾。就本例来说,针对整个行进行排序也没有任何问题,我们这里主要是为了学习这一用法!
如果我们希望得到登录次数最少的用户,我们可以使用 head
来代替tail
。或者使用sort -r
来进行倒序排序。
相当不错。但我们只想获取用户名,而且不要一行一个地显示:
ssh myserver journalctl
+ | grep sshd
+ | grep "Disconnected from"
+ | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
+ | sort | uniq -c
+ | sort -nk1,1 | tail -n10
+ | awk '{print $2}' | paste -sd,
+
awk
其实是一种编程语言,只不过它碰巧非常善于处理文本。
首先, {print $2}
的作用是什么? awk
程序接受一个模式串(可选),以及一个代码块,指定当模式匹配时应该做何种操作。默认当模式串即匹配所有行(上面命令中当用法)。 在代码块中,$0
表示整行的内容,$1
到 $n
为一行中的 n 个区域,区域的分割基于 awk
的域分隔符(默认是空格,可以通过-F
来修改)。在这个例子中,我们的代码意思是:对于每一行文本,打印其第二个部分,也就是用户名。
让我们康康,还有什么炫酷的操作可以做。让我们统计一下所有以c
开头,以 e
结尾,并且仅尝试过一次登录的用户。
| awk '$1 == 1 && $2 ~ /^c[^ ]*e$/ { print $2 }' | wc -l
+
让我们好好分析一下。首先,注意这次我们为 awk
指定了一个匹配模式串(也就是{...}
前面的那部分内容)。该匹配要求文本的第一部分需要等于 1(这部分刚好是uniq -c
得到的计数值),然后其第二部分必须满足给定的一个正则表达式。代码块中的内容则表示打印用户名。然后我们使用 wc -l
统计输出结果的行数。
不过,既然 awk
是一种编程语言,那么则可以这样:
BEGIN { rows = 0 }
+$1 == 1 && $2 ~ /^c[^ ]*e$/ { rows += $1 }
+END { print rows }
+
BEGIN
也是一种模式,它会匹配输入的开头( END
则匹配结尾)。然后,对每一行第一个部分进行累加,最后将结果输出。事实上,我们完全可以抛弃 grep
和 sed
,因为 awk
就可以解决所有问题 (opens new window)。至于怎么做,就留给读者们做课后练习吧。
想做数学计算也是可以的!例如这样,您可以将每行的数字加起来:
| paste -sd+ | bc -l
+
下面这种更加复杂的表达式也可以:
echo "2*($(data | paste -sd+))" | bc -l
+
如果您希望绘制一些简单的图表, gnuplot
可以帮助到您:
ssh myserver journalctl
+ | grep sshd
+ | grep "Disconnected from"
+ | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
+ | sort | uniq -c
+ | sort -nk1,1 | tail -n10
+ | gnuplot -p -e 'set boxwidth 0.5; plot "-" using 1:xtic(2) with boxes'
+
统计 words 文件 (/usr/share/dict/words
) 中包含至少三个a
且不以's
结尾的单词个数。这些单词中,出现频率前三的末尾两个字母是什么? sed
的 y
命令,或者 tr
程序也许可以帮你解决大小写的问题。共存在多少种词尾两字母组合?还有一个很 有挑战性的问题:哪个组合从未出现过?
#! /usr/bin/env zsh
+# 2.sh
+
+for i in {a..z}; do
+ for j in {a..z}; do
+ echo $i$j
+ done
+done > allcomb.txt
+
+cat words.txt | grep -P '(?:a.*){3,}' | sed -E "/'s$/d" | sed -E 's/.*(..)$/\1/' | tr '[:upper:]' '[:lower:]' | sort -u > appcomb.txt
+
+grep -vxFf appcomb.txt allcomb.txt | sort | uniq > notappcomb.txt
+
进行原地替换听上去很有诱惑力,例如: sed s/REGEX/SUBSTITUTION/ input.txt > input.txt
。但是这并不是一个明智的做法,为什么呢?还是说只有 sed
是这样的? 查看 man sed
来完成这个问题
Answer:
当你尝试使用 sed
命令进行原地替换,如使用命令 sed s/REGEX/SUBSTITUTION/ input.txt > input.txt
,看似想要直接在源文件上执行替换操作,实际上这样做是有问题的。这个命令的问题在于它试图将输出重定向回输入文件,这并不是 sed
命令或其他文本处理命令特有的问题,而是 Unix/Linux shell 处理重定向的方式所导致的。
当你执行上述命令时,shell 会先处理重定向(>
),这导致 input.txt
被打开用于写入并且立即被截断(即文件内容被清空),然后 sed
才开始从这个现在已经是空的文件读取数据。结果是,sed
没有数据可供处理,因此也就没有任何数据写回到 input.txt
中,导致文件内容丢失。
对于 sed
,如果你想进行原地编辑,应该使用 -i
选项(或 --in-place
),这样可以直接在文件上执行修改操作而不需要重定向输出到新文件:
sed -i 's/REGEX/SUBSTITUTION/' input.txt
+
这个 -i
选项告诉 sed
直接修改文件内容,而不是输出到标准输出。注意,-i
选项在不同的系统中可能稍有不同,某些系统可能要求为 -i
指定一个扩展名,用于在修改前保存原文件的备份。
例如,如果你希望备份原文件,可以这样做:
sed -i.bak 's/REGEX/SUBSTITUTION/' input.txt
+
这会将原始的 input.txt
保存为 input.txt.bak
,并直接修改 input.txt
。
总结,这并不是只有 sed
会遇到的问题,任何尝试将输出重定向回输入文件的操作都可能遇到同样的问题,因为 Unix/Linux shell 会先处理重定向,导致输入文件被清空。所以,当需要原地编辑文件时,应该使用工具提供的原地编辑功能(比如 sed
的 -i
选项),而不是使用重定向。
找出您最近十次开机的开机时间平均数、中位数和最长时间。在 Linux 上需要用到 journalctl
,而在 macOS 上使用 log show
。找到每次起到开始和结束时的时间戳。
在 Linux 上类似这样:Logs begin at ...
和 systemd[577]: Startup finished in ...
在 macOS 上, 查找 (opens new window): === system boot:
和 Previous shutdown cause: 5
#! /usr/bin/env zsh
+# 4.sh
+
+for i in {0..9}; do
+ journalctl -b -$i | grep "Startup finished" | grep "kernel" | sed -E 's/.*= (.*)s\.$/\1/'
+done > ./startTime.txt
+
+minTime=$(cat startTime.txt | sort -n | head -1)
+echo "Minimum time: ${minTime}s"
+maxTime=$(cat startTime.txt | sort -n | tail -1)
+echo "Maximum time: ${maxTime}s"
+avgTime=$(cat startTime.txt | paste -sd+ | bc -l | awk '{print $1/10}')
+echo "Average time: ${avgTime}s"
+midTime=$(cat startTime.txt | sort -n | paste -sd\ | awk '{print ($5+$6)/2}')
+echo "Median time: ${midTime}s"
+
查看之前三次重启启动信息中不同的部分(参见 journalctl
的-b
选项)。将这一任务分为几个步骤,首先获取之前三次启动的启动日志,也许获取启动日志的命令就有合适的选项可以帮助您提取前三次启动的日志,亦或者您可以使用sed '0,/STRING/d'
来删除STRING
匹配到的字符串前面的全部内容。然后,过滤掉每次都不相同的部分,例如时间戳。下一步,重复记录输入行并对其计数(可以使用uniq
)。最后,删除所有出现过 3 次的内容(因为这些内容是三次启动日志中的重复部分)。
#! /usr/bin/env zsh
+# 5.sh
+
+for i in {0..2}; do
+ journalctl -b -$i >> ./last3.txt
+done
+
+cat last3.txt | sed -E "s/.*pi\ (.*)/\1/" | sort | uniq -c | sort -n | awk '$1!=3 {print}' > 2.5.txt
+
您的 shell 会使用 UNIX 提供的信号机制执行进程间通信。当一个进程接收到信号时,它会停止执行、处理该信号并基于信号传递的信息来改变其执行。就这一点而言,信号是一种软件中断。
在上面的例子中,当我们输入 Ctrl-C
时,shell 会发送一个SIGINT
信号到进程。
下面这个 Python 程序向您展示了捕获信号SIGINT
并忽略它的基本操作,它并不会让程序停止。为了停止这个程序,我们需要使用SIGQUIT
信号,通过输入Ctrl-\
可以发送该信号。
#!/usr/bin/env python
+import signal, time
+
+def handler(signum, time):
+ print("\nI got a SIGINT, but I am not stopping")
+
+signal.signal(signal.SIGINT, handler)
+i = 0
+while True:
+ time.sleep(.1)
+ print("\r{}".format(i), end="")
+ i += 1
+
如果我们向这个程序发送两次 SIGINT
,然后再发送一次 SIGQUIT
,程序会有什么反应?注意 ^
是我们在终端输入Ctrl
时的表示形式:
$ python sigint.py
+24^C
+I got a SIGINT, but I am not stopping
+26^C
+I got a SIGINT, but I am not stopping
+30^\[1] 39913 quit python sigint.pyƒ
+
尽管 SIGINT
和 SIGQUIT
都常常用来发出和终止程序相关的请求。SIGTERM
则是一个更加通用的、也更加优雅地退出信号。为了发出这个信号我们需要使用 [kill](https://www.man7.org/linux/man-pages/man1/kill.1.html)
命令, 它的语法是: kill -TERM <PID>
。
信号可以让进程做其他的事情,而不仅仅是终止它们。例如,SIGSTOP
会让进程暂停。在终端中,键入 Ctrl-Z
会让 shell 发送 SIGTSTP
信号,SIGTSTP
是 Terminal Stop 的缩写(即terminal
版本的 SIGSTOP)。
我们可以使用 [fg](https://www.man7.org/linux/man-pages/man1/fg.1p.html)
或 [bg](http://man7.org/linux/man-pages/man1/bg.1p.html)
命令恢复暂停的工作。它们分别表示在前台继续或在后台继续。
[jobs](http://man7.org/linux/man-pages/man1/jobs.1p.html)
命令会列出当前终端会话中尚未完成的全部任务。您可以使用 pid 引用这些任务(也可以用 [pgrep](https://www.man7.org/linux/man-pages/man1/pgrep.1.html)
找出 pid)。更加符合直觉的操作是您可以使用百分号 + 任务编号(jobs
会打印任务编号)来选取该任务。如果要选择最近的一个任务,可以使用 $!
这一特殊参数。
还有一件事情需要掌握,那就是命令中的 &
后缀可以让命令在直接在后台运行,这使得您可以直接在 shell 中继续做其他操作,不过它此时还是会使用 shell 的标准输出,这一点有时会比较恼人(这种情况可以使用 shell 重定向处理)。
让已经在运行的进程转到后台运行,您可以键入Ctrl-Z
,然后紧接着再输入bg
。注意,后台的进程仍然是您的终端进程的子进程,一旦您关闭终端(会发送另外一个信号SIGHUP
),这些后台的进程也会终止。为了防止这种情况发生,您可以使用 [nohup](https://www.man7.org/linux/man-pages/man1/nohup.1.html)
(一个用来忽略 SIGHUP
的封装) 来运行程序。针对已经运行的程序,可以使用disown
。除此之外,您可以使用终端多路复用器来实现,下一章节我们会进行详细地探讨。
下面这个简单的会话中展示来了些概念的应用:
$ sleep 1000
+^Z
+[1] + 18653 suspended sleep 1000
+
+$ nohup sleep 2000 &
+[2] 18745
+appending output to nohup.out
+
+$ jobs
+[1] + suspended sleep 1000
+[2] - running nohup sleep 2000
+
+$ bg %1
+[1] - 18653 continued sleep 1000
+
+$ jobs
+[1] - running sleep 1000
+[2] + running nohup sleep 2000
+
+$ kill -STOP %1
+[1] + 18653 suspended (signal) sleep 1000
+
+$ jobs
+[1] + suspended (signal) sleep 1000
+[2] - running nohup sleep 2000
+
+$ kill -SIGHUP %1
+[1] + 18653 hangup sleep 1000
+
+$ jobs
+[2] + running nohup sleep 2000
+
+$ kill -SIGHUP %2
+
+$ jobs
+[2] + running nohup sleep 2000
+
+$ kill %2
+[2] + 18745 terminated nohup sleep 2000
+
+$ jobs
+
SIGKILL
是一个特殊的信号,它不能被进程捕获并且它会马上结束该进程。不过这样做会有一些副作用,例如留下孤儿进程。
您可以在 这里 (opens new window) 或输入 [man signal](https://www.man7.org/linux/man-pages/man7/signal.7.html)
或使用 kill -l
来获取更多关于信号的信息。
当您在使用命令行时,您通常会希望同时执行多个任务。举例来说,您可以想要同时运行您的编辑器,并在终端的另外一侧执行程序。尽管再打开一个新的终端窗口也能达到目的,使用终端多路复用器则是一种更好的办法。
像 [tmux](https://www.man7.org/linux/man-pages/man1/tmux.1.html)
这类的终端多路复用器可以允许我们基于面板和标签分割出多个终端窗口,这样您便可以同时与多个 shell 会话进行交互。
不仅如此,终端多路复用使我们可以分离当前终端会话并在将来重新连接。
这让您操作远端设备时的工作流大大改善,避免了 nohup
和其他类似技巧的使用。
现在最流行的终端多路器是 [tmux](https://www.man7.org/linux/man-pages/man1/tmux.1.html)
。tmux
是一个高度可定制的工具,您可以使用相关快捷键创建多个标签页并在它们间导航。
tmux
的快捷键需要我们掌握,它们都是类似 <C-b> x
这样的组合,即需要先按下Ctrl+b
,松开后再按下 x
。tmux
中对象的继承结构如下:
tmux
开始一个新的会话tmux new -s NAME
以指定名称开始一个新的会话tmux ls
列出当前所有会话tmux
中输入 <C-b> d
,将当前会话分离tmux a
重新连接最后一个会话。您也可以通过 t
来指定具体的会话<C-b> c
创建一个新的窗口,使用 <C-d>
关闭<C-b> N
跳转到第 N 个窗口,注意每个窗口都是有编号的<C-b> p
切换到前一个窗口<C-b> n
切换到下一个窗口<C-b> ,
重命名当前窗口<C-b> w
列出当前所有窗口<C-b> "
水平分割<C-b> %
垂直分割<C-b> <方向>
切换到指定方向的面板,<方向> 指的是键盘上的方向键<C-b> z
切换当前面板的缩放<C-b> [
开始往回卷动屏幕。您可以按下空格键来开始选择,回车键复制选中的部分<C-b> <空格>
在不同的面板排布间切换扩展阅读: 这里 (opens new window) 是一份 tmux
快速入门教程, 而这一篇 (opens new window) 文章则更加详细,它包含了 screen
命令。您也许想要掌握 [screen](https://www.man7.org/linux/man-pages/man1/screen.1.html)
命令,因为在大多数 UNIX 系统中都默认安装有该程序。
输入一长串包含许多选项的命令会非常麻烦。因此,大多数 shell 都支持设置别名。shell 的别名相当于一个长命令的缩写,shell 会自动将其替换成原本的命令。例如,bash 中的别名语法如下:
alias alias_name="command_to_alias arg1 arg2"
+
注意, =
两边是没有空格的,因为 [alias](https://www.man7.org/linux/man-pages/man1/alias.1p.html)
是一个 shell 命令,它只接受一个参数。
# 创建常用命令的缩写
+alias ll="ls -lh"
+
+# 能够少输入很多
+alias gs="git status"
+alias gc="git commit"
+alias v="vim"
+
+# 手误打错命令也没关系
+alias sl=ls
+
+# 重新定义一些命令行的默认行为
+alias mv="mv -i" # -i prompts before overwrite
+alias mkdir="mkdir -p" # -p make parent dirs as needed
+alias df="df -h" # -h prints human readable format
+
+# 别名可以组合使用
+alias la="ls -A"
+alias lla="la -l"
+
+# 在忽略某个别名: 命令前加上反斜杠 \
+\ls
+# 或者禁用别名
+unalias la
+
+# 获取别名的定义
+alias ll
+# 会打印 ll='ls -lh'
+
值得注意的是,在默认情况下 shell 并不会保存别名。为了让别名持续生效,您需要将配置放进 shell 的启动文件里,像是.bashrc
或 .zshrc
。
很多程序的配置都是通过纯文本格式的被称作点文件的配置文件来完成的(之所以称为点文件,是因为它们的文件名以 .
开头,例如 ~/.vimrc
。也正因为此,它们默认是隐藏文件,ls
并不会显示它们)。
shell 的配置也是通过这类文件完成的。在启动时,您的 shell 程序会读取很多文件以加载其配置项。根据 shell 本身的不同,您从登录开始还是以交互的方式完成这一过程可能会有很大的不同。关于这一话题,这里 (opens new window) 有非常好的资源
对于 bash
来说,在大多数系统下,您可以通过编辑 .bashrc
或 .bash_profile
来进行配置。在文件中您可以添加需要在启动时执行的命令,例如上文我们讲到过的别名,或者是您的环境变量。
实际上,很多程序都要求您在 shell 的配置文件中包含一行类似 export PATH="$PATH:/path/to/program/bin"
的命令,这样才能确保这些程序能够被 shell 找到。
还有一些其他的工具也可以通过点文件进行配置:
bash
- ~/.bashrc
, ~/.bash_profile
git
- ~/.gitconfig
vim
- ~/.vimrc
和 ~/.vim
目录ssh
- ~/.ssh/config
tmux
- ~/.tmux.conf
我们应该如何管理这些配置文件呢,它们应该在它们的文件夹下,并使用版本控制系统进行管理,然后通过脚本将其 符号链接 到需要的地方。这么做有如下好处:
配置文件中需要放些什么?您可以通过在线文档和帮助手册 (opens new window)了解所使用工具的设置项。另一个方法是在网上搜索有关特定程序的文章,作者们在文章中会分享他们的配置。还有一种方法就是直接浏览其他人的配置文件:您可以在这里找到无数的dotfiles 仓库 (opens new window) —— 其中最受欢迎的那些可以在这里 (opens new window)找到(我们建议您不要直接复制别人的配置)。这里 (opens new window) 也有一些非常有用的资源。
本课程的老师们也在 GitHub 上开源了他们的配置文件: Anish (opens new window), Jon (opens new window), Jose (opens new window).
配置文件的一个常见的痛点是它可能并不能在多种设备上生效。例如,如果您在不同设备上使用的操作系统或者 shell 是不同的,则配置文件是无法生效的。或者,有时您仅希望特定的配置只在某些设备上生效。
有一些技巧可以轻松达成这些目的。如果配置文件 if 语句,则您可以借助它针对不同的设备编写不同的配置。例如,您的 shell 可以这样做:
if [[ "$(uname)" == "Linux" ]]; then {do_something}; fi
+
+# 使用和 shell 相关的配置时先检查当前 shell 类型
+if [[ "$SHELL" == "zsh" ]]; then {do_something}; fi
+
+# 您也可以针对特定的设备进行配置
+if [[ "$(hostname)" == "myServer" ]]; then {do_something}; fi
+
如果配置文件支持 include 功能,您也可以多加利用。例如:~/.gitconfig
可以这样编写:
[include]
+ path = ~/.gitconfig_local
+
然后我们可以在日常使用的设备上创建配置文件 ~/.gitconfig_local
来包含与该设备相关的特定配置。您甚至应该创建一个单独的代码仓库来管理这些与设备相关的配置。
如果您希望在不同的程序之间共享某些配置,该方法也适用。例如,如果您想要在 bash
和 zsh
中同时启用一些别名,您可以把它们写在 .aliases
里,然后在这两个 shell 里应用:
# Test if ~/.aliases exists and source it
+if [ -f ~/.aliases ]; then
+ source ~/.aliases
+fi
+
对于程序员来说,在他们的日常工作中使用远程服务器已经非常普遍了。如果您需要使用远程服务器来部署后端软件或您需要一些计算能力强大的服务器,您就会用到安全 shell(SSH)。和其他工具一样,SSH 也是可以高度定制的,也值得我们花时间学习它。
通过如下命令,您可以使用 ssh
连接到其他服务器:
ssh foo@bar.mit.edu
+
这里我们尝试以用户名 foo
登录服务器 bar.mit.edu
。服务器可以通过 URL 指定(例如bar.mit.edu
),也可以使用 IP 指定(例如foobar@192.168.1.42
)。后面我们会介绍如何修改 ssh 配置文件使我们可以用类似 ssh bar
这样的命令来登录服务器。
ssh
的一个经常被忽视的特性是它可以直接远程执行命令。 ssh foobar@server ls
可以直接在用 foobar 的命令下执行 ls
命令。 想要配合管道来使用也可以, ssh foobar@server ls | grep PATTERN
会在本地查询远端 ls
的输出而 ls | ssh foobar@server grep PATTERN
会在远端对本地 ls
输出的结果进行查询。
基于密钥的验证机制使用了密码学中的公钥,我们只需要向服务器证明客户端持有对应的私钥,而不需要公开其私钥。这样您就可以避免每次登录都输入密码的麻烦了秘密就可以登录。不过,私钥(通常是 ~/.ssh/id_rsa
或者 ~/.ssh/id_ed25519
) 等效于您的密码,所以一定要好好保存它。
使用 [ssh-keygen](http://man7.org/linux/man-pages/man1/ssh-keygen.1.html)
命令可以生成一对密钥:
ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519
+
您可以为密钥设置密码,防止有人持有您的私钥并使用它访问您的服务器。您可以使用 [ssh-agent](https://www.man7.org/linux/man-pages/man1/ssh-agent.1.html)
或 [gpg-agent](https://linux.die.net/man/1/gpg-agent)
,这样就不需要每次都输入该密码了。
如果您曾经配置过使用 SSH 密钥推送到 GitHub,那么可能您已经完成了这里 (opens new window) 介绍的这些步骤,并且已经有了一个可用的密钥对。要检查您是否持有密码并验证它,您可以运行 ssh-keygen -y -f /path/to/key
.
ssh
会查询 .ssh/authorized_keys
来确认那些用户可以被允许登录。您可以通过下面的命令将一个公钥拷贝到这里:
cat .ssh/id_ed25519.pub | ssh foobar@remote 'cat >> ~/.ssh/authorized_keys'
+
+# 如果支持 ssh-copy-id 的话,可以使用下面这种更简单的解决方案:
+ssh-copy-id -i .ssh/id_ed25519.pub foobar@remote
+
在 GitHub(或其他类似的服务)上使用 SSH 密钥时,邮箱地址作为一个注释或标识附加到公钥的末尾。这并不是 SSH 密钥本身所必需的,而是为了帮助识别公钥的所有者。当你在多个设备上生成不同的 SSH 密钥并将它们添加到 GitHub 时,邮箱地址可以帮助你区分每个密钥对应的设备或用途。
在使用 ssh-keygen
生成密钥时,通常在命令行最后提示你输入一个文件名来保存新的密钥时,你可以附加一个注释,如你的电子邮箱。例如:
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
+
这里 -C "your_email@example.com"
就是为生成的公钥添加一个标签或注释,通常是你的电子邮件地址。当你将公钥添加到 GitHub 时,这个邮箱地址会一同显示,但它不影响密钥的功能。这个注释在公钥的末尾,不参与密钥认证过程,仅仅是为了方便用户识别。
一个电脑可以拥有多个 SSH 密钥对,而不仅限于一个。SSH 密钥对通常包括两部分:一个私钥和一个公钥。私钥应该保密不外泄,而公钥可以安全地分享给任何人或任何服务。
在同一台电脑上生成多个 SSH 密钥对可以用于不同目的,比如你可能想为不同的服务器或服务使用不同的密钥对,或者你可能需要不同类型或不同加密强度的密钥对。生成不同的密钥对可以提高安全性,因为即使一个密钥对被破解或泄露,其他密钥对仍然是安全的。
使用 ssh-keygen
命令生成密钥对时,如果不指定输出文件(使用 -f
选项),默认会在 ~/.ssh
目录下生成名为 id_rsa
的私钥和名为 id_rsa.pub
的公钥。如果该文件已经存在,ssh-keygen
会提示是否覆盖。为了生成多个密钥对,你可以指定不同的文件名来保存新的密钥对,例如:
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_github
+ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_gitlab
+
这样,你就可以为不同的服务创建专用的密钥对。在连接时,你可以在 SSH 客户端配置文件(~/.ssh/config
)中指定使用哪个密钥对,或者在使用 ssh
命令时通过 -i
选项指定私钥的路径。
记得,每个生成的公钥(例如 id_rsa_github.pub
和 id_ed25519_gitlab.pub
)可以上传到相应的服务上(如 GitHub 或 GitLab),而对应的私钥则应该安全地存储在你的设备上,不应该泄露或被无关人员访问。
使用 ssh 复制文件有很多方法:
ssh+tee
, 最简单的方法是执行 ssh
命令,然后通过这样的方法利用标准输入实现 cat localfile | ssh remote_server tee serverfile
。回忆一下,[tee](https://www.man7.org/linux/man-pages/man1/tee.1.html)
命令会将标准输出写入到一个文件;[scp](https://www.man7.org/linux/man-pages/man1/scp.1.html)
:当需要拷贝大量的文件或目录时,使用scp
命令则更加方便,因为它可以方便的遍历相关路径。语法如下:scp path/to/local_file remote_host:path/to/remote_file
;[rsync](https://www.man7.org/linux/man-pages/man1/rsync.1.html)
对 scp
进行了改进,它可以检测本地和远端的文件以防止重复拷贝。它还可以提供一些诸如符号连接、权限管理等精心打磨的功能。甚至还可以基于 -partial
标记实现断点续传。rsync
的语法和scp
类似;LocalForward 9999 localhost:8888
+
LocalForward 9999 localhost:8888
是 SSH 配置文件中的一个指令,用于设置本地端口转发。这一行指令的出现在 SSH 配置文件(通常是 ~/.ssh/config
)中表示以下行为:
localhost:8888
地址和端口上。localhost
是指远程机器的本地回环接口,而不是你的本地机器。所以,实际上数据是从本地机器的 9999 端口转发到远程机器的 8888 端口。在实际应用中,这种端口转发可以用于多种情况,例如:
在你提供的配置中,如果有服务在远程主机的 localhost:8888
上监听,你可以简单地通过访问你本地机器上的 localhost:9999
来访问它,所有通信都会安全地通过 SSH 隧道转发。
使用 ~/.ssh/config
Host vm
+ User foobar
+ HostName 172.16.174.141
+ Port 2222
+ IdentityFile ~/.ssh/id_ed25519
+ LocalForward 9999 localhost:8888
+
+# 在配置文件中也可以使用通配符
+Host *.mit.edu
+ User foobaz
+
这么做的好处是,使用 ~/.ssh/config
文件来创建别名,类似 scp
、rsync
和mosh
的这些命令都可以读取这个配置并将设置转换为对应的命令行选项。
注意,~/.ssh/config
文件也可以被当作配置文件,而且一般情况下也是可以被导入其他配置文件的。不过,如果您将其公开到互联网上,那么其他人都将会看到您的服务器地址、用户名、开放端口等等。这些信息可能会帮助到那些企图攻击您系统的黑客,所以请务必三思。
服务器侧的配置通常放在 /etc/ssh/sshd_config
。您可以在这里配置免密认证、修改 ssh 端口、开启 X11 转发等等。 您也可以为每个用户单独指定配置。
我们可以使用类似 ps aux | grep
这样的命令来获取任务的 pid ,然后您可以基于 pid 来结束这些进程。但我们其实有更好的方法来做这件事。在终端中执行 sleep 10000
这个任务。然后用 Ctrl-Z
将其切换到后台并使用 bg
来继续允许它。现在,使用 [pgrep](https://www.man7.org/linux/man-pages/man1/pgrep.1.html)
来查找 pid 并使用 [pkill](https://www.man7.org/linux/man-pages/man1/pgrep.1.html)
结束进程而不需要手动输入 pid。(提示:: 使用 -af
标记)。
如果您希望某个进程结束后再开始另外一个进程, 应该如何实现呢?在这个练习中,我们使用 sleep 60 &
作为先执行的程序。一种方法是使用 [wait](http://man7.org/linux/man-pages/man1/wait.1p.html)
命令。尝试启动这个休眠命令,然后待其结束后再执行 ls
命令。
但是,如果我们在不同的 bash 会话中进行操作,则上述方法就不起作用了。因为 wait
只能对子进程起作用。之前我们没有提过的一个特性是,kill
命令成功退出时其状态码为 0 ,其他状态则是非 0。kill -0
则不会发送信号,但是会在进程不存在时返回一个不为 0 的状态码。请编写一个 bash 函数 pidwait
,它接受一个 pid 作为输入参数,然后一直等待直到该进程结束。您需要使用 sleep
来避免浪费 CPU 性能。
#! /usr/bin/env zsh
+pidwait(){
+ # here while loop will keep checking if the process is still running
+ # if the process is still running, it will sleep for 1 second and then check again
+ # if the process is not running, it will list the files in the current directory
+ # kill -0 returns 0 if the process is running, 1 if the process is not running
+ # while 0 is returned, the loop will keep running
+ while kill -0 $1; do
+ sleep 1
+ done
+ ls
+}
+
#!/usr/bin/env zsh
+
+# copy.sh
+
+# copy all dotfiles to ./dotfiles
+# mkdir ./dotfiles if it doesn't exist
+if [ ! -d "./dotfiles" ]; then
+ mkdir ./dotfiles
+fi
+cp ~/.zshrc ./dotfiles
+cp ~/.vimrc ./dotfiles
+
+echo "Copied dotfiles to ./dotfiles"
+ls -lah ./dotfiles
+
#!/bin/bash
+
+# autoconfig.sh
+
+files=$(ls -a $1 | grep -E '.[^.]+' |grep -v .git)
+# 去掉 ls -a 返回结果中的 ". .. .git"
+for file in `echo $files`; do
+ ln -s $1/$file ~/$file # 创建软链接
+done
+
版本控制系统 (VCSs) 是一类用于追踪源代码(或其他文件、文件夹)改动的工具。顾名思义,这些工具可以帮助我们管理代码的修改历史;不仅如此,它还可以让协作编码变得更方便。VCS 通过一系列的快照将某个文件夹及其内容保存了起来,每个快照都包含了文件或文件夹的完整状态。同时它还维护了快照创建者的信息以及每个快照的相关信息等等。
为什么说版本控制系统非常有用?即使您只是一个人进行编程工作,它也可以帮您创建项目的快照,记录每个改动的目的、基于多分支并行开发等等。和别人协作开发时,它更是一个无价之宝,您可以看到别人对代码进行的修改,同时解决由于并行开发引起的冲突。
现代的版本控制系统可以帮助您轻松地(甚至自动地)回答以下问题:
尽管版本控制系统有很多, 其事实上的标准则是 Git 。
Git 将顶级目录中的文件和文件夹作为集合,并通过一系列快照来管理其历史记录。在 Git 的术语里,文件被称作 Blob 对象(数据对象),也就是一组数据。目录则被称之为“树”,它将名字与 Blob 对象或树对象进行映射(使得目录中可以包含其他目录)。快照则是被追踪的最顶层的树。例如,一个树看起来可能是这样的:
<root> (tree)
+|
++- foo (tree)
+| |
+| + bar.txt (blob, contents = "hello world")
+|
++- baz.txt (blob, contents = "git is wonderful")
+
这个顶层的树包含了两个元素,一个名为 “foo” 的树(它本身包含了一个 blob 对象 “bar.txt”),以及一个 blob 对象 “baz.txt”。
版本控制系统和快照有什么关系呢?线性历史记录是一种最简单的模型,它包含了一组按照时间顺序线性排列的快照。不过出于种种原因,Git 并没有采用这样的模型。
在 Git 中,历史记录是一个由快照组成的有向无环图。有向无环图,听上去似乎是什么高大上的数学名词。不过不要怕,您只需要知道这代表 Git 中的每个快照都有一系列的“父辈”,也就是其之前的一系列快照。注意,快照具有多个“父辈”而非一个,因为某个快照可能由多个父辈而来。例如,经过合并后的两条分支。
在 Git 中,这些快照被称为“提交”。通过可视化的方式来表示这些历史提交记录时,看起来差不多是这样的:
o <-- o <-- o <-- o
+ ^
+ \
+ --- o <-- o
+
上面是一个 ASCII 码构成的简图,其中的 o
表示一次提交(快照)。
箭头指向了当前提交的父辈(这是一种“在…之前”,而不是“在…之后”的关系)。在第三次提交之后,历史记录分岔成了两条独立的分支。这可能因为此时需要同时开发两个不同的特性,它们之间是相互独立的。开发完成后,这些分支可能会被合并并创建一个新的提交,这个新的提交会同时包含这些特性。新的提交会创建一个新的历史记录,看上去像这样(最新的合并提交用粗体标记):
o <-- o <-- o <-- o <---- o
+ ^ /
+ \ v
+ --- o <-- o
+
Git 中的提交是不可改变的。但这并不代表错误不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。而引用(参见下文)则被更新为指向这些新的提交。
以伪代码的形式来学习 Git 的数据模型,可能更加清晰:
// 文件就是一组数据
+type blob = array<byte>
+
+// 一个包含文件和目录的目录
+type tree = map<string, tree | blob>
+
+// 每个提交都包含一个父辈,元数据和顶层树
+type commit = struct {
+ parent: array<commit>
+ author: string
+ message: string
+ snapshot: tree
+}
+
这是一种简洁的历史模型。
Git 中的对象可以是 blob、树或提交:
type object = blob | tree | commit
+
Git 在储存数据时,所有的对象都会基于它们的 SHA-1 哈希 (opens new window) 进行寻址。
objects = map<string, object>
+
+def store(object):
+ id = sha1(object)
+ objects[id] = object
+
+def load(id):
+ return objects[id]
+
Blobs、树和提交都一样,它们都是对象。当它们引用其他对象时,它们并没有真正的在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。
例如,上面 (opens new window)例子中的树(可以通过 git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d
来进行可视化),看上去是这样的:
100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt
+040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo
+
树本身会包含一些指向其他内容的指针,例如 baz.txt
(blob) 和 foo
(树)。如果我们用 git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85
,即通过哈希值查看 baz.txt 的内容,会得到以下信息:
git is wonderful
+
现在,所有的快照都可以通过它们的 SHA-1 哈希值来标记了。但这也太不方便了,谁也记不住一串 40 位的十六进制字符。
针对这一问题,Git 的解决方法是给这些哈希值赋予人类可读的名字,也就是引用(references)。引用是指向提交的指针。与对象不同的是,它是可变的(引用可以被更新,指向新的提交)。例如,master
引用通常会指向主分支的最新一次提交。
references = map<string, string>
+
+def update_reference(name, id):
+ references[name] = id
+
+def read_reference(name):
+ return references[name]
+
+def load_reference(name_or_id):
+ if name_or_id in references:
+ return load(references[name_or_id])
+ else:
+ return load(name_or_id)
+
这样,Git 就可以使用诸如 “master” 这样人类可读的名称来表示历史记录中某个特定的提交,而不需要在使用一长串十六进制字符了。
有一个细节需要我们注意, 通常情况下,我们会想要知道“我们当前所在位置”,并将其标记下来。这样当我们创建新的快照的时候,我们就可以知道它的相对位置(如何设置它的“父辈”)。在 Git 中,我们当前的位置有一个特殊的索引,它就是 “HEAD”。
最后,我们可以粗略地给出 Git 仓库的定义了:对象
和 引用
。
在硬盘上,Git 仅存储对象和引用:因为其数据模型仅包含这些东西。所有的 git
命令都对应着对提交树的操作,例如增加对象,增加或删除引用。
当您输入某个指令时,请思考一下这条命令是如何对底层的图数据结构进行操作的。另一方面,如果您希望修改提交树,例如“丢弃未提交的修改和将 ‘master’ 引用指向提交 5d83f9e
时,有什么命令可以完成该操作(针对这个具体问题,您可以使用 git checkout master; git reset --hard 5d83f9e
)
Git 中还包括一个和数据模型完全不相关的概念,但它确是创建提交的接口的一部分。
就上面介绍的快照系统来说,您也许会期望它的实现里包括一个 “创建快照” 的命令,该命令能够基于当前工作目录的当前状态创建一个全新的快照。有些版本控制系统确实是这样工作的,但 Git 不是。我们希望简洁的快照,而且每次从当前状态创建快照可能效果并不理想。例如,考虑如下场景,您开发了两个独立的特性,然后您希望创建两个独立的提交,其中第一个提交仅包含第一个特性,而第二个提交仅包含第二个特性。或者,假设您在调试代码时添加了很多打印语句,然后您仅仅希望提交和修复 bug 相关的代码而丢弃所有的打印语句。
Git 处理这些场景的方法是使用一种叫做 “暂存区(staging area)”的机制,它允许您指定下次快照中要包括那些改动。
git help <command>
: 获取 git 命令的帮助信息git init
: 创建一个新的 git 仓库,其数据会存放在一个名为 .git
的目录下git status
: 显示当前的仓库状态git add <filename>
: 添加文件到暂存区git commit
: 创建一个新的提交
+git log
: 显示历史日志git log --all --graph --decorate
: 可视化历史记录(有向无环图) --oneline
一行git diff <filename>
: 显示与暂存区文件的差异git diff <revision> <filename>
: 显示某个文件两个版本之间的差异git checkout <revision>
: 更新 HEAD 和目前的分支git checkout <filename>
: 把文件回到之前(HEAD)版本git branch
: 显示分支git branch <name>
: 创建分支git checkout -b <name>
: 创建分支并切换到该分支
+git branch <name>; git checkout <name>
git merge <revision>
: 合并到当前分支git mergetool
: 使用工具来处理合并冲突git rebase
: 将一系列补丁变基(rebase)为新的基线git remote
: 列出远端git remote add <name> <url>
: 添加一个远端git push <remote> <local branch>:<remote branch>
: 将对象传送至远端并更新远端引用git branch --set-upstream-to=<remote>/<remote branch>
: 创建本地和远端分支的关联关系git fetch
: 从远端获取对象/索引git pull
: 相当于 git fetch; git merge
git clone
: 从远端下载仓库git commit --amend
: 编辑提交的内容或信息git reset HEAD <file>
: 恢复暂存的文件git checkout -- <file>
: 丢弃修改git restore
: git2.32 版本后取代 git reset 进行许多撤销操作git config
: Git 是一个 高度可定制的 (opens new window) 工具git clone --depth=1
: 浅克隆(shallow clone),不包括完整的版本历史信息git add -p
: 交互式暂存git rebase -i
: 交互式变基git blame
: 查看最后修改某行的人git stash
: 暂时移除工作目录下的修改内容git bisect
: 通过二分查找搜索历史记录.gitignore
: 指定 (opens new window) 故意不追踪的文件将版本历史可视化并进行探索
是谁最后修改了 README.md
文件?(提示:使用 git log
命令并添加合适的参数)
最后一次修改_config.yml
文件中 collections:
行时的提交信息是什么?(提示:使用 git blame
和 git show
)
使用 Git 时的一个常见错误是提交本不应该由 Git 管理的大文件,或是将含有敏感信息的文件提交给 Git 。尝试向仓库中添加一个文件并添加提交信息,然后将其从历史中删除 ( 这篇文章也许会有帮助 (opens new window));
从历史记录中删除文件
要从历史记录中彻底删除 secret.txt
文件,你可以使用 git filter-branch
命令或更新的 git filter-repo
。git filter-repo
是 git filter-branch
的替代品,效率更高但需要单独安装。这里我将展示使用 git filter-branch
的方法,因为它不需要额外安装。
使用 git filter-branch
删除文件:
git filter-branch --force --index-filter \
+"git rm --cached --ignore-unmatch secret.txt" \
+--prune-empty --tag-name-filter cat -- --all
+
-force
: 强制运行,覆盖备份。-index-filter
: 对每个修订版本使用的过滤器,这里用于删除 secret.txt
。git rm --cached --ignore-unmatch secret.txt
: 删除指定的文件,即使它在某些修订版本中不存在。-prune-empty
: 删除因为文件删除而变成空的提交。-tag-name-filter cat
: 保留标签名称不变。-all
: 应用于所有分支和标签。推送更改到远程仓库:
删除文件后,你需要强制推送更改到远程仓库,因为这是一次重写历史的操作:
git push origin --force --all
+
这会强制更新所有分支到远程仓库。
清理和回收空间:
在本地,运行以下命令来清理 Git 对象并回收空间:
git for-each-ref --format="delete %(refname)" refs/original | git update-ref --stdin
+git reflog expire --expire=now --all
+git gc --prune=now
+
从 GitHub 上克隆某个仓库,修改一些文件。当您使用 git stash
会发生什么?当您执行 git log --all --oneline
时会显示什么?通过 git stash pop
命令来撤销 git stash
操作,什么时候会用到这一技巧?
执行git stash
后,添加到暂存区的内容不会再提示需要提交(Changes to be committed)。而且,尽管执行git stash
使得提交记录新增了两项,但是可以发现HEAD
引用并没有变动。
当我们将改动添加到暂存区(git add
)后,通过执行git stash
后,可以自由地切换到其他分支(注意:在暂存区存有改动时,切换分支(checkout
)是不被允许的)
另外,运用stash
和stash pop
,可以自由选择 stash 存储的改动 即将提交到的分支
与其他的命令行工具一样,Git 也提供了一个名为 ~/.gitconfig 配置文件 (或 dotfile)。请在 ~/.gitconfig 中创建一个别名,使您在运行 git graph 时,您可以得到 git log –all –graph –decorate –oneline 的输出结果;
[alias]
+ graph = log --all --graph --decorate --oneline
+
您可以通过执行 git config –global core.excludesfile ~/.gitignore_global 在 ~/.gitignore_global 中创建全局忽略规则。配置您的全局 gitignore 文件来自动忽略系统或编辑器的临时文件,例如 .DS_Store;
git config --global core.excludesfile ~/.gitignore .DS_Store
+
+ 2024.03.05-二叉树 + + → +
/*
+ * @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
)的原因是在每一轮搜索中,我们需要更新当前层次的节点。由于在遍历当前层次的节点时不能直接修改正在遍历的集合(这会影响迭代器的有效性),因此我们先将新发现的节点存储在一个临时集合中。在当前层次的所有节点都遍历完毕后,我们再用这个临时集合来更新主集合,为下一轮搜索做准备。/*
+ * @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=144 lang=cpp
+ *
+ * [144] 二叉树的前序遍历
+ *
+ * https://leetcode.cn/problems/binary-tree-preorder-traversal/description/
+ *
+ * algorithms
+ * Easy (71.71%)
+ * Likes: 1230
+ * Dislikes: 0
+ * Total Accepted: 1M
+ * Total Submissions: 1.4M
+ * Testcase Example: '[1,null,2,3]'
+ *
+ * 给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
+ *
+ *
+ *
+ * 示例 1:
+ *
+ *
+ * 输入:root = [1,null,2,3]
+ * 输出:[1,2,3]
+ *
+ *
+ * 示例 2:
+ *
+ *
+ * 输入:root = []
+ * 输出:[]
+ *
+ *
+ * 示例 3:
+ *
+ *
+ * 输入:root = [1]
+ * 输出:[1]
+ *
+ *
+ * 示例 4:
+ *
+ *
+ * 输入:root = [1,2]
+ * 输出:[1,2]
+ *
+ *
+ * 示例 5:
+ *
+ *
+ * 输入:root = [1,null,2]
+ * 输出:[1,2]
+ *
+ *
+ *
+ *
+ * 提示:
+ *
+ *
+ * 树中节点数目在范围 [0, 100] 内
+ * -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:
+ vector<int> res;
+ vector<int> preorderTraversal(TreeNode* root) {
+ if(root == nullptr){
+ return res;
+ }
+ res.push_back(root->val);
+ preorderTraversal(root->left);
+ preorderTraversal(root->right);
+ return res;
+ }
+};
+
class Solution {
+public:
+ vector<int> preorderTraversal(TreeNode* root) {
+ vector<int> res;
+ if (root == nullptr) {
+ return res;
+ }
+ stack<TreeNode*> stk;
+ stk.push(root);
+ while (!stk.empty()) {
+ TreeNode* node = stk.top();
+ stk.pop();
+ res.push_back(node->val);
+ if (node->right != nullptr) {
+ stk.push(node->right); // 先压入右子树
+ }
+ if (node->left != nullptr) {
+ stk.push(node->left); // 再压入左子树
+ }
+ }
+ return res;
+ }
+};
+
+ ← + + 2024.03.05-104.二叉树的最大深度 + + 2024.03.05-543.二叉树的直径 + + → +
/*
+ * @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
+
/*
+ * @lc app=leetcode.cn id=34 lang=cpp
+ *
+ * [34] 在排序数组中查找元素的第一个和最后一个位置
+ *
+ * https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/description/
+ *
+ * algorithms
+ * Medium (43.05%)
+ * Likes: 2636
+ * Dislikes: 0
+ * Total Accepted: 936.9K
+ * Total Submissions: 2.2M
+ * Testcase Example: '[5,7,7,8,8,10]\n8'
+ *
+ * 给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
+ *
+ * 如果数组中不存在目标值 target,返回 [-1, -1]。
+ *
+ * 你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
+ *
+ *
+ *
+ * 示例 1:
+ *
+ *
+ * 输入:nums = [5,7,7,8,8,10], target = 8
+ * 输出:[3,4]
+ *
+ * 示例 2:
+ *
+ *
+ * 输入:nums = [5,7,7,8,8,10], target = 6
+ * 输出:[-1,-1]
+ *
+ * 示例 3:
+ *
+ *
+ * 输入:nums = [], target = 0
+ * 输出:[-1,-1]
+ *
+ *
+ *
+ * 提示:
+ *
+ *
+ * 0 <= nums.length <= 10^5
+ * -10^9 <= nums[i] <= 10^9
+ * nums 是一个非递减数组
+ * -10^9 <= target <= 10^9
+ *
+ *
+ */
+
+// @lc code=start
+class Solution
+{
+public:
+ vector<int> searchRange(vector<int> &nums, int target)
+ {
+ int left = leftRange(nums, target);
+ int right = rightRange(nums, target);
+ vector<int> res = {left, right};
+ return res;
+ }
+ int leftRange(vector<int> &nums, int target)
+ {
+ int left = 0, right = nums.size() - 1, res = -1;
+ while (left <= right)
+ {
+ int mid = left + (right - left) / 2;
+ if (nums[mid] >= target)
+ {
+ if (nums[mid] == target)
+ res = mid;
+ right = mid - 1;
+ }
+ else
+ {
+ left = mid + 1;
+ }
+ }
+ return res;
+ }
+ int rightRange(vector<int> &nums, int target)
+ {
+ int left = 0, right = nums.size() - 1, res = -1;
+ while (left <= right)
+ {
+ int mid = left + (right - left) / 2;
+ if (nums[mid] <= target)
+ {
+ if (nums[mid] == target)
+ res = mid;
+ left = mid + 1;
+ }
+ else
+ {
+ right = mid - 1;
+ }
+ }
+ return res;
+ }
+};
+
+ 2024.03.11-704.二分搜索 + + → +
/*
+ * @lc app=leetcode.cn id=704 lang=cpp
+ *
+ * [704] 二分查找
+ *
+ * https://leetcode.cn/problems/binary-search/description/
+ *
+ * algorithms
+ * Easy (55.07%)
+ * Likes: 1541
+ * Dislikes: 0
+ * Total Accepted: 1.2M
+ * Total Submissions: 2.1M
+ * Testcase Example: '[-1,0,3,5,9,12]\n9'
+ *
+ * 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的
+ * target,如果目标值存在返回下标,否则返回 -1。
+ *
+ *
+ * 示例 1:
+ *
+ * 输入: nums = [-1,0,3,5,9,12], target = 9
+ * 输出: 4
+ * 解释: 9 出现在 nums 中并且下标为 4
+ *
+ *
+ * 示例 2:
+ *
+ * 输入: nums = [-1,0,3,5,9,12], target = 2
+ * 输出: -1
+ * 解释: 2 不存在 nums 中因此返回 -1
+ *
+ *
+ *
+ *
+ * 提示:
+ *
+ *
+ * 你可以假设 nums 中的所有元素是不重复的。
+ * n 将在 [1, 10000]之间。
+ * nums 的每个元素都将在 [-9999, 9999]之间。
+ *
+ *
+ */
+
+// @lc code=start
+
+class Solution {
+public:
+ int search(vector<int>& nums, int target) {
+ int left = 0;
+ int right = nums.size() - 1;
+ while(left <= right){
+ int mid = left + (right - left) / 2;
+ if(nums[mid] == target)
+ return mid;
+ else if(nums[mid] < target){
+ left = mid + 1;
+ }else{
+ right = mid - 1;
+ }
+ }
+ return -1;
+ }
+};
+// @lc code=end
+
int right = nums.size() - 1
;left ≤ right
,有个等号。即当while中那个区间为空的时候就该跳出while了left + (right - left) / 2
的目的是避免溢出,而不是直接 left + right / 2
left = mid + 1
; 由于闭区间,所以更新边界的时候要考虑到不能包含原来的边界值/*
+ * @lc app=leetcode.cn id=76 lang=cpp
+ *
+ * [76] 最小覆盖子串
+ *
+ * https://leetcode.cn/problems/minimum-window-substring/description/
+ *
+ * algorithms
+ * Hard (45.57%)
+ * Likes: 2847
+ * Dislikes: 0
+ * Total Accepted: 534.5K
+ * Total Submissions: 1.2M
+ * Testcase Example: '"ADOBECODEBANC"\n"ABC"'
+ *
+ * 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""
+ * 。
+ *
+ *
+ *
+ * 注意:
+ *
+ *
+ * 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
+ * 如果 s 中存在这样的子串,我们保证它是唯一的答案。
+ *
+ *
+ *
+ *
+ * 示例 1:
+ *
+ *
+ * 输入:s = "ADOBECODEBANC", t = "ABC"
+ * 输出:"BANC"
+ * 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
+ *
+ *
+ * 示例 2:
+ *
+ *
+ * 输入:s = "a", t = "a"
+ * 输出:"a"
+ * 解释:整个字符串 s 是最小覆盖子串。
+ *
+ *
+ * 示例 3:
+ *
+ *
+ * 输入: s = "a", t = "aa"
+ * 输出: ""
+ * 解释: t 中两个字符 'a' 均应包含在 s 的子串中,
+ * 因此没有符合条件的子字符串,返回空字符串。
+ *
+ *
+ *
+ * 提示:
+ *
+ *
+ * ^m == s.length
+ * ^n == t.length
+ * 1 <= m, n <= 10^5
+ * s 和 t 由英文字母组成
+ *
+ *
+ *
+ * 进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗?
+ */
+
+// @lc code=start
+class Solution
+{
+public:
+ string minWindow(string s, string t)
+ {
+ // 左闭右开区间 [left, right)
+ int left = 0, right = 0;
+ unordered_map<char, int> target, window;
+ for (char c : t)
+ target[c]++;
+ int hit = 0, resStart = 0, resLen = INT_MAX;
+ while (right < s.size())
+ {
+ right++;
+ char in = s[right - 1];
+ if (target.count(in))
+ {
+ window[in]++;
+ if (window[in] == target[in])
+ hit += 1;
+ }
+ while (hit == target.size())
+ {
+ left++;
+ if ((right - left + 1) < resLen)
+ {
+ resStart = left - 1;
+ resLen = right - left + 1;
+ }
+ char out = s[left - 1];
+ if (target.count(out)){
+ if (window[out] == target[out])
+ hit -= 1;
+ window[out]--;
+ }
+ }
+ }
+ return resLen == INT_MAX ? "" : s.substr(resStart, resLen);
+ }
+};
+// @lc code=end
+
/*
+ * @lc app=leetcode.cn id=3 lang=cpp
+ *
+ * [3] 无重复字符的最长子串
+ *
+ * https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/
+ *
+ * algorithms
+ * Medium (39.02%)
+ * Likes: 8247
+ * Dislikes: 0
+ * Total Accepted: 2M
+ * Total Submissions: 5.2M
+ * Testcase Example: '"abcabcbb"'
+ *
+ * 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
+ *
+ *
+ *
+ * 示例 1:
+ *
+ *
+ * 输入: s = "abcabcbb"
+ * 输出: 3
+ * 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
+ *
+ *
+ * 示例 2:
+ *
+ *
+ * 输入: s = "bbbbb"
+ * 输出: 1
+ * 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
+ *
+ *
+ * 示例 3:
+ *
+ *
+ * 输入: s = "pwwkew"
+ * 输出: 3
+ * 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
+ * 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
+ *
+ *
+ *
+ *
+ * 提示:
+ *
+ *
+ * 0 <= s.length <= 5 * 10^4
+ * s 由英文字母、数字、符号和空格组成
+ *
+ *
+ */
+
+// @lc code=start
+// class Solution
+// {
+// public:
+// int lengthOfLongestSubstring(string s)
+// {
+// int result = 0;
+// unordered_map<char, int> num;
+// int l = 0;
+// for (int r = 0; r < s.length(); ++r)
+// {
+// num[s[r]]++;
+// while (num[s[r]] >= 2)
+// {
+// num[s[l++]]--;
+// }
+// result = max(r - l + 1, result);
+// }
+// return result;
+// }
+// };
+class Solution
+{
+public:
+ int lengthOfLongestSubstring(string s)
+ {
+ unordered_map<char, int> window;
+ int left = 0, right = 0;
+ int res = 0;
+ while (right < s.size())
+ {
+ char in = s[right];
+ right++;
+ window[in]++;
+ while (window[in] > 1)
+ {
+ char out = s[left];
+ left++;
+ window[out]--;
+ }
+ res = max(res, right - left);
+ }
+ return res;
+ }
+};
+// @lc code=end
+
/*
+ * @lc app=leetcode.cn id=438 lang=cpp
+ *
+ * [438] 找到字符串中所有字母异位词
+ *
+ * https://leetcode.cn/problems/find-all-anagrams-in-a-string/description/
+ *
+ * algorithms
+ * Medium (53.57%)
+ * Likes: 1413
+ * Dislikes: 0
+ * Total Accepted: 407.9K
+ * Total Submissions: 761.5K
+ * Testcase Example: '"cbaebabacd"\n"abc"'
+ *
+ * 给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
+ *
+ * 异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
+ *
+ *
+ *
+ * 示例 1:
+ *
+ *
+ * 输入: s = "cbaebabacd", p = "abc"
+ * 输出: [0,6]
+ * 解释:
+ * 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
+ * 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
+ *
+ *
+ * 示例 2:
+ *
+ *
+ * 输入: s = "abab", p = "ab"
+ * 输出: [0,1,2]
+ * 解释:
+ * 起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
+ * 起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
+ * 起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
+ *
+ *
+ *
+ *
+ * 提示:
+ *
+ *
+ * 1 <= s.length, p.length <= 3 * 10^4
+ * s 和 p 仅包含小写字母
+ *
+ *
+ */
+
+// @lc code=start
+class Solution {
+public:
+ vector<int> findAnagrams(string s, string p) {
+ unordered_map<char, int> target, window;
+ for (char c : p)
+ target[c]++;
+ int left = 0, right = 0;
+ int hit = 0;
+ vector<int> res;
+ while (right < s.size())
+ {
+ char in = s[right];
+ right++;
+ if (target.count(in))
+ {
+ window[in]++;
+ if (window[in] == target[in])
+ hit++;
+ }
+ while (hit == target.size())
+ {
+ if ((right - left) == p.size())
+ res.push_back(left);
+ char out = s[left];
+ left++;
+ if (target.count(out))
+ {
+ if (window[out] == target[out])
+ hit--;
+ window[out]--;
+ }
+ }
+ }
+ return res;
+ }
+};
+// @lc code=end
+
+ ← + + 2024.04.02-3.无重复字符的最长子串 + + 2024.04.02-567.字符串的排列 + + → +
/*
+ * @lc app=leetcode.cn id=567 lang=cpp
+ *
+ * [567] 字符串的排列
+ *
+ * https://leetcode.cn/problems/permutation-in-string/description/
+ *
+ * algorithms
+ * Medium (44.96%)
+ * Likes: 996
+ * Dislikes: 0
+ * Total Accepted: 286.4K
+ * Total Submissions: 637.1K
+ * Testcase Example: '"ab"\n"eidbaooo"'
+ *
+ * 给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。
+ *
+ * 换句话说,s1 的排列之一是 s2 的 子串 。
+ *
+ *
+ *
+ * 示例 1:
+ *
+ *
+ * 输入:s1 = "ab" s2 = "eidbaooo"
+ * 输出:true
+ * 解释:s2 包含 s1 的排列之一 ("ba").
+ *
+ *
+ * 示例 2:
+ *
+ *
+ * 输入:s1= "ab" s2 = "eidboaoo"
+ * 输出:false
+ *
+ *
+ *
+ *
+ * 提示:
+ *
+ *
+ * 1 <= s1.length, s2.length <= 10^4
+ * s1 和 s2 仅包含小写字母
+ *
+ *
+ */
+
+// @lc code=start
+class Solution
+{
+public:
+ bool checkInclusion(string s1, string s2)
+ {
+ unordered_map<char, int> target, window;
+ for (char c : s1)
+ target[c]++;
+ int left = 0, right = 0;
+ int hit = 0;
+ while (right < s2.size())
+ {
+ char in = s2[right];
+ right++;
+ if (target.count(in))
+ {
+ window[in]++;
+ if (window[in] == target[in])
+ hit++;
+ }
+ while (hit == target.size())
+ {
+ if ((right - left) == s1.size())
+ return true;
+ char out = s2[left];
+ left++;
+ if (target.count(out))
+ {
+ if (window[out] == target[out])
+ hit--;
+ window[out]--;
+ }
+ }
+ }
+ return false;
+ }
+};
+// @lc code=end
+
ssh <user_id>@162.105.133.209
+
搭建ubuntu
singularity build --fakeroot --sandbox Geant4 docker://ubuntu:22.04
+
进入Geant4环境(sandbox)
singularity shell --fakeroot -w Geant4
+
安装基本环境
apt-get install build-essential
+apt-get install wget
+
下载geant4
mkdir geant4
+wget https://geant4-data.web.cern.ch/releases/geant4-v11.1.0.tar.gz
+tar -xzvf geant4-v11.1.0.tar.gz
+
安装依赖包(若报错,则分别单独安装)
sudo apt-get install -y cmake libx11-dev libxext-dev libxtst-dev libxrender-dev libxmu-dev libxmuu-dev libhdf5-serial-dev hdf5-tools
+sudo apt-get install -y libexpat1-dev
+sudo apt install -y qt5*
+
编译安装
mkdir build
+cd build
+cmake -DCMAKE_INSTALL_PREFIX=/root/Geant4/home/geant4 /root/Geant4/home/geant4-v11.1.0
+cmake -DGEANT4_INSTALL_DATA=ON .
+make -jN # N表示处理器数量,需修改为实际值
+make install
+
配置环境变量
cd /root/Geant4/etc/skel/
+vim .bashrc
+
将下列内容添加到 .bashrc
source /root/Geant4/home/geant4/bin/geant4.sh
+source /root/Geant4/home/geant4/share/Geant4/geant4make/geant4make.sh
+
配置
cp /root/Geant4/etc/skel/.bashrc /root/Geant4/home
+cd /root/Geant4/home
+source .bashrc
+
检验geant4是否配置成功
cd /root/Geant4/home/geant4/share/Geant4/examples/basic/B1
+mkdir build && cd build
+cmake ..
+make
+./exampleB1 run1.mac
+
若无报错则配置成功
退出
exit
+
singularity build --fakeroot s-Geant4.sif Geant4/
+
# 超算Data节点
+export VERSION=1.17.2 OS=linux ARCH=amd64
+wget https://dl.google.com/go/go$VERSION.$OS-$ARCH.tar.gz
+tar -xzvf go1.17.2.linux-amd64.tar.gz
+rm -f go1.17.2.linux-amd64.tar.gz
+export PATH=/lustre/home/<user_id>/go/bin:$PATH
+
module load singularity/3.11.3
+
cd $HOME
+vim .bashrc
+# 将下列语句添加到.bashrc中
+# export PATH=/lustre/home/<user_id>/go/bin:$PATH
+source .bashrc
+
scp -r <user_id>@162.105.133.209:/home/<user_id>/s-Geant4.sif /gpfs/share/home/<user_id>/
+
singularity build --sandbox Geant4 s-Geant4.sif
+singularity shell -w Geant4
+
cd Geant4/home
+vim .bashrc
+
+# 将下列语句添加到文件末尾
+# source /lustre/home/2201210084/singularity/Geant4/home/geant4/bin/geant4.sh
+# source /lustre/home/2201210084/singularity/Geant4/home/geant4/share/Geant4/geant4make/geant4make.sh
+
+source .bashrc
+
看见如下字样表示成功
# 1. 将SIF格式的容器转换成sandbox;
+singularity build --sandbox XXX XXX.sif
+
+# 2. 将sandbox容器镜像转化成SIF格式;
+singularity build XXX.sif XXX
+
# 假设要删除的为文件夹名为molspin的sandbox镜像
+
+# 首先,以可读的模式进入要删除的镜像
+singularity shell --fakeroot -w molspin
+
+# 删除掉容器中,基于fakeroot创建的所有文件
+rm -rf /* 1>/dev/null 2>&1
+
+# 退出镜像
+exit
+
+# 将创建好的软件镜像上传到高性能计算集群,加载singularity软件环境
+# 删除掉剩下的
+rm -rf molspin
+
import copy
+import os
+import sys
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+
+# Path of the file to read
+path = "../output_below.csv"
+# open the csv file
+df = pd.read_csv(path)
+
+# columns of the csv file
+particleName = df.iloc[:, 0].values.tolist()
+eventID = df.iloc[:, 1].values.tolist()
+trackID = df.iloc[:, 2].values.tolist()
+preVolumeName = df.iloc[:, 3].values.tolist()
+preX = df.iloc[:, 4].values.tolist()
+preY = df.iloc[:, 5].values.tolist()
+preZ = df.iloc[:, 6].values.tolist()
+preKE = df.iloc[:, 7].values.tolist()
+preProcessName = df.iloc[:, 8].values.tolist()
+postProcessName = df.iloc[:, 9].values.tolist()
+postVolumeName = df.iloc[:, 10].values.tolist()
+postX = df.iloc[:, 11].values.tolist()
+postY = df.iloc[:, 12].values.tolist()
+postZ = df.iloc[:, 13].values.tolist()
+postKE = df.iloc[:, 14].values.tolist()
+depositEnergy = df.iloc[:, 15].values.tolist()
+stepLength = df.iloc[:, 16].values.tolist()
+trackLength = df.iloc[:, 17].values.tolist()
+
+# get the energy density
+unit = 'MeV*cm^2/g'
+densitySiC = 3.217 * 1e-3import os
+import sys
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+
+# Path of the file to read
+path = "../output_below.csv"
+
+# open the csv file
+df = pd.read_csv(path)
+
+# columns of the csv file
+particleName = df.iloc[:, 0].values.tolist()
+eventID = df.iloc[:, 1].values.tolist()
+trackID = df.iloc[:, 2].values.tolist()
+preVolumeName = df.iloc[:, 3].values.tolist()
+preX = df.iloc[:, 4].values.tolist()
+preY = df.iloc[:, 5].values.tolist()
+preZ = df.iloc[:, 6].values.tolist()
+preKE = df.iloc[:, 7].values.tolist()
+preProcessName = df.iloc[:, 8].values.tolist()
+postProcessName = df.iloc[:, 9].values.tolist()
+postVolumeName = df.iloc[:, 10].values.tolist()
+postX = df.iloc[:, 11].values.tolist()
+postY = df.iloc[:, 12].values.tolist()
+postZ = df.iloc[:, 13].values.tolist()
+postKE = df.iloc[:, 14].values.tolist()
+depositEnergy = df.iloc[:, 15].values.tolist()
+stepLength = df.iloc[:, 16].values.tolist()
+trackLength = df.iloc[:, 17].values.tolist()
+
+# get the energy density
+unit = 'MeV*cm^2/g'
+densitySiC = 3.217 * 1e-3
+densityAl = 2.6989 * 1e-3
+densitySi = 2.3296 * 1e-3
+densitySiO2 = 2.2 * 1e-3
+depositEnergy = np.array(depositEnergy)
+stepLength = np.array(stepLength)
+depositEnergyDensity = depositEnergy / stepLength
+for i in range(len(preVolumeName)):
+ if preVolumeName[i].strip() == 'V1':
+ depositEnergyDensity[i] = depositEnergyDensity[i] / densitySi
+ continue
+ if preVolumeName[i].strip() == 'V2':
+ depositEnergyDensity[i] = depositEnergyDensity[i] / densitySiO2
+ continue
+ if (preVolumeName[i].strip() == 'Vs1') or (preVolumeName[i].strip() == 'Vs2') or (preVolumeName[i].strip() == 'VD'):
+ depositEnergyDensity[i] = depositEnergyDensity[i] / densityAl
+ continue
+ else:
+ depositEnergyDensity[i] = depositEnergyDensity[i] / densitySiC
+depositEnergyDensity = depositEnergyDensity.tolist()
+depositEnergyDensity250000 = []
+preX_250000=[]
+preZ_250000=[]
+for i in range(len(depositEnergyDensity)):
+ if depositEnergyDensity[i] < 250000:
+ depositEnergyDensity250000.append(depositEnergyDensity[i])
+ preX_250000.append(preX[i])
+ preZ_250000.append(preZ[i])
+
+# draw the heatmap according to the coordinates and the energy deposit
+# the energy deposit is the color of the heatmap
+# the coordinates are the x and y axis of the heatmap
+# the z axis is the energy deposit
+# the colorbar is the energy deposit
+# draw the heatmap
+plt.scatter(preX, preZ, c=depositEnergy, s=0.1, cmap='jet',marker='.')
+plt.xlabel("preX")
+plt.ylabel("preZ")
+plt.title("heatmap_total")
+plt.colorbar()
+plt.savefig("heatmap_total.png", dpi=500)
+plt.show()
+
+plt.scatter(preX, preZ, c=depositEnergyDensity, s=0.1, cmap='jet',marker='.')
+plt.xlabel("preX")
+plt.ylabel("preZ")
+plt.title("heatmap_total")
+plt.colorbar()
+plt.savefig("heatmap_density_total.png", dpi=500)
+plt.show()
+
+plt.scatter(preX_250000, preZ_250000, c=depositEnergyDensity250000, s=1, cmap='jet',marker='.')
+plt.xlabel("preX")
+plt.ylabel("preZ")
+plt.title("heatmap_<250000")
+plt.colorbar()
+plt.savefig("heatmap_density_2500000.png", dpi=500)
+plt.show(
+densityAl = 2.6989 * 1e-3
+densitySi = 2.3296 * 1e-3
+densitySiO2 = 2.2 * 1e-3
+depositEnergy = np.array(depositEnergy)
+stepLength = np.array(stepLength)
+depositEnergyDensity = depositEnergy / stepLength
+for i in range(len(preVolumeName)):
+ if preVolumeName[i].strip() == 'V1':
+ depositEnergyDensity[i] = depositEnergyDensity[i] / densitySi
+ continue
+ if preVolumeName[i].strip() == 'V2':
+ depositEnergyDensity[i] = depositEnergyDensity[i] / densitySiO2
+ continue
+ if (preVolumeName[i].strip() == 'Vs1') or (preVolumeName[i].strip() == 'Vs2') or (preVolumeName[i].strip() == 'VD'):
+ depositEnergyDensity[i] = depositEnergyDensity[i] / densityAl
+ continue
+ else:
+ depositEnergyDensity[i] = depositEnergyDensity[i] / densitySiC
+depositEnergyDensity = depositEnergyDensity.tolist()
+depositEnergyDensity250000 = []
+for i in range(len(depositEnergyDensity)):
+ if depositEnergyDensity[i] < 250000:
+ depositEnergyDensity250000.append(depositEnergyDensity[i])
+
+# draw the histogram of the energy deposit
+plt.hist(depositEnergy, bins=50, facecolor="blue",
+ edgecolor="black", alpha=0.7)
+# plt.yscale('log')
+plt.xlabel("depositEnergy(MeV)")
+plt.ylabel("Number")
+plt.title("depositEnergy_total")
+plt.savefig("depositEnergy_total.png", dpi=500)
+plt.show()
+
+# draw the histogram of the energy deposit density
+plt.hist(depositEnergyDensity, bins=50, facecolor="blue",
+ edgecolor="black", alpha=0.7)
+plt.yscale('log')
+plt.xlabel("depositEnergyDensity(MeV*cm^2/g)")
+plt.ylabel("Number")
+plt.title("depositEnergyDensity_total")
+plt.savefig("depositEnergyDensity_total.png", dpi=500)
+plt.show()
+
+# make a dictionary of the columns
+names = [('V1', []), ('V2', []), ('V31', []), ('V32', []), ('V51', []), ('V52', []), ('V66', []), ('VD', []),
+ ('Vs1', []), ('Vs2', []), ('V7', [])]
+dict_dp = dict(names)
+dict_dpd = copy.deepcopy(dict_dp)
+# extract the keys of the dictionary to a list
+volumeNames = list(dict_dp.keys())
+
+# for every column, determine the preVolumeName and fill the dictionary
+for i in range(len(preVolumeName)):
+ for j in volumeNames:
+ if preVolumeName[i].strip() == j:
+ dict_dp[j].append(depositEnergy[i])
+ dict_dpd[j].append(depositEnergyDensity[i])
+ break
+
+# draw the histogram of the energy deposit for each volume and merge in one page
+plt.figure(figsize=(16, 16))
+for i in range(len(volumeNames)):
+ plt.subplot(3, 4, i + 1)
+ plt.hist(dict_dp[volumeNames[i]], bins=50,
+ facecolor="blue", edgecolor="black", alpha=0.7)
+ # plt.yscale('log')
+ plt.xlabel("depositEnergy(MeV)")
+ plt.ylabel("Number")
+ plt.title(volumeNames[i])
+plt.savefig("depositEnergy_each.png", dpi=500)
+plt.show()
+
+# draw the histogram of the energy deposit density for each volume and merge in one page
+plt.figure(figsize=(16, 16))
+for i in range(len(volumeNames)):
+ plt.subplot(3, 4, i + 1)
+ plt.hist(dict_dpd[volumeNames[i]], bins=50,
+ facecolor="blue", edgecolor="black", alpha=0.7)
+ # plt.yscale('log')
+ plt.xlabel("depositEnergyDensity(MeV*cm^2/g)")
+ plt.ylabel("Number")
+ plt.title(volumeNames[i])
+plt.savefig("depositEnergyDensity_each.png", dpi=500)
+plt.show()
+
import os
+import sys
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+
+# Path of the file to read
+path = "../output_below.csv"
+
+# open the csv file
+df = pd.read_csv(path)
+
+# columns of the csv file
+particleName = df.iloc[:, 0].values.tolist()
+eventID = df.iloc[:, 1].values.tolist()
+trackID = df.iloc[:, 2].values.tolist()
+preVolumeName = df.iloc[:, 3].values.tolist()
+preX = df.iloc[:, 4].values.tolist()
+preY = df.iloc[:, 5].values.tolist()
+preZ = df.iloc[:, 6].values.tolist()
+preKE = df.iloc[:, 7].values.tolist()
+preProcessName = df.iloc[:, 8].values.tolist()
+postProcessName = df.iloc[:, 9].values.tolist()
+postVolumeName = df.iloc[:, 10].values.tolist()
+postX = df.iloc[:, 11].values.tolist()
+postY = df.iloc[:, 12].values.tolist()
+postZ = df.iloc[:, 13].values.tolist()
+postKE = df.iloc[:, 14].values.tolist()
+depositEnergy = df.iloc[:, 15].values.tolist()
+stepLength = df.iloc[:, 16].values.tolist()
+trackLength = df.iloc[:, 17].values.tolist()
+
+# get the energy density
+unit = 'MeV*cm^2/g'
+densitySiC = 3.217 * 1e-3
+densityAl = 2.6989 * 1e-3
+densitySi = 2.3296 * 1e-3
+densitySiO2 = 2.2 * 1e-3
+depositEnergy = np.array(depositEnergy)
+stepLength = np.array(stepLength)
+depositEnergyDensity = depositEnergy / stepLength
+for i in range(len(preVolumeName)):
+ if preVolumeName[i].strip() == 'V1':
+ depositEnergyDensity[i] = depositEnergyDensity[i] / densitySi
+ continue
+ if preVolumeName[i].strip() == 'V2':
+ depositEnergyDensity[i] = depositEnergyDensity[i] / densitySiO2
+ continue
+ if (preVolumeName[i].strip() == 'Vs1') or (preVolumeName[i].strip() == 'Vs2') or (preVolumeName[i].strip() == 'VD'):
+ depositEnergyDensity[i] = depositEnergyDensity[i] / densityAl
+ continue
+ else:
+ depositEnergyDensity[i] = depositEnergyDensity[i] / densitySiC
+depositEnergyDensity = depositEnergyDensity.tolist()
+depositEnergyDensity250000 = []
+preX_250000=[]
+preZ_250000=[]
+for i in range(len(depositEnergyDensity)):
+ if depositEnergyDensity[i] < 250000:
+ depositEnergyDensity250000.append(depositEnergyDensity[i])
+ preX_250000.append(preX[i])
+ preZ_250000.append(preZ[i])
+
+# draw the heatmap according to the coordinates and the energy deposit
+# the energy deposit is the color of the heatmap
+# the coordinates are the x and y axis of the heatmap
+# the z axis is the energy deposit
+# the colorbar is the energy deposit
+# draw the heatmap
+plt.scatter(preX, preZ, c=depositEnergy, s=0.1, cmap='jet',marker='.')
+plt.xlabel("preX")
+plt.ylabel("preZ")
+plt.title("heatmap_total")
+plt.colorbar()
+plt.savefig("heatmap_total.png", dpi=500)
+plt.show()
+
+plt.scatter(preX, preZ, c=depositEnergyDensity, s=0.1, cmap='jet',marker='.')
+plt.xlabel("preX")
+plt.ylabel("preZ")
+plt.title("heatmap_total")
+plt.colorbar()
+plt.savefig("heatmap_density_total.png", dpi=500)
+plt.show()
+
+plt.scatter(preX_250000, preZ_250000, c=depositEnergyDensity250000, s=1, cmap='jet',marker='.')
+plt.xlabel("preX")
+plt.ylabel("preZ")
+plt.title("heatmap_<250000")
+plt.colorbar()
+plt.savefig("heatmap_density_2500000.png", dpi=500)
+plt.show()
+
源代码:
// Create particle source
+G4GeneralParticleSource* particleSource = new G4GeneralParticleSource();
+
+// First particle source
+particleSource->SetCurrentSourceto(particleSource->GetNumberofSource() - 1);
+particleSource->GetCurrentSource()->GetPosDist()->SetCentreCoords(G4ThreeVector(-5.0*cm, 0.0, 0.0));
+particleSource->GetCurrentSource()->GetAngDist()->SetAngDistType("iso");
+particleSource->GetCurrentSource()->GetEneDist()->SetEnergyDisType("Mono");
+particleSource->GetCurrentSource()->GetEneDist()->SetMonoEnergy(10*MeV);
+particleSource->GetCurrentSource()->SetParticleDefinition(G4Neutron::NeutronDefinition());
+
+// Point source
+particleSource->GetCurrentSource()->GetPosDist()->SetPosDisType("Point");
+
+// Second particle source
+particleSource->AddaSource(1.0);
+particleSource->SetCurrentSourceto(particleSource->GetNumberofSource() - 1);
+particleSource->GetCurrentSource()->GetPosDist()->SetCentreCoords(G4ThreeVector(5.0*cm, 0.0, 0.0));
+particleSource->GetCurrentSource()->GetAngDist()->SetAngDistType("iso");
+particleSource->GetCurrentSource()->GetEneDist()->SetEnergyDisType("Mono");
+particleSource->GetCurrentSource()->GetEneDist()->SetMonoEnergy(20*MeV);
+particleSource->GetCurrentSource()->SetParticleDefinition(G4Proton::ProtonDefinition());
+
+// Plane source
+particleSource->GetCurrentSource()->GetPosDist()->SetPosDisType("Plane");
+particleSource->GetCurrentSource()->GetPosDist()->SetPosDisShape("Circle");
+particleSource->GetCurrentSource()->GetPosDist()->SetRadius(2*cm);
+
mac文件内:
/gps/source/0/intensity 500
+/gps/source/1/intensity 1000
+
+ ← + + 2023.03.12-PKA数据处理 + + 2023.06.14-Geant4各版本的安装 + + → +
sudo apt-get install -y build-essential
+sudo apt-get install -y wget
+// sudo apt-get install -y aptitude
+sudo apt-get install -y cmake libx11-dev libxext-dev libxtst-dev libxrender-dev libxmu-dev libxmuu-dev libhdf5-serial-dev hdf5-tools
+sudo apt-get install -y libexpat1-dev libxerces-c-dev libxt-dev libmotif-dev
+sudo apt install qt5* qt6* libqt5opengl5-dev
+
mkdir Geant4_11.1.1
+wget https://geant4-data.web.cern.ch/releases/geant4-v11.1.1.tar.gz
+// https://gitlab.cern.ch/geant4/geant4/-/archive/v10.5.1/geant4-v10.5.1.tar.gz
+tar -xzvf geant4-v11.1.1.tar.gz
+
+mkdir build
+cd build
+cmake -DCMAKE_INSTALL_PREFIX=/home/tiancheng/Geant4_11.1.1 /home/tiancheng/geant4-v11.1.1
+cmake -DGEANT4_INSTALL_DATA=ON -DGEANT4_BUILD_MULTITHREADED=ON -DGEANT4_USE_GDML=ON -DGEANT4_USE_QT=ON -DGEANT4_USE_OPENGL_X11=ON -DGEANT4_USE_RAYTRACER_X11=ON -DGEANT4_USE_NETWORKDAWN=ON -DGEANT4_USE_XM=ON -DGEANT4_USE_NETWORKVRML=ON .
+
+make -jN
+make install
+source /root/Geant4/home/geant4/bin/geant4.sh
+source /root/Geant4/home/geant4/share/Geant4/geant4make/geant4make.sh
+
main
:#include "G4RunManager.hh"
+
+int main()
+{
+ // 初始化 run manager
+ G4RunManager runManager;
+
+ // 设置物理列表
+ runManager.SetUserInitialization(new SimplePhysicsList());
+
+ // ... 其他初始化和设置
+
+ // 开始模拟
+ runManager.BeamOn();
+
+ return 0;
+}
+
#include "G4VModularPhysicsList.hh"
+#include "G4ParticleDefinition.hh"
+#include "G4ProcessManager.hh"
+#include "G4Proton.hh" // 或其他你关心的粒子
+
+class SimplePhysicsList : public G4VModularPhysicsList
+{
+public:
+ void ConstructParticle() override
+ {
+ // 创建或获取粒子类型
+ G4Proton::ProtonDefinition();
+ // ... 其他粒子
+ }
+
+ void ConstructProcess() override
+ {
+ // 获取粒子的过程管理器
+ G4ProcessManager* pManager = G4Proton::Proton()->GetProcessManager();
+
+ // 添加自定义的碰撞过程
+ pManager->AddDiscreteProcess(new SimpleCollisionProcess());
+ }
+};
+
+void YourCustomPhysicsList::ConstructProcess()
+{
+ // ... other processes
+
+ auto particleIterator = GetParticleIterator();
+ particleIterator->reset();
+
+ while ((*particleIterator)())
+ {
+ G4ParticleDefinition* particle = particleIterator->value();
+ G4ProcessManager* pmanager = particle->GetProcessManager();
+
+ if (YourCustomProcess::IsApplicable(*particle))
+ {
+ pmanager->AddDiscreteProcess(new YourCustomProcess());
+ }
+ }
+}
+
#include "G4VDiscreteProcess.hh"
+#include "G4Step.hh"
+#include "G4VParticleChange.hh"
+
+class SimpleCollisionProcess : public G4VDiscreteProcess
+{
+public:
+ G4VParticleChange* PostStepDoIt(const G4Track& track, const G4Step& /*step*/) override
+ {
+ // 这里执行碰撞后的粒子状态更改
+ // 例如,改变动量、能量等
+
+ aParticleChange.Initialize(track);
+ // aParticleChange.SetXXX(...); // 设置粒子状态
+ return &aParticleChange;
+ }
+
+ G4double GetMeanFreePath(const G4Track& /*track*/, G4double /*previousStepSize*/, G4ForceCondition* /*condition*/) override
+ {
+ // 返回这个过程的平均自由路径
+ // 这里只返回一个固定的值作为示例
+ return 1.0 * cm;
+ }
+};
+
PostStepDoIt
: 这个函数用于描述在一步运动后粒子的状态如何改变。这通常是必须要重写的。GetMeanFreePath
: 这个函数返回给定粒子在特定条件下的平均自由路径。这通常也需要重写以适应你的模型。这两个函数通常是最主要需要重写的。然而,根据你的具体需求,有时还可能需要重写其他函数,比如:
AlongStepDoIt
: 如果你从 G4VContinuousProcess
继承,这个函数用于描述粒子在一步中如何连续地改变。IsApplicable
: 这个函数决定这个过程是否应用于某个粒子。AtRestDoIt
: 用于描述一个静止粒子如何起始一个新的过程(如果适用)。StartTracking
、EndTracking
: 这些是在粒子轨迹开始和结束时被调用的。aParticleChange
:aParticleChange
是一个 G4VParticleChange
对象,它用于保存由这个过程产生的粒子状态改变。当 PostStepDoIt
被调用时,你需要用它来设置新的粒子状态(如位置、动量、能量等)。这个对象通常是 G4VDiscreteProcess
或 G4VContinuousProcess
的成员变量,由基类初始化。
例如,你可以这样设置新的动量方向:
aParticleChange.ProposeMomentumDirection(newDirection);
+
或者,设置新的能量:
aParticleChange.ProposeEnergy(newEnergy)
+
这样,在 PostStepDoIt
函数返回后,Geant4 会用 aParticleChange
中的信息来更新粒子状态。
aParticleChange.Initialize(track)
这个函数初始化 aParticleChange
对象,使其包含给定轨迹(track
)的当前状态。这通常是你开始修改粒子状态之前的第一步。
GetMeanFreePath
的作用GetMeanFreePath
函数是用来告知 Geant4 在多长的距离后应该调用 PostStepDoIt
函数的。简单地说,这个函数决定了一个物理过程发生的"频率"或者概率。在每一步结束后,Geant4 会评估所有的离散过程来看哪一个将被下一个执行,这一决定是基于每个过程的平均自由路径和其他概率性因素来的。
比如说,如果 GetMeanFreePath
返回了一个很小的数值,这表示该过程非常可能在下一步就会发生。相反地,一个大的返回值意味着该过程相对较不可能发生。
在 Geant4 中,一粒子的轨迹(track)被分解为多个连续的 "步"(steps)。在每一步结束时,Geant4 会评估所有应用于该粒子类型的离散物理过程(例如电离、散射等)。这个评估的目的是为了决定下一步应该执行哪个过程。
假设我们有三个不同的物理过程 A、B 和 C。每一个都有自己的 GetMeanFreePath
函数,这个函数返回一个距离值,该值表示粒子在该距离后大概会经历该过程。
GetMeanFreePath
函数,获得 A、B、C 的平均自由路径,分别记作λA,λB,λC。PostStepDoIt
函数来实际模拟该过程,更新粒子的状态。PostStepDoIt
的结果,粒子的状态被更新,然后开始下一步的模拟。这样,通过在每一步结束时动态评估哪一个过程应该被执行,Geant4 能够模拟一个粒子轨迹中多种物理过程的竞争和相互作用。这是为什么需要定义 GetMeanFreePath
的原因:它为这个概率性决策提供了必要的信息。
#include "G4VDiscreteProcess.hh"
+#include "G4Step.hh"
+#include "G4ParticleChange.hh"
+
+class SimpleElasticScattering : public G4VDiscreteProcess {
+public:
+ // ... Constructors, destructor, etc.
+
+ G4VParticleChange* PostStepDoIt(const G4Track& track, const G4Step& step) override {
+ aParticleChange.Initialize(track);
+ // ... Do some calculations for scattering, modify aParticleChange
+ return &aParticleChange;
+ }
+
+ G4double GetMeanFreePath(const G4Track&, G4double, G4ForceCondition*) override {
+ return 1.0; // Just a constant mean free path for demonstration
+ }
+};
+
+class EnergyAbsorption : public G4VDiscreteProcess {
+public:
+ // ... Constructors, destructor, etc.
+
+ G4VParticleChange* PostStepDoIt(const G4Track& track, const G4Step& step) override {
+ aParticleChange.Initialize(track);
+ // ... Do some calculations for energy absorption, modify aParticleChange
+ return &aParticleChange;
+ }
+
+ G4double GetMeanFreePath(const G4Track&, G4double, G4ForceCondition*) override {
+ return 2.0; // Another constant mean free path for demonstration
+ }
+};
+
在物理列表中添加:
#include "MyPhysicsList.hh"
+#include "G4ParticleDefinition.hh"
+#include "G4ProcessManager.hh"
+
+void MyPhysicsList::ConstructProcess() {
+ // ... Other setup code
+
+ // Get the process manager for ions (or whatever particle you're interested in)
+ G4ParticleDefinition* ion = G4Ion::IonDefinition();
+ G4ProcessManager* pmanager = ion->GetProcessManager();
+
+ // Add the custom processes
+ SimpleElasticScattering* elasticScattering = new SimpleElasticScattering();
+ EnergyAbsorption* energyAbsorption = new EnergyAbsorption();
+
+ pmanager->AddDiscreteProcess(elasticScattering);
+ pmanager->AddDiscreteProcess(energyAbsorption);
+
+ // ... Other code for adding standard processes
+}
+
G4VParticleChange* YourProcess::PostStepDoIt(const G4Track& track, const G4Step& step)
+{
+ aParticleChange.Initialize(track);
+
+ // Execute your methods
+ FreeLength();
+ ChoseCollisionAtom();
+ EleEnergyLoss();
+ EmissionAngle();
+ IonNewCondition();
+
+ // Update particle state in aParticleChange (not shown)
+ // ...
+
+ // Check whether the ion is still under processing
+ int IonFlag = 1;
+ OutOrNot(IonFlag);
+
+ // If IonFlag is changed by OutOrNot() to indicate the ion's journey is done,
+ // you might need additional logic here to finalize the ion's record.
+ // ...
+
+ return &aParticleChange;
+}
+
在 Geant4 中,如果一个粒子已经不再需要进一步处理,你通常需要更新这个粒子的状态,以便 Geant4 知道不再需要追踪这个粒子。这通常是通过设置 G4Track
的状态来完成的,具体来说,是通过 aParticleChange
对象(这通常是 G4VParticleChange
类型或其子类)。
一个简单的例子:
if (some_condition) { // Replace this with your actual stopping condition
+ aParticleChange.ProposeTrackStatus(fStopAndKill);
+}
+
+
在这里,fStopAndKill
是一个枚举值,表示该粒子应停止并被“杀死”,即不再被追踪。其他可用的状态还包括 fStopButAlive
(停止但仍然“存活”,可能在以后的时间步重新开始)和 fAlive
(继续运动)。
如果在 PostStepDoIt
函数中确定粒子需要被停止,你可以通过上面的代码片段来告诉 Geant4。这样,Geant4 在执行完该步骤后就会知道不再需要对这个粒子进行后续计算。
void PKU_MC::DoOneIon()
+{
+ int IonFlag;
+ // init the orginal paramters for current ions
+ InitialIon();
+ IonFlag = 1;
+
+ while (IonFlag == 1) // the current ion are under processing
+ {
+ FreeLength();
+ ChoseCollisionAtom();
+ EleEnergyLoss();
+ EmissionAngle();
+ IonNewCondition();
+ ProcessRecorder();
+ OutOrNot(IonFlag);
+ } // end of one ions
+
+ CollisionRecording(); // add the results of this ion to the recording arraies
+}
+
InitialIon()
void PKU_MC::InitialIon()
+{
+
+ E = IonEnergyEV; // Energy
+ COSIN = cos(ALPHA); // direction to the normal
+ SINY = COSIN;
+
+ SINE = sin(ALPHA);
+ COSY = SINE;
+
+ CurrentLayer = 1;
+ IonWayLength = 0;
+ IonCollisionNo = 0;
+ X = 0;
+ Y = 0;
+}
+
FreeLength()
double PKU_MC::FreeLength()
+{
+ float tempvalue;
+
+ // when the ion energy is high
+ IonCollisionNo++;
+ ReducedCollEnergy = E * F[CurrentLayer];
+ EEG = sqrt(ReducedCollEnergy * EPSDG[CurrentLayer]);
+ PMAX[CurrentLayer] = A[CurrentLayer] / (EEG + sqrt(EEG) + 0.125 * pow(EEG, 0.1));
+ IonFreeLength = 1.0 / (PI * PMAX[CurrentLayer] * PMAX[CurrentLayer] * ARHO[CurrentLayer]);
+
+ // when the ion enrgy is low
+
+ tempvalue = PKURandom();
+
+ if (IonCollisionNo == 1)
+ IonFreeLength = tempvalue * AMIN1(IonFreeLength, channelwidth);
+
+ return IonFreeLength;
+}
+
ReducedCollEnergy
: 同样表示与被选中的原子碰撞后的"减少的"能量。E
: 未定义在这个函数中,但似乎是离子或粒子的能量。F[CurrentLayer]
: 当前层对应的一个因子,用于计算ReducedCollEnergy
。EEG
: 一个与ReducedCollEnergy
和EPSDG[CurrentLayer]
有关的参数。EPSDG[CurrentLayer]
: 与当前层有关的一个常数或参数。PMAX[CurrentLayer]
: 当前层中的最大动量或概率。A[CurrentLayer]
: 当前层对应的一个参数或常数。IonFreeLength
: 离子的自由路径长度。ARHO[CurrentLayer]
: 当前层的密度或者与密度相关的一个参数。channelwidth
: 一个与离子自由路径长度有关的参数或者常数。E
, F
, EPSDG
, A
, ARHO
IonCollisionNo
, ReducedCollEnergy
, IonFreeLength
, PMAX
ChoseCollisionAtom()
double PKU_MC::ChoseCollisionAtom()
+{
+
+ int MaxElement;
+ float tempRandValue;
+
+ tempRandValue = PKURandom();
+ while (tempRandValue == 0)
+ {
+ tempRandValue = PKURandom();
+ }
+ P = PMAX[CurrentLayer] * sqrt(tempRandValue);
+ MaxElement = LayerElementNo[CurrentLayer];
+
+ // decided which kinds of atoms to be knocked!
+
+ tempRandValue = PKURandom();
+
+ for (CurrentElement = 1; CurrentElement <= MaxElement; CurrentElement++)
+ {
+ tempRandValue = tempRandValue - TargetAtomConc[CurrentLayer][CurrentElement];
+ if (tempRandValue < 0)
+ break;
+ }
+ if (tempRandValue >= 0)
+ CurrentElement = LayerElementNo[CurrentLayer];
+
+ ReducedCollEnergy = FItemp[CurrentLayer][CurrentElement] * E;
+ B = P / ScreenLen[CurrentLayer][CurrentElement];
+
+ return TargetAtomNumber[CurrentLayer][CurrentElement];
+}
+
P
: 未定义在这个函数中,但在函数内部进行了计算。可能是与粒子动量或概率有关的一个参数。PMAX[CurrentLayer]
: 当前层中的最大动量或概率。CurrentLayer
: 当前所在的层编号。LayerElementNo[CurrentLayer]
: 当前层包含的元素(原子)种类数量。CurrentElement
: 当前正在考虑的元素(原子)的编号。TargetAtomConc[CurrentLayer][CurrentElement]
: 当前层和元素对应的原子浓度或概率。ReducedCollEnergy
: 表示与被选中的原子碰撞后的"减少的"能量。FItemp[CurrentLayer][CurrentElement]
: 与当前层和元素有关的一个因子,用于计算ReducedCollEnergy
。E
: 未定义在这个函数中,但用于计算ReducedCollEnergy
,可能是粒子的能量。B
: 另一个与碰撞有关的参数。ScreenLen[CurrentLayer][CurrentElement]
: 与当前层和元素有关的一个参数,用于计算B
。TargetAtomNumber[CurrentLayer][CurrentElement]
: 返回的是当前层和元素对应的目标原子的编号。PMAX
, FItemp
, E
, ScreenLen
P
, ReducedCollEnergy
, B
EleEnergyLoss()
double PKU_MC::EleEnergyLoss()
+{
+ double Ion_Se;
+
+ IE = int(E / IonEnergyKeV + .5);
+ if (IE != 0)
+ SEE = SE[CurrentLayer][IE];
+ if (E < IonEnergyKeV)
+ SEE = SE[CurrentLayer][1] * sqrt(E / IonEnergyKeV);
+ DEE = IonFreeLength * SEE;
+ Ion_Se = DEE;
+ return Ion_Se;
+}
+
double Ion_Se
: 存储电子或离子的能量损失。IE
: 一个与离子或粒子能量E
有关的整数变量。E
: 离子或粒子的能量,未在此函数内定义。IonEnergyKeV
: 离子能量的一个特定单位(千电子伏特,KeV)。SEE
: 与离子或粒子能量损失有关的一个参数。SE[CurrentLayer][IE]
: 当前层和能量级对应的能量损失率。DEE
: 实际能量损失,是IonFreeLength
(离子的自由路径长度)和SEE
(能量损失率)的乘积。E
, IonEnergyKeV
, SE
, IonFreeLength
DEE
,
EmissionAngle()
double PKU_MC::EmissionAngle()
+{
+ double AngleValue, R, Q;
+ // using the magic method
+
+ if (ReducedCollEnergy <= 10)
+ {
+ R = B;
+ RR = -2.7 * log(ReducedCollEnergy * B);
+ if (RR >= B)
+ {
+ RR = -2.7 * log(ReducedCollEnergy * RR);
+ if (RR >= B)
+ R = RR;
+ }
+ do
+ {
+ EX1 = 0.18175 * exp(-3.1998 * R);
+ EX2 = 0.50986 * exp(-0.94229 * R);
+ EX3 = 0.28022 * exp(-0.4029 * R);
+ EX4 = 0.028171 * exp(-0.20162 * R);
+ V = (EX1 + EX2 + EX3 + EX4) / R;
+ V1 = -(V + 3.1998 * EX1 + 0.94229 * EX2 + 0.4092 * EX3 + 0.20162 * EX4) / R;
+
+ FR = B * B / R + V * R / ReducedCollEnergy - R;
+ FR1 = -B * B / (R * R) + (V + V1 * R) / ReducedCollEnergy - 1;
+ Q = FR / FR1;
+ R = R - Q;
+ } while (fabs(Q / R) > 0.001);
+
+ ROC = -2.0 * (ReducedCollEnergy - V) / V1;
+ SQE = sqrt(ReducedCollEnergy);
+ CC = (0.011615 + SQE) / (0.0071222 + SQE);
+ AA = 2.0 * ReducedCollEnergy * (1.0 + (0.99229 / SQE)) * pow(B, CC);
+ FF = (sqrt(AA * AA + 1.0) - AA) * ((9.3066 + ReducedCollEnergy) / (14.813 + ReducedCollEnergy));
+ DELTA = (R - B) * AA * FF / (FF + 1.0);
+ CO = (B + DELTA + ROC) / (R + ROC);
+ C2 = CO * CO;
+ S2 = 1.0 - C2;
+ if (S2 < -1)
+ printf("\n%d %lf\n", ElapsedIonNo, B);
+ CT = 2.0 * C2 - 1.0;
+ ST = sqrt(1.0 - CT * CT);
+ }
+ // using the RusefScattering methods
+ else{
+ S2 = 1.0 / (1.0 + (1.0 + B * (1.0 + B)) * (2.0 * ReducedCollEnergy * B) * (2.0 * ReducedCollEnergy * B));
+ if (S2 < -1)
+ printf("\n%d %lf\n", ElapsedIonNo, B);
+ C2 = 1.0 - S2;
+ CT = 2.0 * C2 - 1.0;
+ ST = sqrt(1.0 - CT * CT);
+ }
+ AngleValue = acos(CT);
+ return AngleValue;
+}
+
ReducedCollEnergy
, B
,
RR
, ST
, S2
, CT
,
IonNewCondition()
double PKU_MC::IonNewCondition()
+{
+ double MAX(0), X1; // MAX is not OK!!!!!!!!!!!!!!!!!!!!!!!!!
+ float tempvalue;
+
+ DEN = recoilfactor[CurrentLayer][CurrentElement] * S2 * E;
+ E = E - DEN - DEE;
+ if (DEE > MAX)
+ MAX = DEE;
+ IonWayLength = IonWayLength + IonFreeLength - DistanceNuclear;
+
+ X = X + (IonFreeLength - DistanceNuclear) * COSIN;
+ Y = Y + (IonFreeLength - DistanceNuclear) * COSY;
+
+ I = AMIN1(fabs(X / channelwidth) + 1.0, 100.0);
+ J = AMIN1(fabs(Y / channelwidth) + 1.0, 50.0);
+
+ if ((ALPHA != 0) && (Y <= 0))
+ J = 1;
+
+ tempvalue = PKURandom();
+ PHI = 2.0 * PI * tempvalue;
+
+ PSI = atan(ST / (CT + M1_to_M2[CurrentLayer][CurrentElement]));
+ if (PSI < 0)
+ PSI = PSI + PI;
+ X1 = -COSIN * COSY / (SINE * SINY + pow(10, -8));
+ if (fabs(X1) > 1.0)
+ X1 = X1 / fabs(X1);
+ DELTA = PHI - acos(X1);
+
+ COSIN = COSIN * cos(PSI) + SINE * sin(PSI) * cos(PHI);
+ COSY = COSY * cos(PSI) + SINY * sin(PSI) * cos(DELTA);
+
+ SINY = sqrt(1.0 - COSY * COSY);
+ SINE = sqrt(1.0 - COSIN * COSIN);
+ return 1;
+}
+
recoilfactor
, DistanceNuclear
, IonFreeLength
, channelwidth
, ALPHA
, M1_to_M2
,
if (channelwidth == 0)
+ channelwidth = 0.01 * LayertoSurface[3]; // for recording, the total sample are dividied into 100 segment
+
DEN
, E
, IonWayLength
, COSIN
, COSY
, X
, Y
, I
, J
, PHI
, PSI
, SINE
, SINY
, DELTA
,
ProcessRecorder()
void PKU_MC::ProcessRecorder()
+{
+
+ double EPSD, EN;
+
+ // total energy
+ MTOT[I - 1][J] = MTOT[I - 1][J] + DEN + DEE;
+
+ // the nuclear energy loss treamting process
+ if (DEN < DisEnergy)
+ PhoneEDis[I] = PhoneEDis[I] + DEN;
+ else
+ {
+ EPSD = DamageEFract[CurrentLayer] * DEN;
+
+ // Modified Kinchin-Pease model
+ EN = DEN / (1.0 + ElectronEFract[CurrentLayer] * (EPSD + 0.4 * pow(EPSD, 0.75) + 3.4 * pow(EPSD, (1.0 / 6.0))));
+
+ if (EN < DisEnergy)
+ PhoneEDis[I] = PhoneEDis[I] + DEN;
+ else
+ {
+
+ MVAC[I - 1][J] = MVAC[I - 1][J] + 1;
+ IVAC[I] = IVAC[I] + 1;
+ if (EN > 0) //!!!!
+ RPHON[I] = RPHON[I] + EN - DisEnergy;
+
+ // Multi-defect production!
+ if (EN >= 2.5 * DisEnergy)
+ {
+ MVAC[I - 1][J] = MVAC[I - 1][J] - 1.0 + 0.4 * EN / DisEnergy;
+ RPHON[I] = RPHON[I] + DisEnergy - 0.4 * EN;
+ RVAC[I] = RVAC[I] - 1.0 + 0.4 * EN / DisEnergy;
+ }
+
+ MION[I - 1][J] = MION[I - 1][J] + DEN - EN;
+ RION[I] = RION[I] + DEN - EN;
+ }
+ }
+
+ // eletric energy loss treating process
+ IonizatinEDis[I] = IonizatinEDis[I] + DEE;
+
+ MION[I - 1][J] = MION[I - 1][J] + DEE;
+}
+
I
, J
, DEN
, DEE
, DisEnergy
, DamageEFract
, ElectronEFract
,
MTOT
, PhoneEDis
, MVAC
, IVAC
, RPHON
, RVAC
, MION
, RION
, IonizatinEDis
,
OutOrNot(int &Ionflag)
int PKU_MC::OutOrNot(int &Ionflag)
+{
+ if (X < 0)
+ {
+ Ionflag = 0;
+ return 1;
+ }
+ else
+ {
+ if (X <= LayertoSurface[1])
+ CurrentLayer = 1;
+ else if (X <= LayertoSurface[2])
+ CurrentLayer = 2;
+ else
+ CurrentLayer = 3;
+ if (X >= LayertoSurface[CurrentLayer])
+ {
+ Ionflag = 0;
+ return 1;
+ }
+ }
+ if (E <= StoppingEnergy)
+ {
+ Ionflag = 0;
+ return 1;
+ }
+}
+
X
, LayertoSurface
, StoppingEnergy
, E
,
Ionflag
,
CMakeLists.txt
与run.mac
的自动创建# Python script to generate CMakeLists.txt and run.mac files with customizable parameters.
+
+def create_cmake_lists(project_name, executable_name, files_to_copy):
+ """
+ Creates a CMakeLists.txt with customizable project and executable names.
+ Args:
+ - project_name: Name of the project
+ - executable_name: Name of the executable
+ - files_to_copy: List of files to copy
+ Returns:
+ - A string containing the contents of the CMakeLists.txt file
+ """
+ cmake_template = f"""cmake_minimum_required(VERSION 3.16)
+project({project_name})
+
+# default build type: Debug
+if(NOT CMAKE_BUILD_TYPE)
+ set(CMAKE_BUILD_TYPE Debug)
+endif()
+
+#----------------------------------------------------------------------------
+# Find Geant4 package, activating all available UI and Vis drivers by default
+# You can set WITH_GEANT4_UIVIS to OFF via the command line or ccmake/cmake-gui
+# to build a batch mode only executable
+#
+option(WITH_GEANT4_UIVIS "Build example with Geant4 UI and Vis drivers" ON)
+if(WITH_GEANT4_UIVIS)
+ find_package(Geant4 REQUIRED ui_all vis_all)
+else()
+ find_package(Geant4 REQUIRED)
+endif()
+
+#----------------------------------------------------------------------------
+# Setup Geant4 include directories and compile definitions
+# Setup include directory for this project
+#
+include_directories(${{PROJECT_SOURCE_DIR}}/include)
+include(${{Geant4_USE_FILE}})
+
+#----------------------------------------------------------------------------
+# Locate sources and headers for this project
+# NB: headers are included so they will show up in IDEs
+#
+file(GLOB sources ${{PROJECT_SOURCE_DIR}}/src/*.cc)
+file(GLOB headers ${{PROJECT_SOURCE_DIR}}/include/*.hh)
+
+#----------------------------------------------------------------------------
+# Add the executable, and link it to the Geant4 libraries
+#
+add_executable({executable_name} main.cc ${{sources}} ${{headers}})
+# target_compile_definitions({executable_name} PUBLIC G4GEOM_USE_USOLIDS)
+target_link_libraries({executable_name} ${{Geant4_LIBRARIES}})
+
+#----------------------------------------------------------------------------
+# Copy all scripts to the build directory, i.e. the directory in which we
+# build PKU_Trim_Geant4. This is so that we can run the executable directly
+# because it relies on these scripts being in the current working directory.
+#
+set(FILES_TO_COPY{files_to_copy})
+foreach(_script ${{FILES_TO_COPY}})
+ configure_file(
+ ${{PROJECT_SOURCE_DIR}}/${{_script}}
+ ${{PROJECT_BINARY_DIR}}/${{_script}}
+ COPYONLY
+ )
+endforeach()
+"""
+ return cmake_template
+
+def create_run_mac(
+ numberOfThreads, outputFileName, iondefinition, ionenergy, numberOfIons
+):
+ """
+ Creates a run.mac file with customizable parameters.
+ Args:
+ - numberOfThreads: Number of threads
+ - outputFileName: Name of the output file
+ - iondefinition: Ion definition (Z A Q E)
+ - ionenergy: Ion energy
+ - numberOfIons: Number of ions
+ Returns:
+ - A string containing the contents of the run.mac file
+ """
+ run_mac_content = f"""# Macro file for the runtime control of the example
+
+# Minimal run.mac file for testing the application
+
+# Set number of threads
+# /run/numberOfThreads {numberOfThreads}
+# /tracking/verbose 2
+
+# Initialize kernel
+/run/initialize
+/myApp/setOutputFileName {outputFileName}
+
+# Start simulation
+/gun/particle ion
+/gun/ion {iondefinition}
+/gun/energy {ionenergy}
+/run/beamOn {numberOfIons}
+"""
+ return run_mac_content
+
+if __name__ == "__main__":
+ # Default values
+ project_name = "PKU_Trim_Geant4"
+ executable_name = "PKU_Trim_Geant4"
+ files_to_copy = """
+ init_vis.mac
+ vis.mac
+ run.mac
+ scoef.data
+ scoefh.data
+ thresholds.txt
+ """
+
+ # Generate CMakeLists.txt
+ cmake_content = create_cmake_lists(project_name, executable_name, files_to_copy)
+
+ outputFileName = "FinFET_p_2MeV.txt"
+ iondefinition = "1 1 0 0" # Z A Q E
+ ionenergy = "2 MeV"
+ numberOfIons = 10000
+
+ # Generate run.mac
+ run_mac_content = create_run_mac(
+ 4, outputFileName, iondefinition, ionenergy, numberOfIons
+ )
+
+ # Define the paths for the output files
+ cmake_lists_path = "./CMakeLists.txt"
+ run_mac_path = "./run.mac"
+
+ # Write the contents to the respective files
+ with open(cmake_lists_path, "w") as cmake_file:
+ cmake_file.write(cmake_content)
+
+ with open(run_mac_path, "w") as run_mac_file:
+ run_mac_file.write(run_mac_content)
+
CMakeLists.txt
中:executable_name
:生成的可执行文件名
files_to_copy
:需要复制到build文件夹中的内容
run.mac
中numberOfThreads
:Geant4运行时使用的核心数(若使用Docker可不填)
outputFileName
:Geant4产生的数据文件名
iondefinition
:入射离子类型 # Z A Q E
ionenergy
:入射离子能量
numberOfIons
:入射离子数
Dockerfile
:# 原单Geant4镜像
+FROM outispku/geant4:v1
+
+# 指定启动脚本
+COPY start.sh /
+RUN chmod +x /start.sh
+RUN mkdir program
+ENTRYPOINT ["/start.sh"]
+
start.sh
:#!/bin/bash
+
+# Source Geant4 environment scripts
+source /geant4/bin/geant4.sh
+source /geant4/share/Geant4/geant4make/geant4make.sh
+
+# 获取系统的线程数
+NUM_THREADS=$(expr $(nproc) - 1)
+
+cd /program
+
+# 构建命令
+CMD_TO_ADD="/run/numberOfThreads $NUM_THREADS"
+
+# 将命令添加到 run.mac 的第一行
+# 注意不要重复添加
+sed -i "1i $CMD_TO_ADD" ./run.mac
+
+# 定义目录路径
+BUILD_DIR="/program/build"
+
+# 检查目录是否存在
+if [ -d "$BUILD_DIR" ]; then
+ # 如果目录存在,删除它
+ rm -rf "$BUILD_DIR"
+fi
+
+# 创建新的目录
+mkdir -p "$BUILD_DIR"
+cd build
+cmake ..
+make -j $NUM_THREADS
+# PKU_Trim_Geant4 为程序名称(可执行文件),由CMakeLists.txt中的add_executable(PKU_Trim_Geant4 PKU_Trim_Geant4.cc)决定
+# 使用前请修改
+./PKU_Trim_Geant4 run.mac > /dev/null 2>>error.txt
+
默认采用处理器最大核心数减1作为使用的核心数量
默认程序名为PKU_Trim_Geant4
,若在CMakeLists中修改,请重新使用Dockerfile创建镜像
docker build -t outispku/geant4:v2 .
+# -t 后为新镜像名
+
创建共享文件路径并运行容器
docker run -v D:\FinFET:/program --name geant4_program outispku/geant4:v2
+# -v 后为映射本地路径到容器内(共享项目文件)(program为容器内路径,请不要修改)
+# --name 为容器名
+
Docker 是一个应用打包、分发、部署的工具
你也可以把它理解为一个轻量的虚拟机,它只虚拟你软件需要的运行环境,多余的一点都不要,
而普通虚拟机则是一个完整而庞大的系统,包含各种不管你要不要的软件。
特性 | 普通虚拟机 | Docker |
---|---|---|
跨平台 | 通常只能在桌面级系统运行,例如 Windows/Mac,无法在不带图形界面的服务器上运行 | 支持的系统非常多,各类 windows 和 Linux 都支持 |
性能 | 性能损耗大,内存占用高,因为是把整个完整系统都虚拟出来了 | 性能好,只虚拟软件所需运行环境,最大化减少没用的配置 |
自动化 | 需要手动安装所有东西 | 一个命令就可以自动部署好所需环境 |
稳定性 | 稳定性不高,不同系统差异大 | 稳定性好,不同系统都一样部署方式 |
打包:就是把你软件运行所需的依赖、第三方库、软件打包到一起,变成一个安装包
分发:你可以把你打包好的“安装包”上传到一个镜像仓库,其他人可以非常方便的获取和安装
部署:拿着“安装包”就可以一个命令运行起来你的应用,自动模拟出一摸一样的运行环境,不管是在 Windows/Mac/Linux。
常规应用开发部署方式:自己在 Windows 上开发、测试 --> 到 Linux 服务器配置运行环境部署。
问题:我机器上跑都没问题,怎么到服务器就各种问题了
用 Docker 开发部署流程:自己在 Windows 上开发、测试 --> 打包为 Docker 镜像(可以理解为软件安装包) --> 各种服务器上只需要一个命令部署好
优点:确保了不同机器上跑都是一致的运行环境,不会出现我机器上跑正常,你机器跑就有问题的情况。
例如 易文档 (opens new window),SVNBucket (opens new window) 的私有化部署就是用 Docker,轻松应对客户的各种服务器。
镜像:可以理解为软件安装包,可以方便的进行传播和安装。
容器:软件安装后的状态,每个软件运行环境都是独立的、隔离的,称之为容器。
桌面版:https://www.docker.com/products/docker-desktop (opens new window)
服务器版:https://docs.docker.com/engine/install/#server (opens new window)
报错截图
解决方法:
控制面板->程序->启用或关闭 windows 功能,开启 Windows 虚拟化和 Linux 子系统(WSL2)
命令行安装 Linux 内核
wsl.exe --install -d Ubuntu
你也可以打开微软商店 Microsoft Store 搜索 Linux 进行安装,选择一个最新版本的 Ubuntu 或者 Debian 都可以
上面命令很可能你安装不了,微软商店你也可能打不开,如果遇到这个问题,参考:https://blog.csdn.net/qq_42220935/article/details/104714114
设置开机启动 Hypervisor
bcdedit /set hypervisorlaunchtype auto
注意要用管理员权限打开 PowerShell
设置默认使用版本2
wsl.exe --set-default-version 2
查看 WSL 是否安装正确
wsl.exe --list --verbose
应该如下图,可以看到一个 Linux 系统,名字你的不一定跟我的一样,看你安装的是什么版本。
并且 VERSION 是 2
确保 BIOS 已开启虚拟化,下图检查是否已开启好
如果是已禁用,请在开机时按 F2 进入 BIOS 开启一下,不会设置的可以网上搜索下自己主板的设置方法,Intel 和 AMD 的设置可能稍有不同
出现下图错误,点击链接安装最新版本的 WSL2
https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi (opens new window)
镜像加速器 | 镜像加速器地址 |
---|---|
Docker 中国官方镜像 | https://registry.docker-cn.com |
DaoCloud 镜像站 | http://f1361db2.m.daocloud.io |
Azure 中国镜像 | https://dockerhub.azk8s.cn |
科大镜像站 | https://docker.mirrors.ustc.edu.cn |
阿里云 | https://ud6340vz.mirror.aliyuncs.com |
七牛云 | https://reg-mirror.qiniu.com |
网易云 | https://hub-mirror.c.163.com |
腾讯云 | https://mirror.ccs.tencentyun.com |
"registry-mirrors": ["https://registry.docker-cn.com"]
+ 02. 💻 Docker 快速安装软件 + + → +
本文档课件配套 视频教程
Redis 官网:https://redis.io/ (opens new window)
官网下载安装教程只有源码安装方式,没有 Windows 版本。想要自己安装 windows 版本需要去找别人编译好的安装包。
Docker 官方镜像仓库查找 Redis :https://hub.docker.com/ (opens new window)
一个命令跑起来:docker run -d -p 6379:6379 --name redis redis:latest
命令参考:https://docs.docker.com/engine/reference/commandline/run/ (opens new window)
docker-compose.yml
version: '3.1'
+
+services:
+
+ wordpress:
+ image: wordpress
+ restart: always
+ ports:
+ - 8080:80
+ environment:
+ WORDPRESS_DB_HOST: db
+ WORDPRESS_DB_USER: exampleuser
+ WORDPRESS_DB_PASSWORD: examplepass
+ WORDPRESS_DB_NAME: exampledb
+ volumes:
+ - wordpress:/var/www/html
+
+ db:
+ image: mysql:5.7
+ restart: always
+ environment:
+ MYSQL_DATABASE: exampledb
+ MYSQL_USER: exampleuser
+ MYSQL_PASSWORD: examplepass
+ MYSQL_RANDOM_ROOT_PASSWORD: '1'
+ volumes:
+ - db:/var/lib/mysql
+
+volumes:
+ wordpress:
+ db:
+
docker run -p 5601:5601 -p 9200:9200 -p 5044:5044 -it --name elk sebp/elk
转到用户目录 cd ~
,路径类似这个:C:\Users\<UserName>
创建 .wslconfig
文件填入以下内容
[wsl2]
+memory=10GB # Limits VM memory in WSL 2 to 4 GB
+processors=2 # Makes the WSL 2 VM use two virtual processors
+
生效配置,命令行运行 wsl --shutdown
docker ps
查看当前运行中的容器
docker images
查看镜像列表
docker rm container-id
删除指定 id 的容器
docker stop/start container-id
停止/启动指定 id 的容器
docker rmi image-id
删除指定 id 的镜像
docker volume ls
查看 volume 列表
docker network ls
查看网络列表
+ ← + + 01. 🎉 Docker 简介和安装 + + 03. 💽 制作自己的镜像 + + → +
示例项目代码:https://github.com/gzyunke/test-docker (opens new window)
这是一个 Nodejs + Koa2 写的 Web 项目,提供了简单的两个演示页面。
软件依赖:nodejs (opens new window)
项目依赖库:koa、log4js、koa-router
本文档课件配套 视频教程
FROM node:11
+MAINTAINER easydoc.net
+
+# 复制代码
+ADD . /app
+
+# 设置容器启动后的默认运行目录
+WORKDIR /app
+
+# 运行命令,安装依赖
+# RUN 命令可以有多个,但是可以用 && 连接多个命令来减少层级。
+# 例如 RUN npm install && cd /app && mkdir logs
+RUN npm install --registry=https://registry.npm.taobao.org
+
+# CMD 指令只能一个,是容器启动后执行的命令,算是程序的入口。
+# 如果还需要运行其他命令可以用 && 连接,也可以写成一个shell脚本去执行。
+# 例如 CMD cd /app && ./start.sh
+CMD node app.js
+
Dockerfile文档 (opens new window)
实用技巧:
如果你写 Dockerfile 时经常遇到一些运行错误,依赖错误等,你可以直接运行一个依赖的底,然后进入终端进行配置环境,成功后再把做过的步骤命令写道 Dockerfile 文件中,这样编写调试会快很多。
例如上面的底是
node:11
,我们可以运行docker run -it -d node:11 bash
,跑起来后进入容器终端配置依赖的软件,然后尝试跑起来自己的软件,最后把所有做过的步骤写入到 Dockerfile 就好了。掌握好这个技巧,你的 Dockerfile 文件编写起来就非常的得心应手了。
编译 docker build -t test:v1 .
-t 设置镜像名字和版本号
命令参考:https://docs.docker.com/engine/reference/commandline/build/ (opens new window)
运行 docker run -p 8080:8080 --name test-hello test:v1
-p 映射容器内端口到宿主机
-name
容器名字d
后台运行命令参考文档:https://docs.docker.com/engine/reference/run/ (opens new window)
docker ps
查看当前运行中的容器
docker images
查看镜像列表
docker rm container-id
删除指定 id 的容器
docker stop/start container-id
停止/启动指定 id 的容器
docker rmi image-id
删除指定 id 的镜像
docker volume ls
查看 volume 列表
docker network ls
查看网络列表
+ ← + + 02. 💻 Docker 快速安装软件 + + 04. 🥙 目录挂载 + + → +
build
和run
,很是麻烦。目录挂载解决以上问题
本文档课件配套 视频教程
bind mount
直接把宿主机目录映射到容器内,适合挂代码目录和配置文件。可挂到多个容器上volume
由容器创建和管理,创建在宿主机,所以删除容器不会丢失,官方推荐,更高效,Linux 文件系统,适合存储数据库数据。可挂到多个容器上tmpfs mount
适合存储临时文件,存宿主机内存中。不可多容器共享。文档参考:https://docs.docker.com/storage/ (opens new window)
bind mount
方式用绝对路径 -v D:/code:/app
volume
方式,只需要一个名字 -v db-data:/app
示例:
docker run -p 8080:8080 --name test-hello -v D:/code:/app -d test:v1
注意!
因为挂载后,容器里的代码就会替换为你本机的代码了,如果你代码目录没有
node_modules
目录,你需要在代码目录执行下npm install --registry=https://registry.npm.taobao.org
确保依赖库都已经安装,否则可能会提示“Error: Cannot find module ‘koa’”如果你的电脑没有安装 nodejs (opens new window),你需要安装一下才能执行上面的命令。
+ ← + + 03. 💽 制作自己的镜像 + + 05. 👨👦👦 多容器通信 + + → +
项目往往都不是独立运行的,需要数据库、缓存这些东西配合运作。
这节我们把前面的 Web 项目增加一个 Redis 依赖,多跑一个 Redis 容器,演示如何多容器之间的通信。
本文档课件配套 视频教程
要想多容器之间互通,从 Web 容器访问 Redis 容器,我们只需要把他们放到同个网络中就可以了。
文档参考:https://docs.docker.com/engine/reference/commandline/network/ (opens new window)
test-net
的网络:docker network create test-net
test-net
网络中,别名redis
docker run -d --name redis --network test-net --network-alias redis redis:latest
redis
的地址为网络别名docker run -p 8080:8080 --name test -v D:/test:/app --network test-net -d test:v1
http://localhost:8080/redis
容器终端查看数据是否一致
docker ps
查看当前运行中的容器
docker images
查看镜像列表
docker rm container-id
删除指定 id 的容器
docker stop/start container-id
停止/启动指定 id 的容器
docker rmi image-id
删除指定 id 的镜像
docker volume ls
查看 volume 列表
docker network ls
查看网络列表
+ ← + + 04. 🥙 目录挂载 + + 06. 🍁 Docker-Compose + + → +
在上节,我们运行了两个容器:Web 项目 + Redis
如果项目依赖更多的第三方软件,我们需要管理的容器就更加多,每个都要单独配置运行,指定网络。
这节,我们使用 docker-compose 把项目的多个服务集合到一起,一键运行。
本文档课件配套 视频教程
docker-compose
检查是否安装成功要把项目依赖的多个服务集合到一起,我们需要编写一个docker-compose.yml
文件,描述依赖哪些服务
参考文档:https://docs.docker.com/compose/ (opens new window)
version: "3.7"
+
+services:
+ app:
+ build: ./
+ ports:
+ - 80:8080
+ volumes:
+ - ./:/app
+ environment:
+ - TZ=Asia/Shanghai
+ redis:
+ image: redis:5.0.13
+ volumes:
+ - redis:/data
+ environment:
+ - TZ=Asia/Shanghai
+
+volumes:
+ redis:
+
容器默认时间不是北京时间,增加 TZ=Asia/Shanghai 可以改为北京时间
在docker-compose.yml
文件所在目录,执行:docker-compose up
就可以跑起来了。
命令参考:https://docs.docker.com/compose/reference/up/ (opens new window)
在后台运行只需要加一个 -d 参数docker-compose up -d
查看运行状态:docker-compose ps
停止运行:docker-compose stop
重启:docker-compose restart
重启单个服务:docker-compose restart service-name
进入容器命令行:docker-compose exec service-name sh
查看容器运行log:docker-compose logs [service-name]
+ ← + + 05. 👨👦👦 多容器通信 + + 07. 🚚 发布和部署 + + → +
镜像仓库用来存储我们 build 出来的“安装包”,Docker 官方提供了一个 镜像库 (opens new window),里面包含了大量镜像,基本各种软件所需依赖都有,要什么直接上去搜索。
我们也可以把自己 build 出来的镜像上传到 docker 提供的镜像库中,方便传播。
当然你也可以搭建自己的私有镜像库,或者使用国内各种大厂提供的镜像托管服务,例如:阿里云、腾讯云
本文档课件配套 视频教程
创建一个镜像库
命令行登录账号:docker login -u username
新建一个tag,名字必须跟你注册账号一样docker tag test:v1 username/test:v1
推上去docker push username/test:v1
部署试下docker run -dp 8080:8080 username/test:v1
version: "3.7"
+
+services:
+ app:
+# build: ./
+ image: helloguguji/test:v1
+ ports:
+ - 80:8080
+ volumes:
+ - ./:/app
+ environment:
+ - TZ=Asia/Shanghai
+ redis:
+ image: redis:5.0.13
+ volumes:
+ - redis:/data
+ environment:
+ - TZ=Asia/Shanghai
+
+volumes:
+ redis:
+
docker 官方的镜像托管有时候上传和下载都太慢了,如果你想要更快的速度,可以使用阿里云的免费镜像托管
+ ← + + 06. 🍁 Docker-Compose + + 08. 🎯 备份和迁移数据 + + → +
容器中的数据,如果没有用挂载目录,删除容器后就会丢失数据。
前面我们已经讲解了如何 挂载目录
如果你是用bind mount
直接把宿主机的目录挂进去容器,那迁移数据很方便,直接复制目录就好了
如果你是用volume
方式挂载的,由于数据是由容器创建和管理的,需要用特殊的方式把数据弄出来。
本文档课件配套 视频教程
备份:
导入:
运行一个 mongodb,创建一个名叫mongo-data
的 volume 指向容器的 /data 目录
docker run -p 27018:27017 --name mongo -v mongo-data:/data -d mongo:4.4
运行一个 Ubuntu 的容器,挂载mongo
容器的所有 volume,映射宿主机的 backup 目录到容器里面的 /backup 目录,然后运行 tar 命令把数据压缩打包
docker run --rm --volumes-from mongo -v d:/backup:/backup ubuntu tar cvf /backup/backup.tar /data/
最后你就可以拿着这个 backup.tar 文件去其他地方导入了。
docker run --rm --volumes-from mongo -v d:/backup:/backup ubuntu bash -c "cd /data/ && tar xvf /backup/backup.tar --strip 1"
注意,volumes-from 指定的是容器名字
strip 1 表示解压时去掉前面1层目录,因为压缩时包含了绝对路径
+ ← + + 07. 🚚 发布和部署 + + 2023.12.12-制作定制镜像 + + → +
docker run -it ubuntu:22.04 /bin/bash
+
apt-get update && apt-get upgrade
+
docker commit -m="geant4 installed" -a="OutisPKU" f87240cdb380 outispku/geant4:v1
+
docker run -it -v D:\BaiduSyncdisk\Postgraduate\Research\Geant4\Project:/home --name geant4_test outispku/geant4:v1 /bin/bash
+
其中-v D:\BaiduSyncdisk\Postgraduate\Research\Geant4\Project:/home
为将主机的目录映射到容器中
start.sh
#!/bin/bash
+
+# Source Geant4 environment scripts
+source /geant4/bin/geant4.sh
+source /geant4/share/Geant4/geant4make/geant4make.sh
+
+# 获取系统的线程数
+NUM_THREADS=$(expr $(nproc) - 1)
+
+cd /program
+
+# 构建命令
+CMD_TO_ADD="/run/numberOfThreads $NUM_THREADS"
+
+# 将命令添加到 run.mac 的第一行
+sed -i "1i $CMD_TO_ADD" ./run.mac
+
+# 定义目录路径
+BUILD_DIR="/program/build"
+
+# 检查目录是否存在
+if [ -d "$BUILD_DIR" ]; then
+ # 如果目录存在,删除它
+ rm -rf "$BUILD_DIR"
+fi
+
+# 创建新的目录
+mkdir -p "$BUILD_DIR"
+cd build
+cmake ..
+make -j $NUM_THREADS
+# PKU_Trim_Geant4 为程序名称(可执行文件),由CMakeLists.txt中的add_executable(PKU_Trim_Geant4 PKU_Trim_Geant4.cc)决定
+# 使用前请修改
+./PKU_Trim_Geant4 run.mac > /dev/null 2>>error.txt
+
Dockerfile
# 原单Geant4镜像
+FROM outispku/geant4:v1
+
+# 指定启动脚本
+COPY start.sh /
+RUN chmod +x /start.sh
+RUN mkdir program
+ENTRYPOINT ["/start.sh"]
+
docker build -t outispku/geant4:v2 .
+
docker run -v D:\FinFET:/program --name geant4_program outispku/geant4:v2
+
+ ← + + 08. 🎯 备份和迁移数据 +