0%

计算机组成-verilog进阶

Verilog 语言细节

一、关于常量

1.1 负数

负数在verilog里是按照补码储存的,也就是说 $-2’b1 = 11$,但是需要注意的是,在比较的时候,是无符号的比较,也就是说 $-2’b1 > 2’b0$ 这个事情是对的。更离谱的是,$-2’b1 == 2’d3$ 这件事也是对的,在表达式中运算的时候也是这样的。

1.2 参数

parameter的提出我一开始以为他是多余的,因为define可以代替,后来这个跟C不一样,每次用define定义的宏,宏前面都是需要带 “ ” 符号的,所以写起来太不方便了。常见的还是用parameter比较方便,但是因为define还是有用的,因为parameter的右式必须是符合表达式规则的,但是define就没有这个要求。

1.3 数据截断

verilog里面的数的位宽极为重要,这是因为其实数本身不是真的那个特别抽象的,可以用来描述世界上一切事物的数字,而是更狭隘的只一簇数据线上的高低电平,对于每个1 + 1,在verilog中都是两个输入经过一个加法器,然后得到一个输出,是有具体硬件基础的。所以位宽及其重要。

首先强调一下位宽概念,位宽说的是把这个数用二进制表示后的位数,所以即使是写成 3’dx 这种最大值也不是十进制的999(十进制最大的三位数),而是十进制的7(二进制最大的三位数)。

我认为比较容易犯的错误,就是是数据截断,比如对于一个四位宽数与另一个四位宽数求和,我们知道四位宽数最大是十进制的15,在数学上,他们的和最大是30,但是在verilog,它还是15,因为输出的位宽是两个运算数的最小值,所以还是四位宽,最大是15。

1.4 关于移位

据说>>>这个符号是算数移位符,而这个>>符号是逻辑移位符,然后我用程序进行测试,发现只有这中种情况进行算数右移

module ALU(
    input [4:0] a, // a = 5'b11100
	input [4:0] b, // b = 5'd2
	output reg [4:0] c,
	output reg [4:0] d,
	output reg [4:0] e,
	output reg [4:0] f
    );
	
	always @(*) begin
		c = a >> b; // c = 5'b00111
		d = a >>> b; // d = 5'b00111
		e = $signed(a) >>> b; // e = 5'b11111
		f = $signed(a) >> b; // f = 5'b00111
	end
	
endmodule

二、关于数据类型

2.1 右式左式

对于assign语句,wire和reg都可以作为其左式和右式,但是对于always语句,只有reg变量能作为他的左式。

2.2 分线器

位选运算符[ ] ,可以辅助选出需要的位,在预习中,有一道整除2的幂次的题目,就可以用位选后面的信号实现,远比lowbit的算法实现要容易而且更加硬件

{ }位拼接运算符,实现的就是并线器的功能,在处理减法器的时候,也有过高光表现。

2.3 字符串

字符串或者字符(在verilog里忙不区分)是用 双引号“ ” 来表示的,其实它的逻辑很简单,就是每个字母就是对应八位的一个ASCII值,如果是字符串,那么就是8的n倍的位宽。

"AB" = 16'b01000001_01000010

2.4 矢量声明

矢量声明的时候,比如说

wire [31:0] a;
wire [32:1] b;
wire [0:31] c;

第一个问题,对于 a, 它是{ a[31], a[30], … , a[0] },对于 b ,它是 { b[32], b[31], … , b[1] }。

第二个问题,a 和 c是有本质区别的,他们的本质区别就是最高位和最低位的先后顺序不同,进而,我们进行位选的时候,会有两个不同,一个是普通的位选,两者分别是这样的

a[31:24]; 
c[0:8];

同样都是选取最高的字节。

另一个是在比较高端地位选中,有如下规范

big_vect[lsb_base_expr +: width_expr];
big_vect[msb_base_expr -: width_expr];//适用于a这样的向量

little_vect[msb_base_expr +: width_expr];
little_vect[lsb_base_expr -: width_expr];//适用于c这样的向量

//a[0 +: 4] 就是 a[3:0]
//a[7 -: 4] 就是 a[7:4]
//c[0 +: 4] 就是 c[0:3]
//c[7 -: 4] 就是 c[4:7]

这种高端的位选有什么好处呢?是因为在普通的位选中,高位和低位都必须是常量,也就是说,没办法让类似循环一类的东西辅助位选。但是在这种方法中,位选基准位可以是变量,只要宽度是常量就可以了。


三 、关于语句

3.1 非阻塞赋值

非阻塞赋值其实也是顺序执行的,我们看一段代码

always @(posedge clk) begin
    a <= b; //(1) <= (2)
    b <= c; //(3) <= (4)
    c <= a; //(5) <= (6)
end

执行的顺序是 (2) -> (4) -> (6) -> (1) -> (3) -> (5)。只是不再是 (2) -> (1) -> (4) -> (3) -> (6) -> (5) 了。可以看出这种先计算所有的右式,在个所有的左式赋值的方法,是跟寄存器的电路逻辑很像的,寄存器在之前就已经收集到到了原来的值(对应左式),但只有在边沿处才将存储的值更新(对应右式),所以应该先对所有原来的值取样,然后在更新。

可以说取样就是对寄存器早已存储待更新值的一种模拟。

综上,非阻塞赋值并不是对并行的模拟,所以也就不存在与begin - end顺序块的理论冲突。

3.2 关于语句的使用范围

对于各种语句,使用范围是没有那么自由的,这跟在C中完全不同,比如在C中,我可以在任何一个地方(main,函数里,define)里敲 a = b,但是在verilog里,想这么敲,只有有限的几种方法,比如 assign a = b(assign里面可以对wire型变量赋值)。下面列一下各种语句的使用范围。

赋值语句,分为阻塞赋值语句非阻塞赋值语句,不能自己单独出现,只能出现assign语句过程块语句(指的是 initialalways)中。

块语句,分为begin-end代表的顺序块和fork-join代表的并行块,是不能用在assign语句里的。也就是一个assign语句没有办法完成对多个wire型的赋值。

条件分支语句,分为if-elsecase两种类型,只能用在过程块语句中。注意,与C语言不同,条件语句不能看成独立的一个大句子,所以必须写在块语句之中,即必须写在begin-end之中。此外,对于case,需要明确在冒号之后理论上只能写一条语句,想要写多条,就必须要用begin-end写成一个块语句。另外,case虽然是分支语句,但是它执行过程中是会按照顺序执行的,所以如果两个case之间有重叠的情况,是会先执行第一个case的,case是一般不会有重复的,但是casez会。

循环语句,跟条件分支语句一样,只能用在过程块语句中。后来又看到生成块语句,循环语句和条件分支语句也可用于生成块语句

3.3 再论过程块

关于过程块,有两种说法,一种说initial和always引导的语句块叫做过程块(破案了,这个叫结构说明语句),另一种说法是begin-end引导的叫做过程块,但是其实这两种是差不多的,如果不考虑嵌套情况下,只有initial和always可以使用begin和end,如果考虑嵌套,确实begin-end自己也可以使用begin-end。所以把握精神最重要,就是不能随便用。

对于begin-end引导的块语句,只要给他加上姓名(命名块),里面是可以声明变量的,命名块是设计层次的一部分,命名块中声明的变量可以通过层次名引用进行访问。

3.4 从生成块看硬件描述逻辑

我初学的时候,一直在想,verilog到底跟C有什么不同,现在越学越觉得,verilog与C有什么相同?他们两个好比姐妹种,只是外形相似,而内在完全不同。

而他们完全不同的原因,就是因为他们干的事情是不同的。对于面向过程的C,他描述的一种算法,是加工数据的一个过程,就好像一本小说,最重要的是在时间上流动的情节。对于硬件描述性语言的verilog,他描述的一块电路板,是元器件间的链接。就好像一幅画。确实一本小说里面可以有环境描写和人物介绍(变量声明),但是这些东西都是为了算法服务的。同样,作画的时候,我们说要画一个在草地上奔跑的姑娘,我们要画一百颗星星,我们要用白色和红色勾兑出粉色(分支判断,生成块),但是最后的成品依然是静态的一幅画。所有的代码都是为了描述一个硬件,这就是宗旨。

所以生成块如果用面向过程的角度来看,是不可理解的,生成代码对于C而言,就是生成算法,C可以接受循环,这代表着在时间上重复干一件事,但是绝对不能接受生成算法。但是对于verilog,循化才是不能接受的事情,画在纸上的姑娘可以哭,可以笑,但是由哭变笑,再重复100遍。但是对于生成代码,是十分不自然的事情。但是我们可以接受纸上同时有100个笑着的小姑娘。

我用logisim的时候,觉得这就是硬件描述软件,但是为什么在verilog中花了这么长时间才意识到这件事情。我觉得是因为verilog在简化描述的过程,在logisim的时候,我想要一个16个与门,我就得自己搭16次,连48次线,但是在verilog中,我可以用循环生成,在logisim我想实现一个有分支判断的电路,就要自己搭MUX,但是在这里,if-else就可以了。尽管画作是静态的,但是作画的过程是动态的,正是这里的动态让我产生了疑惑。此外,verilog不仅是为了描述硬件,他还描述测试平台,而测试平台显然是动态的,所以有些语句是为了测试平台写的,而不是为了描述硬件。


四、任务和函数

4.1 跟C的区别

可以说,C的函数的范围是要比数学定义下的函数要广的,数学函数是给定输入,就会得到输出。但是C的函数更像是一段可复用的代码,结合C面向过程的特点,其实C的函数是就是一段可以复用的算法过程

相对于C,verilog的定义就很清楚,task指的是一种可复用的复杂元器件,或者一个测试过程(一般是行为级描述,所有有些像C函数的感觉)。例如:

//一个定义交通灯开启时间的任务
task light;
	//端口声明部分和变量声明部分
	output color;
	input [31:0] tics;
	//语句部分
	begin
		repeat(tics) @(posedge clk);
		color = off;
	end	
endtask

而对于function,其实本质就是一个包含了一个我们直观上不好给出值的寄存器(类似于数学上的函数),我们看到verilog里面函数一般用来算阶乘,其实如果有计算器,完全没必要用function,如:

function factorial;//声明函数的同时也声明了一个与函数同名的寄存器
    //端口声明部分和变量声明部分
	input [31:0] operand;
	//语句部分
	begin
		factorial = 1;
		for(integer i = 2; i <= operand; i = i + 1)
			factorial = i * factorial;
	end
endfaction

4.2 变量的作用范围

对于模块内的任务,模块里面的变量都是可以直接用的,所以经常出现任务没有输入端口的情况。就好像在C里面只要把栈数组开成全局,push()和pop()就可以不用传数组指针的参了。那么为什么还要有输入端口这个设置呢?是因为可以更加精确调控,就好像C里面如果有两个栈数组,即使都开成全局数组,只要不想写两个push(),就必须加入数组指针参量。

对于函数,不太清楚,只知道它必须至少有一个输入变量。