第三章 verilog语法进阶
三、模块的结构、数据类型、变量和基本运算符号
3.1 模块的结构
Verilog的基本设计单元是“模块”(block)。一个模块是由两部分组成的,一部分描述接口,另一部分描述逻辑功能,即定义输入是如何影响输出的。下面举例说明:
图2 模块简图
请看上面的例子: 程序模块旁边有一个电路图的符号。在许多方面,程序模块和电路图符号是一致的,这是因为电路图符号的引脚也就是程序模块的接口。而程序模块描述了电路图符号所实现的逻辑功能。以上就是设计一个简单的Verilog程序模块所需的全部内容。从上面的例子可以看出,Verilog结构位于在module和endmodule声明语句之间,每个Verilog程序包括四个主要部分:端口定义、I/O说明、内部信号声明、功能定义。
3.1.1 模块的端口定义
模块的端口声明了模块的输入输出口。其格式如下:
module 模块名(口1,口2,口3,口4, ………);
…….
endmodule
模块的端口表示的是模块的输入和输出口名,也就是它与别的模块联系端口的标识。在模块被引用时,在引用的模块中,有些信号要输入到被引用的模块中,有的信号需要从被引用的模块中取出来。在引用模块时其端口可以用两种方法连接:
1)在引用时,严格按照模块定义的端口顺序来连接,不用标明原模块定义时规定的端口名,举例说明如下:
模块名 实例化名( 连接端口1信号名, 连接端口2信号名,连接端口3信号名,….,,,);
2)在引用时用“.”标明原模块定义时规定的端口名,举例说明如下:
模块名 实例化名(.端口1名( 连接信号1名),.端口2名( 连接信号2名),….,,,);
这样表示的好处在于可以用端口名与被引用模块的端口对应,不必严格按端口顺序对应,提高了程序的可读性和可移植性。
3.1.2 模块内容
模块的内容包括I/O说明、内部信号声明、功能定义。
3.1.2.1 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… );
3.1.2.2 内部信号说明:
在模块内用到的和与端口有关的wire 和 reg 类型变量的声明。如:
reg [width-1 : 0] R变量1,R变量2 。。。。;
wire [width-1 : 0] W变量1,W变量2 。。。。;
………..
3.1.2.3 功能定义:
模块中最重要的部分是逻辑功能定义部分。有三种方法可在模块中产生逻辑。
1)用“assign”声明语句,如:assign a = b & c;
2)实例化模块,如:and u1( q, a, b );
3)用“always”块
如:always @(posedge clk or posedge clr)
begin
if(clr) q <= 0;
else if(en) q <= d;
end
采用“assign”语句是描述组合逻辑最常用的方法之一。而“always”块既可用于描述组合逻辑也可描述时序逻辑。上面的例子用“always”块生成了一个带有异步清除端的D触发器。“always”块可用很多种描述手段来表达逻辑,例如上例中就用了if...else语句来表达逻辑关系。如按一定的风格来编写“always”块,可以通过综合工具把源代码自动综合成用门级结构表示的组合或时序逻辑电路。
3.1.3 理解要点:
如果用Verilog模块实现一定的功能,首先应该清楚哪些是同时发生的,哪些是顺序发生的。上面分别采用了“assign”语句、实例元件和“always”块,描述的逻辑功能是同时执行的。也就是说,如果把这三项写到一个 VeriIog 模块文件中去,它们的次序不会影响逻辑实现的功能。这三项是同时执行的,也就是并发的。
然而,在“always”模块内,逻辑是按照指定的顺序执行的。“always”块中的语句称为“顺序语句”,因为它们是顺序执行,所以“always”块也称作“过程块”。请注意,两个或更多的“always”语句块,它们是同时执行的,而模块内部的语句是顺序执行的。看一下“always”块内的语句,你就会明白它是如何实现功能的。if..else… if必须顺序执行,否则其功能就没有任何意义。如果else语句在if语句之前执行,其功能就会不符合要求!为了能实现上述描述的功能,“always”语句块内部的语句将按照书写的顺序执行。
在Verilog 模块中所有过程块(如:initial块、always块)、连续赋值语句、实例引用都是并行的。它们表示的是一种通过变量名互相连接的关系。在同一模块中这三者出现的先后次序没有关系。只有连续赋值语句assign 和实例引用语句可以独立于过程块而存在于模块的功能定义部分。以上是与C语言有很大的不同。许多与C语言类似的语句只能出现在过程块中,而不能随意出现在模块功能定义的范围内。
3.2 数据类型及其常量、变量
Verilog HDL中总共有十九种数据类型,数据类型是用来表示数字电路硬件中的数据储存和传送元素的。先只介绍四个最基本的数据类型,它们是:reg型、wire型、integer型、parameter型。
其它数据类型也有很多:large型、medium型、scalared型、time型、small型、tri型、trio型、tri1型、triand型、trior型、trireg型、vectored型、wand型、wor型。
3.2.1 常量
在程序运行过程中,其值不能被改变的量称为常量。下面首先对在Verilog HDL语言中使用的数字及其表示方式进行介绍。
3.2.1.1 整数:
在Verilog HDL中,整型常量即整常数有以下四种进制表示形式:
- 二进制整数(b或B)
- 十进制整数(d或D)
- 十六进制整数(h或H)
- 八进制整数(o或O)
3.2.1.2 x和z值:
在数字电路中,x代表不定值,z代表高阻值。一个x可以用来定义十六进制数的四位二进制数的状态,八进制数的三位,二进制数的一位。z的表示方式同x类似。z还有一种表达方式是可以写作?。在使用case表达式时建议使用这种写法,以提高程序的可读性。见下例:
4'b10x0 //位宽为4的二进制数从低位数起第二位为不定值
4'b101z //位宽为4的二进制数从低位数起第一位为高阻值
12'dz //位宽为12的十进制数其值为高阻值(第一种表达方式)
12'd? //位宽为12的十进制数其值为高阻值(第二种表达方式)
8'h4x //位宽为8的十六进制数其低四位值为不定值
3.2.1.3 负数:
一个数字可以被定义为负数,只需在位宽表达式前加一个减号,减号必须写在数字定义表达式的最前面。注意减号不可以放在位宽和进制之间也不可以放在进制和具体的数之间。如下:
-8'd5 //这个表达式代表5的补数(用八位二进制数表示)
8'd-5 //非法格式
3.2.2 参数(Parameter)型
在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; //用常数表达式赋值
3.2.3 变量
3.2.3.1 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型数据
3.2.3.2 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型数据
3.2.3.3 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的地址索引。
3.3 运算符及表达式
Verilog HDL语言的运算符范围很广,其运算符按其功能可分为以下几类:
- 算术运算符(+,-,×,/,%)
- 赋值运算符(=,<=)
- 关系运算符(>,<,>=,<=)
- 逻辑运算符(&&,||,!)
- 条件运算符(?:)
- 位运算符(
,|,^,&,^) - 移位运算符(<<,>>)
- 拼接运算符({ })
- 其它
在Verilog HDL语言中运算符所带的操作数是不同的,按其所带操作数的个数运算符可分为三种:
- 单目运算符(unary operator):可以带一个操作数,操作数放在运算符的右边。
- 二目运算符(binary operator):可以带二个操作数,操作数放在运算符的两边。
- 三目运算符(ternary operator):可以带三个操作,这三个操作数用三目运算符分隔开。
clock = ~clock; // ~是一个单目取反运算符, clock是操作数。
c = a | b; // 是一个二目按位或运算符, a 和 b是操作数。
r = s ? t : u; // ?: 是一个三目条件运算符, s,t,u是操作数。
下面对常用的几种运算符进行介绍。
3.3.1 基本的算术运算符
在Verilog HDL语言中,算术运算符又称为二进制运算符,共有下面几种:
-
- (加法运算符,或正值运算符,如 rega+regb,+3)
- - (减法运算符,或负值运算符,如 rega-3,-3)
- × (乘法运算符,如rega*3)
- / (除法运算符,如5/3)
- % (模运算符,或称为求余运算符,要求%两侧均为整型数据。如7%3的值为1)
3.3.2 位运算符
Verilog HDL作为一种硬件描述语言,是针对硬件电路而言的。在硬件电路中信号有四种状态值1,0,x,z.在电路中信号进行与或非时,反映在Verilog HDL中则是相应的操作数的位运算。
3.3.2.1 "取反"运算符~
~是一个单目运算符,用来对一个操作数进行按位取反运算。
其运算规则见下表:
图3 取反求值
rega='b1010;//rega的初值为'b1010
rega=~rega;//rega的值进行取反运算后变为'b0101
3.3.2.2 “按位与”运算符 &
按位与运算就是将两个操作数的相应位进行与运算,
其运算规则见下表:
图4 按位与求值
3.3.2.3 “按位或”运算符|
按位或运算就是将两个操作数的相应位进行或运算。
其运算规则见下表:
图5 按位或求值
3.3.2.4 “按位异或”运算符^(也称之为XOR运算符)
按位异或运算就是将两个操作数的相应位进行异或运算。
其运算规则见下表:
图6 按位异或求值
3.3.2.5 “按位同或”运算符^~
按位同或运算就是将两个操作数的相应位先进行异或运算再进行非运算。其运算规则见下表:
图7 按位同或求值
3.3.2.6 不同长度的数据进行位运算
两个长度不同的数据进行位运算时,系统会自动的将两者按右端对齐。位数少的操作数会在相应的高位用0填满,以使两个操作数按位进行操作。
3.4 逻辑运算符
在Verilog HDL语言中存在三种逻辑运算符:
1) && 逻辑与
2) || 逻辑或
3) ! 逻辑非
“&&”和“||”是二目运算符,它要求有两个操作数,如(a>b)&&(b>c),(ab)。下表为逻辑运算的真值表。它表示当a和b的值为不同的组合时,各种逻辑运算所得到的值。
图 8 求与
逻辑运算符中“&&”和“||”的优先级别低于关系运算符,“!” 高于算术运算符。
3.5 关系运算符
关系运算符共有以下四种:
a < b a小于b
a > b a大于b
a <= b a小于或等于b
a >= b a大于或等于b
在进行关系运算时,如果声明的关系是假的(false),则返回值是0,如果声明的关系是真的(true),则返回值是1,如果某个操作数的值不定,则关系是模糊的,返回值是不定值。
3.6 等式运算符
在Verilog HDL语言中存在四种等式运算符:
- = = (等于)
- != (不等于)
- = = = (等于)
- != = (不等于)
注意:求反号、双等号、三个等号之间不能有空格
这四个运算符都是二目运算符,它要求有两个操作数。"=="和"!="又称为逻辑等式运算符。其结果由两个操作数的值决定。由于操作数中某些位可能是不定值x和高阻值z,结果可能为不定值x。而“===”和“!==”运算符则不同,它在对操作数进行比较时对某些位的不定值x和高阻值z也进行比较,两个操作数必需完全一致,其结果才是1,否则为0。“===”和“!==”运算符常用于case表达式的判别,所以又称为“case等式运算符”。这四个等式运算符的优先级别是相同的。下面画出==与===的真值表,帮助理解两者间的区别。
图9 ===与==的区别
下面举一个例子说明“==”和“===”的区别。
例:
if(A==1‘bx) $display(“AisX”);(当A等于X时,这个语句不执行)
if(A===1‘bx) $display(“AisX”);(当A等于X时,这个语句执行)
3.7 移位运算符
在Verilog HDL中有两种移位运算符:
<< (左移位运算符) 和 >>(右移位运算符)。
其使用方法如下:
a >> n 或 a << n
a代表要进行移位的操作数,n代表要移几位。这两种移位运算都用0来填补移出的空位。
3.8 位拼接运算符(Concatation)
在Verilog HDL语言有一个特殊的运算符:位拼接运算符{}。用这个运算符可以把两个或多个信号的某些位拼接起来进行运算操作。其使用方法如下:
{信号1的某几位,信号2的某几位,..,..,信号n的某几位}
即把某些信号的某些位详细地列出来,中间用逗号分开,最后用大括号括起来表示一个整体信号。见下例:
{a,b[3:0], w, 3’b101}
也可以写成为
{a,b[3],b[2],b[1],b[0],w,1’b1,1’b0,1’b1}
在位拼接表达式中不允许存在没有指明位数的信号。这是因为在计算拼接信号的位宽的大小时必需知道其中每个信号的位宽。
位拼接还可以用重复法来简化表达式。见下例:
{4{w}} //这等同于{w,w,w,w}
位拼接还可以用嵌套的方式来表达。见下例:
{b,{3{a,b}}} //这等同于{b,a,b,a,b,a,b}
用于表示重复的表达式如上例中的4和3,必须是常数表达式。
3.9 缩减运算符(reduction operator)
缩减运算符是单目运算符,也有与或非运算。其与或非运算规则类似于位运算符的与或非运算规则,但其运算过程不同。位运算是对操作数的相应位进行与或非运算,操作数是几位数则运算结果也是几位数。而缩减运算则不同,缩减运算是对单个操作数进行或与非递推运算,最后的运算结果是一位的二进制数。缩减运算的具体运算过程是这样的:第一步先将操作数的第一位与第二位进行或与非运算,第二步将运算结果与第三位进行或与非运算,依次类推,直至最后一位。
例如:reg [3:0] B;
reg C;
C = &B;
相当于:
C =( (B[0]&B[1]) & B[2] ) & B[3];
由于缩减运算的与、或 运算规则类似于位运算符与、或 运算规则,这里不再详细讲述,请参照位运算符的运算规则介绍。
3.10 优先级别
下面对各种运算符的优先级别关系作一个总结。见下表:
图10 运算符优先级
3.11 关键词
在Verilog HDL中,所有的关键词是事先定义好的确认符,用来组织语言结构。关键词是用小写字母定义的,因此在编写程序代码时必须注意关键词的书写,以避免出错。下面是Verilog HDL中使用的关键词
always, and, assign, begin, buf, bufif0, bufif1, case, casex, casez, cmos, deassign, default, defparam, disable, edge, else, end, endcase, endmodule, endfunction, endprimitive, endspecify, endtable, endtask, event, for, force, forever, fork, function, highz0, highz1, if, initial, inout, input, integer, join,large, macromodule,medium, module, nand, negedge, nmos, nor, not, notif0, notifl, or, output, parameter, pmos, posedge, primitive, pull0, pull1, pullup, pulldown, rcmos, reg, releses, repeat, mmos, rpmos, rtran, rtranif0, rtranif1, scalared, small, specify, specparam, strength, strong0, strong1, supply0, supply1, table, task, time, tran, tranif0, tranif1, tri, tri0, tri1, triand, trior, trireg, vectored, wait, wand, weak0, weak1, while, wire, wor, xnor, xor
注意在编写Verilog HDL程序时,变量的定义不要与这些关键词冲突。
3.12 赋值语句
3.12.1 赋值语句
在Verilog HDL语言中,信号有两种赋值方式:
(1).非阻塞(Non_Blocking)赋值方式( 如 b <= a; )
A) 在语句块中,上面语句所赋的变量值不能立即就为下面的语句所用;
B) 块结束后才能完成这次赋值操作,而所赋的变量值是上一次赋值得到的;
C) 在编写可综合的时序逻辑模块时,这是最常用的赋值方法。
[注意]:非阻塞赋值符 “ <= ” 与小于等于符 “<= ” 看起来是一样的,但意义完全不同,小于等于符是关系运算符,用于比较大小。而非阻塞赋值符用于时序赋值操作。
(2).阻塞(Blocking)赋值方式( 如 b = a; )
A) 赋值语句执行完后,块才结束;
B) b的值在赋值语句执行完后立刻就改变的;
C) 在时序逻辑设计中使用,可能会产生意想不到的结果。
非阻塞赋值方式和阻塞赋值方式的区别常给设计人员带来问题。问题主要是给"always"块内的reg型信号的赋值方式不易把握。"always"模块内的reg型信号都是采用下面的这种赋值方式:
b <= a;
这种方式的赋值并不是马上执行的,也就是说 "always" 块内的下一条语句执行后,b并不等于a,而是保持原来的值。"always" 块结束后,才进行赋值。而另一种赋值方式阻塞赋值方式,如下所示:
b = a;
这种赋值方式是马上执行的。也就是说执行下一条语句时,b已等于a。尽管这种方式看起来很直观,但是可能引起麻烦
总结:
- 在Verilog 模块中所有过程块(如:initial块、always块)、连续赋值语句、实例引用都是并行的;
- 它们表示的是一种通过变量名互相连接的关系;
- 在同一模块中各个过程块、各条连续赋值语句和各条实例引用语句这三者出现的先后次序没有关系;
- 只有连续赋值语句(即用关键词assign引出的语句)和实例引用语句(即用已定义的模块名引出的语句),可以独立于过程块而存在于模块的功能定义部分;
- 被实例引用的模块其端口可以通过不同名的连线或寄存器类型变量连 接到别的模块相应的输出输入信号端;
- 在“always”块内被赋值的每一个信号都必须定义成reg型;
- 无论是逻辑运算、逻辑比较还是逻辑等式等逻辑操作一般发生在条件判断语句中,其输出只有:1 或 0,也可以理解为成立(真)或不成立(假);
- 位拼接运算符{ },在C语言中没有定义,但在Verilog中是很有用的语法。我们可以借助于拼接符用一个信号名来表示由多位信号组成的复杂信号,其中每个功能信号可以有自己独立的名字和位宽;
- 缩减运算符(reduction operator)也是C语言所没有的,合理地使用缩减运算符可以使程序简洁、明了;
- 阻塞和非阻塞赋值也是C语言所没有的。我们应当理解这是非常重要的概念,特别在编写可综合风格的模块中要加以注意。阻塞语句,如果没有写延迟时间看起来是在同一时刻运行,但实际上是有先后的,即在前面的先运行,然后再运行下面的语句,阻塞语句的次序与逻辑行为有很大的关系。而非阻塞的就不同了,在begin end之间的所有非阻塞语句都在同一时刻被赋值,因此逻辑行为与非阻塞语句的次序就没有关系。在硬件实现时这两者有很大的不同;
- begin end 块语句与C语言中的大括号对(即{})类似,而fork join语句在C语言中没有定义,但其语义并不难理解。在测试模块中描述测试信号时常在initial和always过程块中使用并行块。这种描述方法,由于时间关系只与起点比较,有时这样表达比较容易和清楚。
-
电路设计
+关注
关注
6673文章
2451浏览量
204125 -
Verilog
+关注
关注
28文章
1351浏览量
110074 -
HDL
+关注
关注
8文章
327浏览量
47374 -
数字系统
+关注
关注
0文章
143浏览量
20842
发布评论请先 登录
相关推荐
评论