最近在学计算机基础课程,硬核到不仅仅是汇编,而是直接开始写硬件相关代码了!为了能够跟上课程进度,提前了解一些Verilog语法是很有必要的。
Verilog语法入门
简单介绍一下Verilog的作用:Verilog HDL(简称 Verilog )是一种硬件描述语言,用于数字电路的系统设计。可对算法级、门级、开关级等多种抽象设计层次进行建模。
其实Verilog和C语言很像,只不过在命名、使用上有一些特殊的地方,一开始写起来可能不太习惯。
好吧,要不我们直接来看一段代码?下面将会贴出一段Verilog代码,它定义了一个**4位的十进制计数器,每当计数到10就会输出一个溢出位并清零重新开始计数,**它有两个输入端口(一个输入让计数器复位、另一个输入可以让计数器+1);两个输出端口(一个输出当前计数,另一个则当超过4位计数时输出溢出位)。
知道了代码的作用之后,来看代码吧:
//ref:https://time.geekbang.org/column/article/543867 module counter( //端口定义 input reset_n, //复位端,低电平有效,让计数器清零 input clk, //输入时钟,每获得一个时钟信号,计数器+1 output [3:0] cnt, //4位的输出端口,输出当前的计数值 output cout //1位溢出端口,如果溢出则输出高电平(1) ); reg [3:0] cnt_r ; //4位的计数器寄存器 // 可以把下面这一段代码理解成while(条件) always@(posedge clk or negedge reset_n) begin if(!reset_n) begin //复位时,计时归0 cnt_r <= 4'b0000 ; // 非阻塞赋值 end else if (cnt_r==4'd9) begin //计时10个cycle时,计时归0 cnt_r <=4'b0000; // 非阻塞赋值 end else begin cnt_r <= cnt_r + 1'b1 ; //计数加1 // 非阻塞赋值 end end assign cout = (cnt_r==4'd9) ; //输出周期位 // 阻塞赋值 assign cnt = cnt_r ; //输出实时计时器 // 阻塞赋值 endmodule
其实上述代码可以看作一个“函数”,只不过在Verilog中被称为模块(module),这是最基本的设计单元。
一个模块以module+模块名开始,到endmodule结束,它主要包含两个部分:
端口IO定义、内部逻辑
端口IO定义
“端口”指的是模块的输入信号与输出信号,类似于函数参数。值得一提的是,与其他高级语言不同,这些参数是有方向性的,**需要注明输入和输出。**以上述代码为例,如:
module counter( //端口定义 input reset_n, //复位端,低电平有效,让计数器清零 input clk, //输入时钟,每获得一个时钟信号,计数器+1 output [3:0] cnt, //4位的输出端口,输出当前的计数值 output cout //1位溢出端口,如果溢出则输出高电平(1) );
上述的reset_n、clk、cnt以及cout即是端口IO定义,既定义了端口的名称,又定义了它们的输入输出属性。其中input标注的参数是输入信号,output标明的是输出信号。
顺带一提,Verilog主要有四种数据类型:线网型、寄存器型、常量、参数。
线网型(Wire)
线网型代表元器件之间的连线,是Verilog的缺省类型,上文代码中的reset_n、clk、cout、cnt均为线网型参数。其中上cnt的命名方式类似于数组,[3:0]表示该信号有4条线路。
在二进制计数中,单比特逻辑值只有“0”和“1”两种状态,在 Verilog 语言中,为了对电路了进行精确的建模又增加了两种逻辑状态(“X”和“Z”)。
逻辑0:表示低电平,表示GND(接地)。
逻辑1:表示高电平,表示VCC(接电源)。
逻辑X:表示未知,有可能是高电平,也有可能是低电平。
逻辑Z:表示高阻态,没有激励信号,悬空状态。
寄存器型(Reg)
寄存器型的信号(变量)是一个抽象的数据存储单元,它只能够在always和initial中赋值(后文再表)。上文代码中的cnt_r就表示一个4位的寄存器。注意,这里所说的位数都是对于二进制而言。
常量(Const)
常量又分为了整数型、实数型和字符串型。
整数型的命名方式为**+/- 位宽 进制 数字,位宽表示对应的二进制宽度,进制表示后跟数字的进制。**
例如,4’b1010是二进制写法等同于十进制写法的 4’d10。
实数型可以用科学表示法和十进制写法来表示,例如5.0,2.11e2。
字符串型实质上还是无符号整数,例如:
output [8*4-1:0] ca //定义了一个为输出的32位线网型变量ca assign ca = "Andy"; // 给它赋值"Andy"
其实ca中的值就是32’h416E6479,即Andy字符串的ASCII码。
参数(param)
参数用关键词parameter表示,有点儿像宏定义,如下:
parameter width = 10'd48 ; parameter mem_size = width * 10 ;
参数只能被赋值一次(如果采用实例化可以更改它的值,这里暂时不涉及)。局部变量用localparam关键词表示,它的作用域尽在本模块中使用,且值无法改变。
内部逻辑
内部功能逻辑是指模块中对输入输出信号(变量)的各种操作,下面将介绍一些基础的内部逻辑。
always
之前也提到,always可以简单的看作while(event),即当事件发生的时候,就执行always语句。根据事件event中是否包含时序事件,always分为两种逻辑:时序逻辑和组合逻辑。
时序逻辑表示always只会在对应的信号出现边沿事件时,才会执行对应的代码。如果有多个信号以or连接,这些对应的事件被称为敏感事件列表,如下:
always @ (edge eventa or edge eventb) begin [multiple statements] end
其中edge可以是negedge(下降沿)和posedge(上升沿)。
组合逻辑通常用来监听信号水平事件的发生。当敏感信号出现电平的变化时就会执行always语句。例如always @(a or b or c),a、b、c均为变量,当其中一个发生变化时都会执行后续代码。例如:
always@(a,b) begin out = a&b; end
上述代码中always的敏感事件列表为a b发生电平变化,即当ab其中任何一个发生变化,都会赋予out新值。
有的时候敏感事件变量较多,一个一个写比较麻烦,还有一种特殊的用法,always @(*),此时的敏感列表是module中所有具有形参的信号,其中任何信号发生变化都会触发always语句。上述代码就可以写成:
always@(*) begin out = a&b; end
initial
initial语句只执行一次,常用于产生仿真测试信号,或对某些变量赋初值。
如果module中有多个initial语句,这些语句之间是相互独立的,都是从0时刻开始并行执行,并没有顺序之分。
阻塞/非阻塞赋值
如果现在跳回去看之前的代码,会看到这样的注释:
…… cnt_r <= cnt_r + 1'b1 ; //计数加1 // 非阻塞赋值 …… assign cout = (cnt_r==4'd9) ; //输出周期位 // 阻塞赋值 assign cnt = cnt_r ; //输出实时计时器 // 阻塞赋值 ……
可以发现对变量进行赋值时有两种方法:=(阻塞赋值)和<+(非阻塞赋值)。
两者的区别就在于阻塞赋值会立即计算右手语句值(RHS)并立即赋值给左手语句(LHS), 而且这行代码之后的语句会被阻塞,只有它执行完毕之后才能够继续进行。
而非阻塞赋值将赋值分为两个步骤:计算右边RHS,更新左边(LHS),并且其他语句不会被其所阻塞。非阻塞赋值只能用于对寄存器信号进行赋值。
需要注意的是,阻塞赋值可能会造成数据竞争,例如实现在上升沿时交换两个寄存器的值:
always @(posedge clk) begin a = b ; end always @(posedge clk) begin b = a; end
如果采用上述阻塞赋值,最后a b的值都会相等,而不是我们想要的交换。但是如果使用非阻塞赋值就没有这个顾虑:
always @(posedge clk) begin a <= b ; end always @(posedge clk) begin b <= a; end
算术运算与逻辑运算
Verilog语法支持常规的算术运算和逻辑运算,更详细的介绍和例子可以参考这篇博客:Verilog 表达式。
在线学习网站
要记住,学习一门语言的最佳方式是实践!实践!实践!
换句话说,你只需要不停地敲代码——遇到问题——百度/谷歌——解决问题——总结归纳,你的语言水平自然而然就会得到提升。
这里推荐大家两个网站,一个是菜鸟教程的中文Verilog教程,对于新手来说,花几个小时看完这些就差不多入门了。
**另外一个就是Verilog在线执行网站+英文教程:**https://hdlbits.01xz.net/wiki/Step_one
免去了部署Verilog HDL环境的繁杂步骤,就像刷Leetcode那样学习Verilog语法,还自带详细的英文教程!
总结
回答两个问题。
为什么很多特定算法,用 Verilog 设计并且硬件化之后,要比用软件实现的运算速度快很多?
简单的说越是底层的东西越快,效率越高,因为中间步骤少!从另外一个方面来说,硬件化是一种定制化,在某一方面具有特别突出的能力,例如CPU软解码和GPU硬解码,两者的效率天差地别。
推荐阅读,如果你想进一步了解高级语言是如何一步一步到具体门电路的过程,可参阅:https://zhuanlan.zhihu.com/p/362950660
既然用 Verilog 很容易就可以设计出芯片的数字电路,为什么我们国家还没有完全自主可控的高端 CPU 呢?
CPU设计我们是世界顶尖水平,但是没有机器造不出来,西方国家根本不敢给我们,因为我们如果拿到了,以后CPU基本没有他们的事情了。这就是所谓的技术壁垒,另一种形式的“铁幕”。
module alu(a, b, cin, sel, y); input [7:0] a, b; input cin; input [3:0] sel; output [7:0] y; reg [7:0] y; reg [7:0] arithval; reg [7:0] logicval; // 算术执行单元 always @(a or b or cin or sel) begin case (sel[2:0]) 3'b000 : arithval = a; 3'b001 : arithval = a + 1; 3'b010 : arithval = a - 1; 3'b011 : arithval = b; 3'b100 : arithval = b + 1; 3'b101 : arithval = b - 1; 3'b110 : arithval = a + b; default : arithval = a + b + cin; endcase end // 逻辑处理单元 always @(a or b or sel) begin case (sel[2:0]) 3'b000 : logicval = ~a; 3'b001 : logicval = ~b; 3'b010 : logicval = a & b; 3'b011 : logicval = a | b; 3'b100 : logicval = ~((a & b)); 3'b101 : logicval = ~((a | b)); 3'b110 : logicval = a ^ b; default : logicval = ~(a ^ b); endcase end // 输出选择单元 always @(arithval or logicval or sel) begin case (sel[3]) 1'b0 : y = arithval; default : y = logicval; endcase end endmodule
参考链接
https://time.geekbang.org/column/article/543867
https://hdlbits.01xz.net/wiki/Step_one
https://blog.csdn.net/weixin_42705678/article/details/120904738
https://blog.csdn.net/Reborn_Lee/article/details/107052261
efault : y = logicval;
endcase
end
endmodule
# 参考链接 https://time.geekbang.org/column/article/543867 https://hdlbits.01xz.net/wiki/Step_one https://blog.csdn.net/weixin_42705678/article/details/120904738 https://blog.csdn.net/Reborn_Lee/article/details/107052261 https://blog.csdn.net/Lee_tr/article/details/122488970