计算机组成 - verilog 进阶
一、关于常量
1.1 负数
负数在 verilog 里是按照补码储存的,也就是说 ,但是需要注意的是,在比较的时候,是无符号的比较,也就是说 这个事情是对的。更离谱的是, 这件事也是对的,在表达式中运算的时候也是这样的。
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( |
二、关于数据类型
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; |
第一个问题,对于 a, 它是 {a [31], a [30], ... , a [0]},对于 b ,它是 {b [32], b [31], ... , b [1]}。
第二个问题,a 和 c 是有本质区别的,他们的本质区别就是最高位和最低位的先后顺序不同,进而,我们进行位选的时候,会有两个不同,一个是普通的位选,两者分别是这样的
a[31:24]; |
同样都是选取最高的字节。
另一个是在比较高端地位选中,有如下规范
big_vect[lsb_base_expr +: width_expr]; |
这种高端的位选有什么好处呢?是因为在普通的位选中,高位和低位都必须是常量,也就是说,没办法让类似循环一类的东西辅助位选。但是在这种方法中,位选基准位可以是变量,只要宽度是常量就可以了。
三 、关于语句
3.1 非阻塞赋值
非阻塞赋值其实也是顺序执行的,我们看一段代码
always @(posedge clk) begin |
执行的顺序是 (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 语句和过程块语句(指的是 initial 和 always)中。
块语句,分为 begin-end 代表的顺序块和 fork-join 代表的并行块,是不能用在 assign 语句里的。也就是一个 assign 语句没有办法完成对多个 wire 型的赋值。
条件分支语句,分为 if-else 和 case 两种类型,只能用在过程块语句中。注意,与 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 函数的感觉)。例如:
//一个定义交通灯开启时间的任务 |
而对于 function,其实本质就是一个包含了一个我们直观上不好给出值的寄存器(类似于数学上的函数),我们看到 verilog 里面函数一般用来算阶乘,其实如果有计算器,完全没必要用 function,如:
function factorial;//声明函数的同时也声明了一个与函数同名的寄存器 |
4.2 变量的作用范围
对于模块内的任务,模块里面的变量都是可以直接用的,所以经常出现任务没有输入端口的情况。就好像在 C 里面只要把栈数组开成全局,push()和 pop()就可以不用传数组指针的参了。那么为什么还要有输入端口这个设置呢?是因为可以更加精确调控,就好像 C 里面如果有两个栈数组,即使都开成全局数组,只要不想写两个 push(),就必须加入数组指针参量。
对于函数,不太清楚,只知道它必须至少有一个输入变量。