Verilog基本语法全攻略
扫描二维码
随时随地手机看文章
Verilog基本语法
现在做芯片设计已经不是门级电路设计了,一个系统设计达到的门电路规模都是十几二十多个G的规模,所以必须要借助EDA工具进行芯片设计,而描述电路也不是通过画逻辑门电路了,而是用硬件描述语言对电路进行描述,比如verilog和VHDL。通过硬件描述语言把电路描述出来,在通过EDA进行辅助设计,包括前端验证,后端综合等等,整个芯片设计流程都必须要有EDA工具辅助。
在国内大部分还是使用verilog硬件描述语言来进行芯片设计,介绍verilog语法的书也很多,并且verilog的语法是最简单的,没有之一。这里我们也不会像书本上那样详细系统的去罗列出verilog语法。我们主要列举一些笔试和面试中经常考察的verilog语法,提炼出来,方便大家复习,也是为自己做一个学习记录。

内容概括
电路的抽象层次
Verilog描述RTL的框架
模块端口定义
模块I/O格式
模块内部信号声明
模块功能定义
参数
变量
运算符及表达式
函数与TASK
综合与不可综合语法
建立可综合模型的原则
01电路的抽象层次
一个电路我们可以按照五个层次来抽象,具体抽象层次如下所示:
-
系统级(system): 用语言提供的高级结构实现设计模块外部性能的模型。
-
算法级(algorithm): 用语言提供的高级结构实现算法运行的模型。
-
RTL级(Register Transfer Level):描述数据在寄存器之间流动和如何处理和控制这些数据流动的模型。
-
门级(gate-level):描述逻辑门以及逻辑门之间的连接的模型。
-
开关级(switch-level):描述器件中三极管和储存节点以及它们之间连接的模型
抽象层次越高,设计的难度就越低,除了模拟电路还在开关级上进行设计,数字电路基本上都已经在RTL级以上进行设计了。
现在数字芯片的规模越来越大,工艺越来越复杂,已经不可能在开关级或者门级层次上来设计电路了。当然有一些定制电路还是会在开关级或者门级进行设计,但电路的规模都很小。因为设计的层次越高,那么就需要依赖EDA工具辅助设计,对性能和面积肯定不是最优的,这就是为什么有些专用定制电路还是会采用开关级或者门级层级进行设计。
Verilog语言可以支持上面五个层级进行电路设计,但我们平常一般都是在RTL级别进行设计,再通过EDA工具进行辅助设计,这样可以使硬件设计师们得以专注于逻辑,而不需要考虑硬件层面的实现。同样的代码只要经过不同的库综合,就可以在不同的硬件上运行。因此Verilog的代码具有极高的可复用性

从上面的典型例子可以看出,Verilog结构位于在module和endmodule声明语句之间,每个Verilog结构包括四个主要部分:端口定义、I/O说明、内部信号声明、功能定义。
下面详细介绍各个部分的具体语法。
模块的端口声明了模块的输入输出口。其格式如下:
module 模块名(口1,口2,口3,口4, ………);
…….
endmodule
模块的端口表示的是模块的输入和输出口名,也就是它与别的模块联系端口的标识。
在模块被引用时,在引用的模块中,有些信号要输入到被引用的模块中,有的信号需要从被引用的模块中取出来。在引用模块时其端口可以用两种方法连接:
1)在引用时,严格按照模块定义的端口顺序来连接,不用标明原模块定义时规定的端口名,举例说明如下:
模块名 实例化名( 连接端口1信号名, 连接端口2信号名,….,,,);
2)在引用时用“.”标明原模块定义时规定的端口名,举例说明如下:
模块名 实例化名(
.端口1名( 连接信号1名),
.端口2名( 连接信号2名),….,,,
);
这样表示的好处在于可以用端口名与被引用模块的端口对应,不必严格按端口顺序对应,提高了程序的可读性和可移植性。
04
04模块I/O格式
输入口:
input [信号位宽-1 :0] 端口名1;
input [信号位宽-1 :0] 端口名2;
………;
input [信号位宽-1 :0] 端口名i; //(共有i个输入口)
输出口:
output [信号位宽-1 :0] 端口名1;
output [信号位宽-1 :0] 端口名2;
………;
output [信号位宽-1 :0] 端口名j; //(共有j个输出口)
输入/输出口:
inout [信号位宽-1 :0] 端口名1;
inout [信号位宽-1 :0] 端口名2;
………;
inout [信号位宽-1 :0] 端口名k; //(共有k个双向总线端口)
I/O说明也可以写在端口声明语句里。其格式如下:
module module_name(
input port1,
input port2,…
output port1,
output port2… );
0405模块内部信号声明
在模块内用到的和与端口有关的wire 和 reg 类型变量的声明。如:
reg [width-1 : 0] R变量1,R变量2;
wire [width-1 : 0] W变量1,W变量2;reg[width-1:0] mem[depth-1:0];
//声明了一个数组
0406模块功能定义
模块中最重要的部分是逻辑功能定义部分。有三种方法可在模块中产生逻辑。
1)用“assign”声明语句,如:
assign a = b & c;
2)实例化模块,如:and u1( q, a, b );
3)用“always”块
如:
采用“assign”语句是描述组合逻辑最常用的方法之一。而“always”块既可用于描述组合逻辑也可描述时序逻辑。上面的例子用“always”块生成了一个带有异步清除端的D触发器。“always”块可用很多种描述手段来表达逻辑,例如上例中就用了if...else语句来表达逻辑关系。如按一定的风格来编写“always”块,可以通过综合工具把源代码自动综合成用门级结构表示的组合或时序逻辑电路。
理解要点:
如果用Verilog模块实现一定的功能,首先应该清楚哪些是同时发生的,哪些是顺序发生的。上面分别采用了“assign”语句、实例化模块和“always”块,描述的逻辑功能是同时执行的。也就是说,如果把这三项写到一个 VeriIog 模块文件中去,它们的次序不会影响逻辑实现的功能。这三项是同时执行的,也就是并发的。
然而,在“always”模块内,逻辑是按照指定的顺序执行的。“always”块中的语句称为“顺序语句”,因为它们是顺序执行,所以“always”块也称作“过程块”。请注意,两个或更多的“always”语句块,它们是同时执行的,而模块内部的语句是顺序执行的。看一下“always”块内的语句,你就会明白它是如何实现功能的。if..else… if必须顺序执行,否则其功能就没有任何意义。如果else语句在if语句之前执行,其功能就会不符合要求!为了能实现上述描述的功能,“always”语句块内部的语句将按照书写的顺序执行。
在Verilog 模块中所有过程块(如:initial块、always块)、连续赋值语句、实例引用都是并行的。它们表示的是一种通过变量名互相连接的关系。在同一模块中这三者出现的先后次序没有关系。只有连续赋值语句assign 和实例引用语句可以独立于过程块而存在于模块的功能定义部分。以上是与C语言有很大的不同。许多与C语言类似的语句只能出现在过程块中,而不能随意出现在模块功能定义的范围内。
0407参数
在Verilog HDL中用parameter来定义常量,即用parameter来定义一个标识符代表一个常量,称为符号常量,即标识符形式的常量,采用标识符代表一个常量可提高程序的可读性和可维护性。parameter型数据是一种常数型的数据,其说明格式如下:
parameter参数名1=表达式,参数名2=表达式, …,参数名n=表达式;
parameter是参数型数据的确认符,确认符后跟着一个用逗号分隔开的赋值语句表。在每一个赋值语句的右边必须是一个常数表达式。也就是说,该表达式只能包含数字或先前已定义过的参数。见下列:
parameter msb=7; //定义参数msb为常量7
parameter e=25, f=29; //定义二个常数参数
parameter r=5.7; //声明r为一个实型参数
parameter byte_size=8, byte_msb=byte_size-1; //用常数表达式赋值
parameter average_delay = (r+f)/2; //用常数表达式赋值
0408变量
01
Wire型
wire型数据常用来表示用于以assign关键字指定的组合逻辑信号。Verilog程序模块中输入输出信号类型缺省时自动定义为wire型。wire型信号可以用作任何方程式的输入,也可以用作“assign”语句或实例元件的输出。
wire型信号的格式同reg型信号的很类似。其格式如下:
wire [n-1:0] 数据名1,数据名2,…数据名i; //共有i条总线,每条总线内有n条线路
wire [n:1] 数据名1,数据名2,…数据名i;
wire是wire型数据的确认符,[n-1:0]和[n:1]代表该数据的位宽,即该数据有几位。最后跟着的是数据的名字。如果一次定义多个数据,数据名之间用逗号隔开。声明语句的最后要用分号表示语句结束。如下格式:
wire a; //定义了一个一位的wire型数据
wire [7:0] b; //定义了一个八位的wire型数据
wire [4:1] c, d; //定义了二个四位的wire型数据
02
Reg型
寄存器是数据储存单元的抽象。寄存器数据类型的关键字是reg。通过赋值语句可以改变寄存器储存的值,其作用与改变触发器储存的值相当。reg类型数据的缺省初始值为不定值,x。reg型只表示被定义的信号将用在“always”块内。
reg型数据常用来表示用于“always”模块内的指定信号,常代表触发器。通常,在设计中要由“always”块通过使用行为描述语句来表达逻辑关系。在“always”块内被赋值的每一个信号都必须定义成reg型。
reg型数据的格式如下:
reg [n-1:0] 数据名1,数据名2,… 数据名i;
reg [n:1] 数据名1,数据名2,… 数据名i;
reg是reg型数据的确认标识符,[n-1:0]和[n:1]代表该数据的位宽,即该数据有几位(bit)。最后跟着的是数据的名字。如果一次定义多个数据,数据名之间用逗号隔开。声明语句的最后要用分号表示语句结束。如下:
reg rega; //定义了一个一位的名为rega的reg型数据
reg [3:0] regb; //定义了一个四位的名为regb的reg型数据
reg [4:1] regc, regd; //定义了两个四位的名为regc和regd的reg型数据。
03
Memory型
Verilog HDL通过对reg型变量建立数组来对存储器建模,可以描述RAM型存储器,ROM存储器和reg文件。数组中的每一个单元通过一个数组索引进行寻址。在Verilog语言中没有多维数组存在。memory型数据是通过扩展reg型数据的地址范围来生成的。其格式如下:
reg [n-1:0] 存储器名[m-1:0];
或 reg [n-1:0] 存储器名[m:1];
在这里,reg[n-1:0]定义了存储器中每一个存储单元的大小,即该存储单元是一个n位的寄存器。存储器名后的[m-1:0]或[m:1]则定义了该存储器中有多少个这样的寄存器。最后用分号结束定义语句。下面举例说明:
reg [7:0] mema[255:0];
这个例子定义了一个名为mema的存储器,该存储器有256个8位的存储器。该存储器的地址范围是0到255。注意:对存储器进行地址索引的表达式必须是常数表达式。
另外,在同一个数据类型声明语句里,可以同时定义存储器型数据和reg型数据。见下例:
parameter wordsize=16, //定义二个参数
memsize=256;
reg [wordsize-1:0] mem[memsize-1:0], writereg, readreg;
尽管memory型数据和reg型数据的定义格式很相似,但要注意其不同之处。如一个由n个1位寄存器构成的存储器组是不同于一个n位的寄存器的。见下例:
reg [n-1:0] rega; //一个n位的寄存器
reg mema [n-1:0]; //一个由n个1位寄存器构成的存储器组
一个n位的寄存器可以在一条赋值语句里进行赋值,而一个完整的存储器则不行。见下例:
rega =0; //合法赋值语句
mema =0; //非法赋值语句
如果想对memory中的存储单元进行读写操作,必须指定该单元在存储器中的地址。下面的写法是正确的。
mema[3]=0; //给memory中的第3个存储单元赋值为0。
进行寻址的地址索引可以是表达式,这样就可以对存储器中的不同单元进行操作。表达式的值可以取决于电路中其它的寄存器的值。例如可以用一个加法计数器来做RAM的地址索引。
09运算符及表达式
Verilog HDL语言的运算符范围很广,其运算符按其功能可分为以下几类:
-
算术运算符(+,-,×,/,%)
-
赋值运算符(=,<=)
-
关系运算符(>,<,>=,<=)
-
逻辑运算符(&&,||,!)
-
条件运算符(?:)
-
位运算符(~,|,^,&,^~)
-
移位运算符(<<,>>)
-
拼接运算符({ })
其他运算符的功能可以参考各类书籍,这里主要讲一下等式运算
在Verilog HDL语言中存在四种等式运算符:
-
= = (等于)
-
!= (不等于)
-
= = = (等于)
-
!= = (不等于)
注意:求反号、双等号、三个等号之间不能有空格。
这四个运算符都是二目运算符,它要求有两个操作数。'=='和'!='又称为逻辑等式运算符。其结果由两个操作数的值决定。由于操作数中某些位可能是不定值x和高阻值z,结果可能为不定值x。而“===”和“!==”运算符则不同,它在对操作数进行比较时对某些位的不定值x和高阻值z也进行比较,两个操作数必需完全一致,其结果才是1,否则为0。“===”和“!==”运算符常用于case表达式的判别,所以又称为“case等式运算符”。这四个等式运算符的优先级别是相同的。
下面举一个例子说明“==”和“===”的区别。
例:
if(A==1‘bx) $display(“AisX”);(当A等于X时,这个语句不执行)
if(A===1‘bx) $display(“AisX”);(当A等于X时,这个语句执行)
运算符的优先级:
0410函数与Task
-
任务的的定义
任务的定义语法如下:
task <任务名>;
<端口及数据类型声明语句>
<语句1>
<语句2>
……
<语句n>
endtask
-
任务的调用及变量的传递
任务的调用:
<任务名>(端口1,端口2,端口3,……端口n);
-
定义函数的语法:
function <返回值的类型或范围> (函数名);
<端口说明语句>
<变量类型说明语句>
begin
<语句>
……
end
endfuction
注:<返回值的类型或范围>这一项是可选项,如默认则返回值为一位寄存器类型数据。
-
从函数返回的值
函数的定义蕴含声明了与函数同名的、函数内部的寄存器,函数的定义把函数返回值所赋值寄存器的名称初始化为与函数同名的内部变量。
-
函数的调用:
函数的调用时通过将函数作为表达式中的操作数来实现的。
调用格式如下:
<函数名>(<表达式>,…<表达式>)
-
函数的使用规则:
-
函数的定义不能包含任何的时间控制语句,即用任何用#、@或wait来标识的语句;
-
函数不能启动任务,但可以调用其他函数;
-
定义函数时至少要有一个输入变量;
-
在函数定义中必须有一条赋值语句给函数中的一个内部变量赋以函数的结果值,该内部变量具有和函数名相同的名字。
-
关于使用任务和函数的小结
-
任务和函数都是用来对设计中多处使用的公共代码进行定义;使用任务和函数可以将模块分割成许多个可独立管理的子单元,增加了模块的可读性和可维护性;它们和C语言中的子程序起相同作用;
-
任务可以具有任意多个输入、输出和输入\输出(inout)变量;在任务中可以使用延迟、事件和时序控制结构,在任务中可以调用其它的任务和函数;
-
可重入任务使用关键字automatic进行定义,它的每一次调用都对不同的地址空间进行操作。因此在被多次并发调用时,它仍然可以获得正确的结果;
-
函数只能有一个返回值,并且至少要有一个输入变量;在函数中不能使用延迟、事件和时序控制结构,但可以调用其它函数,不能调用任务;
-
当声明函数时,Verilog仿真器都会隐含的声明一个同名的寄存器变量,函数的返回值通过这个寄存器传递回调用处;
-
递归函数使用关键词automatic进行定义,递归函数的每一次调用都拥有不同的地址空间,因此对这种函数的递归调用和并发调用可以得到正确的结果;
-
任务和函数都包含在设计层次之中,可以通过层次名对它们进行调用。
0411综合与不可综合语法
-
一般综合工具支持的verilog HDL结构:


-
一般工具忽略的verilog HDL结构
-
一般综合工具不支持的verilog HDL结构
0412建立可综合模型的原则
-
不使用initial。
-
不使用#10。
-
不使用循环次数不确定的循环语句,如forever、while等。
-
不使用用户自定义原语(UDP元件)。
-
尽量使用同步方式设计电路。
-
除非是关键路径的设计,一般不采用调用门级元件来描述设计的方法,建议采用行为语句来完成设计。
-
用always过程块描述组合逻辑,应在敏感信号列表中列出所有的输入信号。
-
所有的内部寄存器都应该能够被复位,在使用FPGA实现设计时,应尽量使用器件的全局复位端作为系统总的复位。
-
对时序逻辑描述和建模,应尽量使用非阻塞赋值方式。对组合逻辑描述和建模,既可以用阻塞赋值,也可以用非阻塞赋值。但在同一个过程块中,最好不要同时用阻塞赋值和非阻塞赋值。
-
不能在一个以上的always过程块中对同一个变量赋值。而对同一个赋值对象不能既使用阻塞式赋值,又使用非阻塞式赋值。
-
如果不打算把变量推导成锁存器,那么必须在if语句或case语句的所有条件分支中都对变量明确地赋值。
-
避免混合使用上升沿和下降沿触发的触发器。
-
同一个变量的赋值不能受多个时钟控制,也不能受两种不同的时钟条件(或者不同的时钟沿)控制。
-
避免在case语句的分支项中使用x值或z值。





