当前位置: 代码迷 >> 综合 >> [2021-07-18]Verilog HDL语法总结
  详细解决方案

[2021-07-18]Verilog HDL语法总结

热度:141   发布时间:2023-10-17 23:51:40.0

目录

1.引言

2.模块(block)

3.常量、数据类型、运算符

(1)常量

        1)数字

        2)x和z值

(2)数据类型

        1)wire型

        2)reg型

        3)参数型

(3)运算符

        1)算术运算符

        2)赋值运算符

        3)关系运算符

        4)逻辑运算符

        5)

条件运算符

        6)等式运算符

        7)移位运算符

        8)拼接运算符

         9)指数

        10)缩减运算符

        11)运算符优先级排序

4.常用关键词 块语句 生成块(未完待续)

        (1)always 

        (2)initial

        (3)assign

        (4)条件语句if_else

        (5)case语句

        (6)顺序块begin-end

        (7)系统任务

        (8)预编译处理语句


1.引言

        本篇内容是本人对于学习Verilog HDL语法过程中的总结,我的预期是将内容写的细一点,但作为一个初学者难免有所纰漏亦或是逻辑问题,所以会对内容进行长时间的修正、调整和补充。

        首先,Verilog是什么?是一种硬件描述语言( HDL,Hardware Description Language)。

                   描述的是什么?描述的是数字电路或数字系统的模型。

        个人理解就是将数字电路(或系统)的模型,转化为编程语言的形式,将其描述出来。

        我们编的不只是程序,而是可以具象化将一块一块的电路构造好后,连接起来,共同实现所需功能。(我们编写的也可能会是我们的梦想)

        在这之中,模型也分为很多层次:

Verilog语言对应的模型类型
描述级别 介绍
1.系统级(system-level) 后续补充
2.算法级(algorithm-level) 后续补充

3.RTL级(register transfer level)

描述如何控制和处理各类数据在寄存器之间的流动。

4.门级(gate-level)

描述逻辑门之间的连接
5.开关级(switch-level) 描述三极管和储存节点之间的连接

以上1~3都属于行为描述,1~3中只有RTL与逻辑电路有明确的对应关系。

现在明确了 Verilog是可以描述出整个电路系统,用一个不太恰当的比喻,就像我们知道了用砖,水泥或者钢筋、混凝土可以搭出房子一样;而房子是一间一间,一层一层盖起来的,复杂电路系统则是基于模块,一个一个配合共同实现功能的,每一个模块又可以由一个个子模块构成。

        因此,下一节对模块进行简要介绍。

2.模块(block)

        我个人理解:模块是实现特定功能的基本单元;我承认这句话看起来像是一句废话,还是那个不太恰当的比喻,就好像盖房子,用砖和水泥给自己盖一个新家用于生活,那你屋子里至少需要有厨房,卧室,客厅,卫生间等等,这些特定功能的基本单元。

        模块如何用Verilog定义呢?

//模块定义 模块名
module <module name>(CLK,RSTB,……DOUT);input wire CLK;        //时钟
input wire RSTB;       //复位下降沿有效……output wire DOUT;      //输出信号//参数
parameter WIDTH=8;//模块内容
………………
………………//实例化 实例化命名
example example_inst(.CLK(CLK),.RSTB(RSTB),.DOUT(DOUT)//);endmodule

本人目前接触到的模块有,加法器,选择器,触发器,译码器,FIFO,锁存器(这个慎用)等。

以下以一个选择器作为例子。实现功能:

模块有三个输入分别为 in_1,in_2,sel

一个输出out

输入sel为1 模块out = in_1输入的值;

输入sel为0 模块out = in_2。

RTL文件:

//模块开始 模块名
module mux2_1
(   //输入输出列表(注意,除了列表结尾,每行后面都是逗号)//类型 数据类型[位宽] 变量名,input wire [0:0] in_1, 	//input signal 1input wire 		 in_2,	//input signal 2input wire       sel ,	//select signaloutput reg       out	//output//这里out后面不加逗号 直接括号分号结束
);
/*定义模块方法2
module mux2_1(in_1,in_2,out
);
input wire [0:0] in_1; 	//input signal 1
input wire 		 in_2;	//input signal 2
input wire       sel;	//select signaloutput reg       out;
*///模块内容
//always表示块,块内的内容一直运行,后面(*)表示触发条件,即块内的变量sel、out只要有改变,
//就会运行块内语句
always@(*)if(sel == 1'b1)//这里一手简单的判断语句out = in_1;else 	out = in_2;
//快乐的时间就是这么短暂,模块结束了
endmodule

Test_bench文件实现功能:

每隔10us就给RTL文件中的 in_1,in_2,sel,赋值一个二进制随机数0或者1;

可以通过quartus和modelsim联合仿真观察out输出有没有严格实现功能

//时间尺度  单位时间间隔/时间精度
`timescale 1us/1us
//模块开始  输入输出列表为空
module tb_mux2_1();
//定义寄存器型变量
reg		in_1;
reg 	in_2;
reg		sel;
//定义线型变量
wire 	out;
//初始化块,注意这个初始化初学阶段只能用于Test_bench之中跑仿真用,千万不能写在rtl中
initialbeginin_1 <= 1'b0;in_2 <= 1'b0;sel  <= 1'b0;end
//always块没有触发条件就代表一直执行 #10表示延迟10*单位时间间隔的时间 
// %2表示除以2的余数    {$random}%2表示产生随机数除以2求余数 
//                                限定产生的随机数<2 即0和1
always  #10 in_1 <=	{$random} % 2;
always  #10 in_2 <=	{$random} % 2;
always  #10 sel  <=	{$random} % 2;
//
initialbegin//            1e-9s	==  ns$timeformat(-6,0,"us",6);//$monitor(参数列表)用于监控和输出参数列表个参数的值//写法类似于C语言中的printf。$monitor("@time %t:in_1 = %b sel=%b out=%b",$time,in_1,in_2,sel,out);end//实例化,将rtl文件和test_bench文件之间的变量连接起来,
//连接起来后就可以通过test_bench文件给rtl文件中的输入赋值,来测试模块功能能否实现
mux2_1	mux2_1_inst
(.in_1(in_1),	//input signal 1.in_2(in_2),	//input signal 2.sel(sel),		//select signal.out(out)	//output
);
//模块结束
endmodule

以上代码是跟哔哩哔哩上野火FPGA 火哥学的,他的视频讲得很细,网址我附在结尾,如果以上代码或其他内容侵权请联系我,我删。

对了,鲁迅先生说:每个文件内最好只写一个模块,一种功能。

3.常量、数据类型、运算符

(1)常量

        1)数字

表示形式

二进制整数:b或B

八进制整数:o或O

十进制整数:d或D

十六进制整数:h或H

直接举例:

位宽'进制+数字
//例如
8'b0001_0010;//8位二进制数 下划线是为了方便看,不影响实际值,等价于8'b00010010
//注意下划线只能放在数字部分的中间如16'b0001_0000_0010_0100
//不能放在进制和数字之间,这样是错误的,如8'b_00010000
8'h80;//8位十六进制数 转换为二进制为1000_0000;

        2)x和z值

在数字电路中,x代表不定值,z代表高阻态值。

这个我还没用过,就不过多介绍,先放在这里,若果后续有接触到我会进行补充。

(2)数据类型

        就我现在所学,目前常用的就是wire型,reg型以及parameter型。(如果后面我所学有涉及到其他的我会补充)

        1)wire型

格式:

wire [位宽范围] 变量1,变量2……;
//例如
wire [3:0] a,b;//定义位宽为4的 两个wire型变量 a和b。
wire [0:0] c;//定义位宽为1的 一个wire型变量 c。
wire       d;//定义位宽为1的 一个wire型变量 d。
  • wire型数据大多用来表示用assign关键字指定的被赋值的信号;
  • 在模块中,输入输出信号没有特别定义的话,默认为wire型;
  • wire型信号可以用做任何方程式的输入,也可以用作assign语句或实例化元件的输出;
  • 如果不指定位宽的话,默认为1。

        2)reg型

格式1:

reg [位宽范围] 变量1,变量2……;
//例如
reg [4:1] a,b;//定义位宽为4的 两个reg型变量 a和b。
reg [0:0] c;//定义位宽为1的 一个reg型变量 c。
reg       d;//定义位宽为1的 一个reg型变量 d。
reg [4:0] e;
always(posedge clk or negedge rst)begin
if(~rst) e <= 4'b0;
elsee <= {1,2,3,4};
end
//e[0]=4,e[1]=3,e[2]=2,e[3]=1;
  • reg型数据初始值默认为x,即不定值。
  • reg型数据大多用来表示用always块内的被赋值的信号,常代表触发器;
  • always块中,被赋值的每一个信号都必须为reg型;
  • 如果不指定位宽的话,默认为1。

格式2:用于定义memory型的变量

reg [位宽-1:0]  memory名  [深度-1:0];
例如:
parameter FIFO_WIDTH = 8;
parameter FIFO_DEPTH = 8;reg [FIFO_WIDTH-1 : 0] FIFO [FIFO_DEPTH-1:0]; 

        3)参数型

parameter 参数名=参数值;
//例如
parameter FIFO_DEPTH = 8;//定义FIFO_DEPTH为8
parameter FIFO_WIDTH = 8;
  • parameter型数据常用于定义位宽,FIFO深度,以及延迟时间

(3)运算符

        1)算术运算符

算术运算符(parameter a=8,  parameter b=2)
符号 使用示例 结果(十进制) 描述
+ a+b 10 相加
- a-b 6 相减
* a*b 16 相乘
/ a/b 4 相除
% 8%2 0 相除求余数

        2)赋值运算符

赋值运算符(reg [3:0] a;wire [3:0] b;)
符号 使用示例 描述
= b = a 阻塞赋值
<= a <= b; 非阻塞赋值

阻塞赋值:

  • 赋值语句执行完后,块才结束;
  • 上表中b的值在赋值语句执行完了以后,立刻改变;

例如:

wire a;
wire b;
//连续赋值语句
assign a = b;

非阻塞赋值:

  • 在语句块中,表格中的语句a <= b;赋值后不会立即改变a的值,为块内后续语句使用;
  • 块结束后,a的值才会完成复制操作,a的值才会改变;
  • 编写可综合的时序逻辑模块时,这是最常用的赋值方法。

例如:

reg  [3:0] a;
reg  [3:0] b;
reg  [3:0] c;always begina <= b;c <= a;end
/*
打个栗子:
初始值
a = 4'b0001;
b = 4'b0010;
c = 4'b0100;在执行了上面的always语句块后
a = 4'b0010;
b = 4'b0010;
c = 4'b0001;
在执行过程中,a的值不会立刻变为b的值,而是继续进行c <= a 
将a的初值4'b0001赋给c,同样的此时c的值也不会立刻变化的,
而是到end结束后,a和c都完成赋值。
*/

        3)关系运算符

关系运算符
符号 使用示例 结果(十进制) 描述
> 4>3 1 大于
< 4<3 0 小于
>= 4>=3 1 大于等于
<= 4<=3 0 小于等于

注意这里的小于等于长的虽然和非阻塞赋值中(<=)一样,但是实际含义完全不一样。

关系运算符的使用中,如果关系成立如4>3,那就返回1,如果关系不成立如4<=3, 那就返回0;

会经常用于for循环,if判断的话我目前还没用过,后续如果用过我来补充例如:

integer   i;
reg       a;
reg [7:0] b;
for(i=0;i<8;i=i+1) a=b[i];

        4)逻辑运算符

逻辑运算符
符号 使用示例 结果(十进制) 描述
&& 1&&1 1 逻辑与
|| 0||0 0 逻辑或
! !1 0 逻辑非

真值表:

逻辑运算真值表
a b a&&b a||b !a !b

注意:只要a是不为0的任何整数就是真。

5)位运算符

位运算符
符号 使用示例 结果(十进制) 描述
~ ~4'b0110 4'b1001 按位取反
^ 4'b0010^4'b0100 4'b0110 按位异或
^~ 4'b0010^~4'b0100 4‘b1001 按位同或(异或非)
& 4'b1100&4'b0101 4'b0100 按位与
| 4'0010||4'b1010 4'b1010 按位或

取反运算符真值表

~ 结果
0 1
1 0
x x

按位异或真值表

^ 0 1 x
0 0 1 x
1 1 0 x
x x x x
按位同或真值表
^~ 0 1 x
0 1 0 x
1 0 1 x
x x x x
按位与真值表
& 0 1 x
0 0 0 0
1 0 1 x
x 0 x x
按位或真值表
| 0 1 x
0 0 1 x
1 1 1 1
x x 1 x

        5)

条件运算符

判断真假? 值1:值2(判断真假,返回值1,返回值2)。

举个例子吧

//这里如果b大于c ,a=1 如果 b小于等于c,a=0 
a = (b>c)? 1:0; 

        6)等式运算符

等式运算符
符号 使用示例 结果(十进制) 描述
== 1==0 0 等于
!= 1!=0 1 不等于
=== 1===1 1 等于
!== 1!==1 0 不等于

其中==和===的区别主要区别是在对不定值x和高阻值z的判断上如下表

== 0 1 x z
0 1 0 x x
1 0 1 x x
x x x x x
z x x x x
=== 0 1 x z
0 1 0 0 0
1 0 1 0 0
x 0 0 1 0
z 0 0 0 1

        7)移位运算符

        这个直接给例子吧:

4'b0010 >> 1 = 4'b0001;
4'b0100 << 1 = 4'b1000;
4'b0101 << 2 = 5'b10100;
4'b0101 >> 4 = 4'b0000; 
1<<3 =32'b1000;//默认位宽32位。

        8)拼接运算符

        上例子:

{a[0],b[2],a[2],b[1]}
//组合成一个4位宽的数 最高位为a[0]的值,次高位为b[2]的值,次低位为a[2]的值,最低位为b[1]的值
{1'b0,1'b1,a[1],a[3]}
//组合成一个4位宽的数 最高位为0,次高位为1,次低位为a[1]的值,最低位为a[3]的值 
{5{1'b1}}
//组合成一个5位宽的数 5'b11111
{4{a[1]}}
//组合成一个4位宽的数 4位都是a[1]的值

         9)指数

例如:

2**2 //2的2次方
2**3 //2的3次方

        10)缩减运算符

//缩减运算符符号与位运算符符号相同
&1001 // 等价于((1&0)&0&1)   = 0
|1001 // 等价于 ((1|0)|0)|1  = 1
^0111 //等价于  ((0^1)^1)^1  = 1

        11)运算符优先级排序

一般来说优先级排序由 单目运算符>双目运算符>三目运算符。

什么是单目,双目,三目?单目就是操作数为1,如~a,!a,^a,同理操作数为2即为双目,如a&&b,三目应该就只有 ?:。

优先级细分的话从上到下最高到最低:

!    ~
/   *   %
+    -
<<    >>
>=    >    <    <=
==    !=    ===    !==
&
^    ^~
|
&&
||
?:

4.常用关键词 块语句 生成块(未完待续)

        这一部分的内容我先写自己经常遇到的,属于有些浅显的认识了,还没有完全的形成体系。

        首先,先要认识到,当仿真开始的时候,像always,assign,initial这些声明语句他们不是按照Verilog程序的实际书写顺序依次执行的,而是在仿真开始的时候,就并行执行。在这些声明语句块内根据具体情况有并行,有顺序执行,但切记,这些声明语句他们之间是没有先后顺序,并行执行的。

        (1)always 

        always顾名思义,在仿真一开始,只要满足触发条件,就一直在执行,直到仿真结束,块内的执行是按照顺序执行完后,继续从always的开头执行。

always <触发条件> <语句>
//一般来说,always的使用都要加上触发条件(也就是时序控制),防止产生死锁//常用的例子有:
//例1:异步FIFO中的时钟同步
always@(posedge WCLK or negedge WRSTB)begin//这里if(!WRSTB){WP2, WP1} <= 8'h0;else{WP2, WP1} <= {WP1, RGRAY};
end
//@(posedge WCLK or negedge WRSTB) 表示触发条件为 WCLK的上升沿或WRSTB的下降沿出现都是触发条件
//一旦满足触发条件,就开始执行always块内的语句
//这一段代码的作用是用于异步FIFO时钟同步,打两拍。//例2:选择器
always@(*)if(sel == 1'b1)out = in_1;else 	out = in_2;
endmodule
//@(*)表示触发条件将块内输入变量sel、in_1、in_2放入敏感列表,只要有一个变化,就会触发
//还可以表示成always@(sel or in_1 or in_2),可以达到类似的效果//例3:仿真时,test_bench时钟信号的产生
always	#10 sys_clk = ~sys_clk;
//#10表示延迟10个单位时间间隔,如果时间单位设置的是1ns 这里#10 表示延迟10ns后执行块内语句
//如果sys_clk初值为低电平,10ns后,sys_clk低电平变高电平,再10ns后跳转为低电平,如此循环
//我在实际的程序中`timescale 1ns/1ns ,即时间尺度为1ns 时间精度也为1ns,
//#10即代表延迟10ns,例3即产生一个周期为20ns的方波,

        (2)initial

        鄙人不才,initial语句只在做仿真写test_bench的时候才会用到,与always相同的是,initial在仿真开始的时候就会执行,与always不同的是,initial在仿真开始后只执行一次,所以常用来在testbench中给变量赋初值。

initial begin语句1;语句2;····
end//例1 在testbench里面给时钟和复位信号赋初值
initial begin sys_clk = 1'b1;sys_rst_n = 1'b0;#20                //延迟20个时间单位后顺序执行块内后续语句sys_rst_n = 1'b1;end

        (3)assign

        assign是连续赋值声明语句,必须放在任何initial,always块外,assign与initial,always并行执行,assign会执行到仿真结束。

assign <延时> 赋值语句
wire [变量位宽-1:0] <延时>  赋值语句
//以上两种都是连续赋值语句,效果相同,常用于将寄存器变量的值通过连线,赋值给wire型变量//例1 二进制码转格雷码
assign RGRAY = RBIN^(RBIN>>1);
wire [3:0] RGRAY = RBIN^(RBIN>>1);

        (4)条件语句if_else

        条件语句常用于always块内,就是如果条件成立,执行指定语句,如果条件不成立执行另外指定的语句。当使用if_else语句时,如果能考虑全所有的情况,就考虑全,不能的话最好把一些考虑不到的情况再加一个else进行一个稳妥的赋值处理,避免产生锁存器latch。

//通常情况下,if_else语句的使用
if(表达式)
语句1;
else if(表达式)
语句2;
else
语句3;
//也可以配合begin end使用
if(表达式)begin
语句1_1;
语句1_2;
语句1_3;
end
else if(表达式)begin
语句2_1;
语句2_2;
语句2_3;
end
else begin
语句3_1;
语句3_2;
语句3_3;
end
//例1
//当sys_clk为上升沿或sys_rst_n为下降沿就执行always内的语句
always@(posedge sys_clk or negedge sys_rst_n)if(sys_rst_n == 1'b0)                //如果sys_rst_n == 1'b0,就执行po_cola <= 1'b0;po_cola <= 1'b0;else if(state == TWO && pi_money == 1'b1)//如果state == TWO且pi_money == 1'b1po_cola <= 1'b1;                     //就执行po_cola <= 1'b1;else po_cola <= 1'b0;                 //如果上述两个条件都不满足就执行po_cola <= 1'b0; 
//注意,if后面的表达式最后返回的是一个布尔值,返回“真"就执行指定语句,返回“假”就进入下一个判断

        (5)case语句

        我理解的case语句其实也是一个条件语句。在此之前我使用的MATLAB/simulink中的S-function和我曾经做的C语言项目中,case语句是一个很重要的语句。在Verilog中常用于查表和状态机的编写。

//例1
//case(表达式)
case(state)IDLE:if(pi_money == 1'b1) //如果state==IDLE就执行本行if语句state <= ONE;elsestate <= IDLE;ONE:if(pi_money == 1'b1)  //如果state==ONE同样执行本行if语句state <= TWO;else state <= ONE;TWO:if(pi_money == 1'b1)  //如果state==TWO执行本行if语句state <= IDLE;else state <= TWO;default:state <= IDLE;    //如果state不等于IDLE、ONE、TWO就执行defaultendcase		
//结束case语句

        (6)顺序块begin-end

        顺序块即块内的语句是按照程序书写顺序逐个执行的。

//例1
reg x,y;
reg [1:0] a,b;
//initial开始执行后,从上到下顺序执行
initial     begin#2   //延迟2个时间单位x =1'b0;#10   //延迟10个时间单位y = 1'b1;#5   //延迟5个时间单位a = {x,y}; //a == 2'b01 b = {y,x}; //b == 2'b10end   //例2
//always开始执行后,从上到下顺序执行if语句
always@(posedge CLK or negedge RSTB)  if(~RSTB)beginx <= 1'b0;y <= 1'b0;endelse beginx <= 1'b1;y <= 1'b1;end   //当RSTB==1时 always执行完后 x==1'b1 y==1'b1//当RSTB==0时 always执行完后 x==0'b1 y==1'b0

        (7)系统任务

1)$random,被调用时返回一个32位的随机数,可以用于testbench中的测试。

//使用实例:
//产生一个0或1的随机数
num = {$random}%2;
//产生一个-1到1的随机数
num = $random%2;
//产生一个0~59的随机数
num = {$random}%60;
//产生一个29~59的随机数
num = ({$random}%30)+20;

2)$monitor,仿真过程中对数据进行监视,并可以指定格式显示出来,有些类似于C语言中的printf。

$monitor("@time %t:a=%b,d=%b,c=%b",$time,a,d,c);
//依次显示仿真时间 该时间a ,d ,c的值 

例如

$monitor("@time %t: pi_money=%b, state=%b, po_cola=%b", $time, pi_money, state, po_cola);

会在modelsim上显示

[2021-07-18]Verilog HDL语法总结

 3)$time 可以返回一个64位的整数来表示当前仿真时刻值。

        (8)预编译处理语句

        `timescale 用来说明模块的时间单位和时间精度。

`timescale  时间单位/时间精度
//如:
`timescale 1ns/1ns
//时间单位为1ns不带小数`timescale 1ns/1ps
//时间单位为1ns带三位小数

参考:

《Verilog数字系统设计教程》(第3版)夏宇闻编著

【野火】FPGA系列教学视频,真正的手把手教学,