基础资料
- 硬件描述语言HDL概述
- Verilog HDL的基本单元--模块
- Verilog HDL词法与常量及变量
- Verilog HDL运算符和表达式
- Verilog HDL语句
- Verilog HDL的task和function
- Verilog HDL的编译向导与宏
- Verilog HDL常用的建模方式
- Verilog HDL代码编写风格
- Verilog HDL逻辑验证与测试平台
- 逻辑综合与静态时序分析
- Verilog-2001标准
- ANSI C风格的模块声明:
- 敏感信号列表:
- generate语句:
- 有符号的算术扩展:
- 指数运算符:
- 变量声明时赋值:
- 常数函数:
- 向量的位选和域选:
- 数组的功能扩展:
- 模块实例化时的参数重载:
- register改为variable:
- 条件编译增加`elsif和`ifndef:
- 超过32位的自动宽度扩展:
- 可重入任务和递归函数:
- 文件和行编译指示:
- 增强文件输入/输出操作:
- Intel FPGA/CPLD家族系列
1. 硬件描述语言HDL概述:
数字系统设计的过程实质上是系统高层次功能描述向低层次结构描述的转换。硬件描述语言HDL(Hardware Description Language)是一种利用文字描述数字电路系统的方法,可以起到和传统的电路原理图描述相同的效果。描述文件按照某种规则进行编写,之后使用EDA工具进行综合、布局布线等工作,就可以转化为实际电路。
硬件描述语言的出现,使得数字电路迅速发展,而数字电路系统的发展也促进了硬件描述语言的发展。目前为止已经出现上百种硬件描述语言,目前件描述语言最流行和通用的是Verilog HDL和VHDL两种。随着数字电路系统的发展,又出现了System Verilog、SystemC等。
Verilog HDL最初与1983年由美国Gateway Design Automation公司的Phil Moorby开发成功,是一种在C语言基础上发展起来的硬件描述语言,最初只设计了一个仿真与验证工具,之后陆续开发了相关故障模拟与时序分析工具。1984~1985年,Moorby设计出了一个名为Verilog-XL的仿真器,获得巨大成功,1986年又提出用于快速门级仿真的XL算法,使Verilog HDL得到迅速推广和使用。1989年,美国Cadence公司收购了Gateway Design Automation公司,Verilog HDL成为了Cadence公司的专利。1990年,Cadence公司公开发表了Verilog HDL,并成立Open Verilog International组织来负责促进Verilog HDL语言的发展。1995年,Verilog HDL成为IEEE标准,即IEEE Std 1364-1995,2001年又发布了Verilog HDL 1364-2001标准,2005年System Verilog IEEE 1800-2005标准的公布,更使得Verilog HDL在仿真、综合、验证和IP模块重用等方面都有大幅提高。
Verilog HDL不仅定义了语法,而且对每个语法结构都清晰定义了仿真语义,便于仿真调试。Verilog HDL继承了C语言的很多操作符和语法结构,易学易用,并有很强扩展性。
VHDL是Very High Speed Integrated Circuit HDL的缩写,是在ADA语言基础上发展起来,诞生于1982年。由于得到美国国防部的支持,并与1987年成为IEEE Std 1076-1987标准,各EDA公司相继推出VHDL设计环境,使其得到广泛应用。1993年,IEEE对VHDL进行了修订,公布了IEEE标准1076-1993。
Verilog HDL最初是为了更简洁有效描述数字电路硬件电路和仿真而设计,很多关键字和语法都继承了C语言的传统,易学易懂,在门级描述的底层具有更强功能。VHDL具有更强的行为描述能力,抽象性更强,成为系统设计领域最佳的硬件描述语言,也就更适合描述更高层次,如行为级或系统级的硬件电路。
2. Verilog HDL的基本单元--模块:
模块Module是Verilog HDL的基本描述单元,用于描述某个设计的功能或结构及其与其他模块通信的外部接口,实际意义是代表硬件电路上的逻辑实体,其范围可以从简单的门到整个大系统。模块之间是并行运行的,模块又是分层的,高层模块通过调用、连接低层模块的实例来实现复杂的功能,各模块连接需要用一个顶层模块完成整个系统。
1) 模块示例:
标准的Verilog HDL模块代码示例:
module and2(
out, //端口列表
a,
b,
);
//端口定义
input a,b;
output out;
//端口数据类型说明
wire a,b;
reg out;
//逻辑功能描述
alway@(a or b)
begin
out=a&b;
end
endmodule
Verilog HDL程序嵌套在module和endmodule这两个关键字中间。上述代码构建了一个二输入与门模型,and2为模块名称。通常情况下,每个模块都可完成一定的逻辑功能,给模块取一个和其功能相关的名字,可以更方便地使用和管理。
一位半加器的描述:
module half_adder(
co, //端口列表
sum,
a,
b,
);
//端口定义
input a,b;
output co,sum;
//端口数据类型说明
wire a,b;
reg co,sum;
//逻辑功能描述
assign {co,sum}=a+b;
endmodule
对于一个复杂的系统来说,从功能上将其反复地划分为若干个小模块是必需的。Verilog HDL程序中,高层模块通过调用低层模块来构建复杂系统。使用上述1位半加器和或门构建1位全加器的示例:
module full_adder(
fco, //端口列表
fsum,
cin,
a,
b);
//端口定义
input cin,a,b;
output fco,fsum;
//端口数据类型说明
wire cin,a,b;
reg fco,fsum;
//内部数据类型说明
wire c1,s1,c2;
//逻辑功能描述
half_adder U1(c1,s1,a,b);
half_adder U2(c2,fsum,s1,cin);
or U3(fco,c1,c2);
endmodule
由示例可见一些语法规则:
· Verilog HDL程序是由模块构成的,每个模块以关键字module开始,以endmodule结尾,其间的程序用来描述电路的逻辑功能。模块可以进行层次嵌套,高层模块通过调用低层模块来构建复杂系统。
· 每个模块主要包括模块声明、端口定义、信号类型说明和逻辑功能描述等几部分,只有module、模块名和endmodule必须出现,其他部分都是可选的,其中模块名是模块的唯一标识符。
· 模块名称后面的括号内是模块的端口列表。Verilog HDL模块有输入端口和输出端口,需要在模块的开始部分就全部列出来。
· 逻辑功能描述部分是对数字电路系统的建模,这是Verilog HDL模块中最重要也是最复杂多变的部分,但其最基本的描述方式只有alway、assign和创建模块实例3种。一个模块中允许使用一种或多种方法描述逻辑功能。
· 除了endmodule语句外,每一条语句都必须以分号结尾。
· 可以用/*...*/和//...对Verilog HDL程序的任何部分作注释,以增强程序的可读性。其中/*...*/为多行注释符,用于书写多行注释;//...为单行注释符。多行注释符不能嵌套。
Verilog HDL程序文件的后缀都是.v,每个v文件里可以有一个或几个模块的描述程序。
对于复杂系统。总能划分为多个小的功能模块,因此系统的设计按下面3个步骤进行:
· 把系统划分成模块
· 规划各模块的接口
· 对模块编程并连接各模块完成系统设计
每个步骤都涉及模块,可见模块是整个设计中最基本最重要的单元。
2) Verilog HDL程序的基本结构:
⑴模块声明:
模块声明包括模块名和端口列表,格式为:
module 模块名(端口1,端口2,端口3,...);
⑵端口定义:
端口是模块与外界环境交互的接口。由于模块内部对于外部环境来说是不可见的,对模块的调用只能通过其端口进行。端口定义是明确说明端口的方向,Verilog HDL端口包括输入端口input、输出端口output和双向端口inout。格式为:
input 端口名1,端口名2,...,端口m名n; //输入端口
output 端口名1,端口名2,...,端口m名n; //输出端口
inout 端口名1,端口名2,...,端口m名n; //输入输出端口
也可以写在端口声明语句中,格式为:
module module_name(input portin1,input portin2,...,output porto1,porto2,...);
⑶信号类型说明:
信号可以分为端口信号和内部信号,出现在端口列表中的信号为端口信号,其他信号为内部信号。对模块中所用到的所有信号都必须进行数据类型的定义,如寄存器类型reg还是连线类型wire。如果信号的数据类型没有定义,则综合器将其默认为wire类型。不能将input和input类型的端口声明为reg数据类型,因为reg类型的变量用于保存数值,而输入端口只反映与其相连的外部信号的变化,并不能保存这些信号值。
端口的位宽最好定义在端口定义中,不要放在数据类型定义中,示例:
module test(addr,read,write,datain,dataout);
//端口定义
input[7:0] datain;
input[15:0] addr;
input read,write;
output[7:0] dataout;
//端口数据类型说明
wire addr,read,write,datain;
reg dataout;
⑷模块中逻辑功能描述:
模块中最核心的部分是逻辑功能描述。有多种方法可在模块中描述和定义逻辑功能,最基本的描述方式有3种,always、assign、创建模块实例。此外,还可以调用函数和任务来描述逻辑功能。
3) 模块中的逻辑功能描述:
⑴用assign连续赋值语句:
采用assign语句是描述组合逻辑最常用的方法之一,称为连续赋值方式,多用于在输出信号可以和输入信号建立某种直接联系的情况下。示例:
assign {co,sum}=a+b;
⑵调用元件:
调用元件方法类似于在电路图输入方式下调用电路元件图形符号来完成设计,每个模块实例都代表了实际电路图中的某个结构单元。Verilog HDL模块中,用线网类型变量将各个模块实例连接在一起,就像在实际电路中用导线将多个结构单元连接起来一样。在Verilog HDL中,可通过调用如下元件的方法来描述电路的结构:
· 调用Verilog HDL内置门元件(门级结构描述)
· 调用开关级元件(晶体管级结构描述)
· 用户定义元件UDP(也在门级)
· 模块实例(创建层次结构)
⑶用always过程块赋值:
用always块来描述逻辑功能,一般称为行为描述方式。在Verilog HDL中,由always指定的内容将不断重复运行。反映了实际电路中在通电情况下不断运行。always既可以用于描述组合逻辑,也可描述时序逻辑。示例:
always@(posedge clk) //每当clk上升沿到来时执行块内语句
begin
if(reset) out=0;
else out=out+1;
end
3. Verilog HDL词法与常量及变量:
1) 词法规定:
⑴关键字:
关键字,也称保留字,是Verilog HDL中预留的用于定义语言结构的特殊字符串,通常为小写字符串,如module、endmodule、input、output、wire、reg、and、assign、always等。Verilog HDL是一种区分大小写的语言。
⑵标识符:
标识符是程序代码中给各种对象,如模块、端口、变量等,命名时所用的字符串,程序通过标识符访问相应的对象。Verilog HDL中的标识符由字母、数字、下划线和$组成,区分大小写,其第1个字符必须为字母或下划线。以$开头的字符串为系统函数保留的,如$display,关键字不能作为标识符使用。
⑶格式:
Verilog HDL是自由格式的,即结构可以跨行编写,也可以在一行内编写。空白符,如换行、换页、Tab和空格,没有特殊意义,但使用空白符可以提高代码可读性。
2) 常量及其表示:
Verilog HDL使用下列4种基本的值来表示逻辑电路的逻辑状态:
· 0:逻辑0或“假”
· 1:逻辑1或“真”
· x:未知状态,通常在这个信号未被赋值之前
· z:高阻
x值和z值都是不分大小写的,在门的输入或表达式中为z的值通常解释成x。
在程序运行中,其值不能被改变的量称为常量,Verilog HDL中的常量是由上述4类基本值组成的,包括3种类型的常量:整型常量、实数常量和字符串常量。
⑴整数:一般表达式<+/-><size>'<base format><number>
其中,<+/->表示常量是正整数还是负整数,当常量为正整数时,正号可以省略;<size>用十进制定义了后面数值<number>的宽度,如果没有定义宽度位数,数值<number>的实际长度就是相应的位数;<base format>为基数符号,定义后面数值<number>的基数格式,可以为二进制(b或B)、八进制(o或O)、十进制(d或D)、十六进制(h或H)中的一种,省略情况下默认十进制;数值<number>是一个数字序列,最左边为最高有效位,最右边为最低有效位。表示负整数时,负号必须写在表达式的前面。
下划线可以出现在除第一个字符外的任何位置,只是为了提高可读性。高阻z还有另外一种写法“?”,在二进制中x、z、?代表1位,在八进制中表示3位,在十六进制中代表4位。如果定义的位宽小于数字序列的实际长度,这个数字序列最左边超出的位将被截断;如果定义的长度大于数字序列的实际长度,最高位为0、x、z时最高位由0、x或z填充;最高位为1时则高位由0填充。
⑵实数:
Verilog HDL中,实数就是浮点数,通常有两种表示方法:
· 十进制格式:由数字和小数点组成,且必须有小数点
· 指数格式:科学计数法,由数字和字符e或E组成,e或E的前面要有数字而后面必须为整数
⑶字符串:
字符串常量是由一对双引号括起来的字符序列,必须在一行内写完,不能包含回车符。如果字符串被作为Verilog HDL中表达式或者赋值语句中的操作数,则每个字符串被看作8位的ASCII值序列,即一个字符对应8位的ASCII值。
3) 变量的数据类型:
变量,即在程序运行中其值可变的量。Verilog HDL中的变量是用来表示数字电路中的物理连线、数据存储和传送单元等物理量,有19种数据类型,包括wire、reg、parameter、large、integer、medium、scalared、time、small、tri、trio、tril、triand、trior、trireg、vectored、wand、wor类型。
⑴线网型变量:
线网型变量可以理解为实际电路中的导线,通常表示为结构实体之间的物理连接。导线是被驱动的,但不可能存储任何值,可以使用连续赋值assign或把元件的输出连接到线网等方式提供驱动,给线网提供驱动的赋值和元件就是驱动源,线网的值由驱动源决定。如果没有驱动源连接到线网类型的变量上,则该变量就是高阻的其值为z。
一个线网类型变量可能同时受到几个驱动源的驱动,此时该线网变量的取值由逻辑强度较高的驱动源决定;如果多个驱动源的逻辑强度相同,则取值为不定态。因此,为了模型中所使用的变量与实际情况相一致,常用的线网型变量包括wire和tri型,这两种变量都是用于连接器件单元,它们具有相同的语法格式和功能。wire变量通常用来表示单个门驱动或连续赋值语句驱动的线网类型,三态线tri型变量则用来表示多驱动源驱动同一根线的线网,即用tri类型表示一个net有多个驱动源,或者将一个net声明为tri表示以指示这个net可以是高阻态,这种情况可以推广至wand和triand、wor和trior。
Verilog HDL程序模块中,被声明为input或者inout型的端口,只能被定义为线网型变量,被声明为output型的端口可以被定义为线网型变量或者寄存器型变量,输入输出信号类型缺省时自动定义为wire型。
wire型信号可以用作任何方程式的输入,也可以用作assign语句或实例元件的输出,不可以在initial和always模块中被赋值。wire型信号定义格式为:
wire [msb:lsb] 变量名1,变量名2,...,变量名n;
其中,msb和lsb定义了范围,它们之间以冒号分隔,并且为常数表达式,这种多位的wire型数据也称为wire型向量vector。如果没有定义范围,则默认为1位的变量。示例:
wire a;
wire[7:0] b;
wire[4:1] c,d;
assign c=d;
线网类型除了常用的wire、tri类型外,还有一些其他的线网类型:
线网类型 | 功能说明 | 可综合性说明 |
---|---|---|
wire tri | 表示单元之间的连线,wire为一般连线,tri为三态线 | √ |
supply0 supply1 | 用于对电源建模 | √ |
wand triand | 多重驱动,具有线与特性的线网类型 | |
wor trior | 多重驱动,具有线或特性的线网类型 | |
tri1 tri0 | 上拉电阻,用于开关级建模 | |
trireg | 具有电荷保持特性的线网类型,用于开关级建模 |
wire/tri
a\b | 0 | 1 | x | z |
---|---|---|---|---|
0 | 0 | x | x | 0 |
1 | x | 1 | x | 1 |
x | x | x | x | x |
z | 0 | 1 | x | z |
a\b | 0 | 1 | x | z |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
1 | 0 | 1 | x | 1 |
x | 0 | x | x | x |
z | 0 | 1 | x | z |
a\b | 0 | 1 | x | z |
---|---|---|---|---|
0 | 0 | 1 | x | 0 |
1 | 1 | 1 | 1 | 1 |
x | x | 1 | x | x |
z | 0 | 1 | x | z |
寄存器register型变量可以理解为实际电路中的寄存器,它具有记忆特性,是数据存储单元的抽象,在输入信号消失后可以保持原有的数值不变。寄存器变量需要明确地赋值,并且在被重新赋值前一直保持原值。寄存器数据类型的关键字为reg,只能在initial或always内部通过赋值语句改变寄存器存储的值,在没有被赋值之前默认值为x。在initial和always块内被赋值的每一个信号都必须定义为reg类型。reg数据的格式为:
reg [msb:lsb] 变量名1,变量名2,...,变量名n;
其中,msb和lsb定义了范围,它们之间以冒号分隔,并且为常数表达式,这种多位的reg类型数据也称为reg型向量vector。如果没有定义范围,默认为1位变量。示例:
reg clock;
reg [3:0] regb;
reg [4:1] regc,regd;
寄存器型变量除了常用的reg类型之外,还有其他一些类型:
寄存器类型 | 功能说明 | 可综合性说明 |
---|---|---|
reg | 常用的寄存器型变量 | √ |
integer | 32位有符号整型变量 | √ |
time | 64位无符号时间变量 | |
real | 64位有符号实型变量 |
integer a,b;
real a,b;
time a;
一个端口看成是由互相连接的两个部分组成,一部分位于模块的内部,另一部分位于模块的外部。当在一个模块中调用另一个模块时,端口之间的互联必须遵守一定的规则,否则会报错。
端口的I/O | 端口的数据类型 | |
---|---|---|
Module内部 | Module外部 | |
input | net | net或reg |
output | net或reg | net |
inout | net | net |
⑶memory型:
在数字电路仿真中,经常需要对存储器进行建模。Verilog HDL通过对reg型变量建立数组来对存储区建模,数组中每一个单元通过数组索引进行寻址。Verilog HDL不支持多维数组,也就是只能对存储区字寻址,而不能对存储器的一个字的位寻址。memory型数据是通过扩展reg型数据的地址范围来生成的,格式为:
reg [msb:lsb] 存储器1[upper1:lower1],
存储器2[upper2:lower2],...;
其中,[msb:lsb]定义了存储器中每一个存储单元的大小;存储器名后的[upper1:lower1]则定义了该存储器中有多少这样的存储器;最后使用分号结束定义语句。示例:
reg [7:0] mem[1023:0];
上例定义了一个1024字节的宽度为8位的存储器。对存储器进行地址索引的表达式必须为常数表达式。
也可以使用parameter参数定义存储器的尺寸。示例:
parameter wordwidth=8,memsize=1024;
reg [wordwidth-1:0] mem[memsize-1:0];
上述语句也定义了一个宽度8位的1024个存储单元的存储器。若对该存储器中的某个单元赋值,可以采用以下方式:
mem[1]=2;
而对存储器中每个单元赋值的示例为:
reg [3:0] rama[4:1],rega;
initial
begin
rama[4]=4'hB;
rama[3]=4'h4;
rama[2]=4'h6;
rama[1]=4'hF;
raga=rama[1];
end
为存储器赋值的另一种方法是使用系统任务,但仅限于在电路仿真中使用。
$ readmemb //加载二进制值
$readmemh //加载十六进制值
这些系统任务从指定的文本文件中读取数据并加载到存储器,文本文件必须包括相应的二进制或者十六进制数。
4) parameter语句:
在Verilog HDL中,为了提高程序的可读性和可维护性,用parameter来定义一个标识符代表一个常量,称为符号常量,格式为:
parameter 参数名1=表达式,参数名2=表达式,...,参数名n=表达式;
用parmeter定义的符号常量,通常出现在module内部,常被用于定义状态机的状态、数据位宽和延时大小等。示例:
parameter width=6;
parameter pi=3.14;
parameter byte_size=8,byte_msb=byte_size-1;
4. Verilog HDL运算符和表达式:
Verilog HDL提供了丰富的运算符,按功能可以分为算术运算符、逻辑运算符、关系运算符、等式运算符、缩减运算符、条件运算符、位运算符、移位运算符和拼接运算符等9类。如果按运算符所带操作数的个数来区分,运算符可以分为单目运算符、双目运算符和三目运算符。
1) 算术运算符:
Verilog HDL中,算术运算符又称为二进制运算符,有+、-、*、/、%(求模)四种。在进行运算时,如果操作数的某一位为x或z,则整个表达式的结果为不确定。在进行算术运算时,Verilog HDL根据表达式中变量的长度对表达式的值自动进行调整。自动截断或扩展赋值语句中右边的值以适应左边变量的长度。将负数赋值给reg或其他无符号变量时,Verilog HDL会自动完成二进制补码计算。示例:
module assign_size;
reg[3:0] a,b;
reg[15:0] c;
initial
begin
a=-1; //a为无符号数,其值为1111
b=8;c=8; //b=c=1000
#10 b=b+a; //10111截断为0111
#10 c=c+a; //c=10111
end
endmodule
2) 位运算符Bitwise Operators:
位运算符即将两个操作数按对应位分别进行逻辑运算,包括取反~、按位与&、按位或|、按位异或^、按位同或^~或~^。原来的操作数有几位,则运算结果仍为几位;如果两个操作数的位宽不一样,则仿真软件会自动将短操作数向左扩展到两个操作数位宽一致。如果操作数的某一位为x时不一定产生x结果。
3) 缩位运算符Reduction Operations:
缩位运算符为单目运算符,仅对一个操作数进行运算,运算时按照从右到左的顺序依次对所有位进行运算,并产生一位的逻辑值。缩位运算符见下表:
符号 | & | ~& | | | ~| | ^ | ~^或^~ |
---|---|---|---|---|---|---|
功能 | 按位取与 | 按位与非 | 按位或 | 按位或非 | 按位异或 | 按位同或 |
4) 关系运算符:
关系运算符包括>、<、>=、<=四种。在进行关系运算时,如果声明的关系为假,则返回值是0;如果声明的关系是真,则返回值是1;如果某一位为x或z,则结果为不确定。
5) 等式运算符:
等式运算符是双目运算符,要求有两个操作数,得到的结果是1位的逻辑值,运算符包括==、!=、===、!==。如果得到1,说明声明的关系为真;如果得到0,说明声明的关系为假。
==和!=又称为逻辑等式运算符,操作数中某些位可能是不定值x和高阻值z,其运算结果可能是逻辑0、1或x。而===和!==运算符则不同,它在对操作数进行比较时对某些位的不定值z和高阻值z也进行比较,两个操作数必须完全一致,其结果才是1,否则为0。===和!==运算符常用于case表达式的判别,所以又称为“case等式运算符”。
6) 逻辑运算符:
逻辑运算符中,&&和||为双目运算符,要求有两个操作数;逻辑非!为单目运算符,只要求一个操作数。
7) 移位运算符:
Verilog HDL中有两种移位运算符,左移位<<和右移位>>,运算符前为需移位的数值,后跟位移位数。
8) 位拼接运算符Concatation:
Verilog HDL中有一种比较特殊的位拼接运算符{},用来将两个或多个信号的某些位拼接起来进行运算操作。对于一些信号的重复连接,可以使用简化的表示式{n{A}},这里A是被拼接的对象,n为重复的次数,它表示将信号A重复连接n次。示例:
ain=3'b010;bin=4'b1100;{ain,bin}=7'b0101100;
{3{2'b10}}=6'b101010;
9) 条件运算符Conditional Operation:
这是一个三目运算符,?:,对3个操作数进行运算与C语言中的定义一样。格式:
信号=条件?表达式1:表达式2
当条件成立时,信号取表达式1的值,反之取表达式2的值。示例:
assign out=(sel==0)?a:b;
上述代码,当sel为0,则out=a;若sel为1,则out=b。如果sel为x或z,且a=b=0,则out=0;若a≠b,则out不确定。
10) 优先级别:
优先级 | 运算符 |
---|---|
高 ↓ 低 |
! ~ |
* / % | |
+ - | |
<< >> | |
< > <= >= | |
== != === !== | |
& | |
^ ~^ | |
| | |
&& | |
|| | |
?: |
5. Verilog HDL语句:
1) 过程语句:
Verilog HDL中,多数过程模块都从属于initial和always两个过程语句。
⑴initial语句:
initial语句指定的内容只执行一次,主要用于仿真测试,不能进行逻辑综合。格式:
initial
begin
语句1;
语句2;
......
语句n;
end
示例:memory初始化
initial
begin
for(index=0;index<size;index=index+1)
begin
memory[index]=0;
end
end
示例:为测试变量a、b提供一组激励
'timescale 100ns/100ns
module test;
reg a,b;
initial
begin
a=0;b=0;
#2 a=1;
#2 b=1;
#2 b=0;
#2 a=0;
#2 finish;
end
endmodule
可见initial语句的另一种用途是用来生成激励波型作为电路的测试仿真信号。
在每一个模块中,initial语句使用次数不受限制,所有的initial语句都是从0时刻开始执行。
⑵always语句:
always块内的语句是不断重复执行的,在仿真和逻辑综合中均可使用。声明格式:
always@(<敏感信号表达式event-expression>)
begin
//过程赋值
//if-else, case, casex, casez选择语句
//while, repeat, for循环
//task, function调用
end
always过程语句通常是带触发条件的,触发条件写在敏感信号表达式中,只有当触发条件满足时,其后面的begin-end块语句才能被执行。在整个程序过程中,如果触发事件不断产生,则always中的语句将反复执行。如果一个always语句没有触发条件,则这个always语句将会发生一个仿真死锁。示例:
always # (duty_cycle*period/100) clk=~clk;
Verilog HDL中,“#”为延时符号,上例生成了周期为period(=2*duty_cycle*period/100)的无限延续的信号波形,常用这种方法来描述时钟信号,作为激励信号来测试所设计的电路。
一个模块中可以有多个always语句,每个always语句只要有相应的触发事件产生,对应的语句就执行,这与各个always语句书写的前后顺序无关,它们之间是并行运行的。
⑶敏感信号表达式与边沿触发:
除了延时可以作为触发条件外,常用的触发条件为电平触发和边沿触发。
所谓敏感信号表达式,又称事件表达式或敏感信号列表,即当表达式中变量的值发生改变时,就会引起块内语句的执行。因此,敏感信号表达式中应列出影响块内取值的所有信号,若有两个或以上信号,它们之间用or连接或者用逗号连接。
使用always块设计组合逻辑电路时,在赋值表达式右端参与赋值的所有信号都必须在敏感电平列表中列出,而且将块内的所有输入都列入敏感表是一个好习惯,因为不同的综合工具对不完全敏感表的处理不同,会造成综合输出和RTL描述的仿真结果不一致。
如果在赋值表达式右端引用了敏感信号列表中没有列出的信号,在综合时将会为没有列出的信号隐含产生一个透明锁存器。因为该信号变化不会立刻引起赋值的变化,而必须等到敏感信号电平列表中的某个信号变化时,它的作用才表现出来,即相当于存在一个透明锁存器,把该信号的变化暂存起来,待敏感电平列表中的某个信号变化时再起作用,纯组合逻辑电路不可能做到这一点,综合器会发出警告。
Verilog HDL中,用always块设计时序电路时,敏感列表中包括时钟信号和控制信号,always中if语句的判断表达式必须在敏感电平列表中列出。每一个always块最好只由一种类型的敏感信号触发,而不要将边沿敏感型和电平敏感型信号列在一起。
在同步时序逻辑电路中,触发器状态的变化仅仅发生在时钟脉冲的上升沿或下降沿。Verilog HDL提供了posedge与negedge两个关键字,分别描述上升沿和下降沿。示例:
always @ (posedge clk)
begin
if(!reset)
q=0;
else
q<=d;
end
同步置位/清零是指只有在时钟有效跳变时置位/清零,即才能使触发器的输出分别转换为1或0。所以,不要把置位/清零信号列入always块的事件控制表达式,但是必须在always块中首先检查置位/清零信号的电平。
示例:同步置位/清零的计数器
module sync(out,d,load,clr,clk)
input d,load,clk,clr;
input[7:0] d;
output[7:0] out;
reg[7:0] out;
always @ (posedge clk)
begin
if(!clr) out<=8'h00; //同步清零,低电平有效
else if(load) out<=d; //同步置数
else out<=out+1; //计数
end
endmodule
上例中,敏感信号表达式中没有列出输入信号load、clr,这时因为它们是同步置数、同步清零,这些信号起作用必须有时钟的上升沿来到。
示例:异步清零
module async(d,clk,clr,q)
input d,clk,clr;
output q;
reg q;
always @ (posedge clk or posedge clr)
begin
if(clr) //异步清零
q<=1'b0;
else
q<=d;
end
end
endmodule
异步置位/清零是与时钟无关的,当异步置位/清零信号到来时,触发器的输出立即被置为1或0,不需要等到时钟边沿到来。所以,需要把置位/清零信号列入always块的事件控制表达式。
⑷always和initial并存:
在每一个模块中,使用initial和always语句的次数不受限制,但initial和always块相互不能嵌套。每个initial和always块的关系都是并行的,所有的initial语句和always语句都是从0时刻并行执行。
2) 块语句:
实际电路中的某些操作需要多条Verilog HDL语句才能描述,这时就需要用块语句将多条语句复合在一起。块语句包括串行块begin-end和并行块fork-join。当块语句只包含1条语句时,块标识符可以省略。
⑴串行块begin-end:
块内的语句按照顺序执行,即只有上面一条语句执行完后下面的语句才能执行。如果语句前面有延时符号#,那么延时的长度是相对于前一条语句而言的。
延时器示例:
'timescale 100ns/100ns //定义仿真时间单位为100ns
module begin_end(dout,din)
input din;
output dout;
wire din;
reg dout;
reg temp1,temp2;
always @ (din)
begin
#1 temp1=din;
#1 temp2=temp1;
#1 dout=temp2;
end
endmodule
⑵并行块fork-join:
块内语句是同时执行的,即程序流程控制一进入到该并行块,块内所有语句开始同时并行地执行。如果语句前面有延时#符号,那么延时长度是相对于fork-join块的开始时间而言的。
3) 赋值语句:
Verilog HDL有两种为变量赋值的方法,一种是连续赋值,另一种是过程赋值,过程赋值又分为阻塞赋值和非阻塞赋值。
⑴连续赋值:
连续赋值常用于数据流行为建模。连续赋值语句,位于过程块语句外,常以assign为关键字,是为线网型变量提供驱动的一种方法,它只能为线网型变量赋值,并且线网型变量也必须用连续赋值的方法赋值。线网型变量可以理解为实际电路中的导线,那么连续赋值就是给导线提供驱动的方法,也就是连续赋值把导线连到驱动源上。示例:
wire adder_out;
assign adder_out=multi_out+out;
上面两条语句的功能等价于:
wire adder_out=multi_out+out; //隐含了连续赋值语句
带函数调用的连续赋值语句示例:
assign c=max(a,b);
连续赋值语句中等号左边必须是线网型变量,右边可以为线网型、寄存器型变量或者是函数调用语句。连续赋值语句属于即刻赋值,即赋值号右边的运算值一旦变化,被赋值变量立刻随之变化,驱动源的任何毛刺都会赋值给左边的变量。
assign可以使用条件运算符进行条件判断后赋值。示例:
module compare2(equal,a,b)
input[1:0] a,b;
output equal;
assign equal=(a==b)?1:0;
endmodule
上述代码描述了一个名为compare2的比较器,对比两比特数a和b,如二者相等,则输出equal高电平,否则为低电平。
⑵过程赋值:
过程赋值多用于对reg型变量进行赋值,这类型变量在被赋值后,其值保持不变,直到赋值进程又被触发,变量才被赋予新值。过程赋值主要出现在过程块always和initial语句内,分为阻塞赋值和非阻塞赋值两种。
· 非阻塞Non_Blocking赋值方式:
非阻塞语句用操作符号“<=”进行连接,如b<=a。非阻塞赋值在整个过程块结束后才完成赋值操作,即b的值并不是立刻就改变,这是一种比较接近真实的电路赋值和输出,因为从综合的角度考虑了延时和并行性。
如果在一个块语句中有多条非阻塞赋值语句,在过程赋值启动后,当执行某条非阻塞赋值语句时,仅仅计算“<=”右侧的表达式的值,但并不马上执行赋值,然后执行后面的操作。这个过程就好像没有阻断程序的执行,因而被称为非阻塞赋值,连续的非阻塞赋值操作是同时完成的,即在同一个顺序块中,非阻塞赋值表达式的书写顺序不影响赋值的结果。示例:
module non_bloking(reg_c,reg_d,data,clk);
input clk,data;
output reg_c,reg_d;
reg reg_c,reg_d;
always@(posedge clk)
begin
reg_c<=data;
reg_d<=reg_c;
end
endmodule
上述代码中,在data上的任何变化将花费两个时钟周期传播到reg_d,reg_d的值落后于reg_c的值一个时钟周期,这是因为always块中的语句是同时执行的,每次执行完毕后,reg_c的值得到更新,而reg_d的值仍然是上一个时钟周期的reg_c值。对应电路用到两个触发器。
· 阻塞Blocking赋值方式:
阻塞语句用操作符号“=”连接,比如b=a。阻塞赋值在该语句结束时就立刻完成赋值操作,即b的值在该语句结束后立刻改变。
如果在一个块语句中有多条阻塞赋值语句,那么写在前面的赋值语句没有完成之前,后面的语句就不能被执行,彷佛被阻塞一样,因而被称为阻塞赋值。连续的阻塞赋值操作是顺序完成的。示例:
module blocking(reg_c,reg_d,data,clk);
input clk,data;
output reg_c,reg_d;
reg reg_c,reg_d;
always@(posedge clk)
begin
reg_c=data;
reg_d=reg_c;
end
endmodule
上述代码中,reg_d的值和reg_c的值一样,因为reg_d直到reg_c已经更新之后才被更新,二者的更新必须在一个时钟周期内发生。电路中只用一个触发器来寄存data的值,同时输出给reg_d和reg_c。
为了避免出错,在同一个always块内,最好不要将输出再作为输入使用,为了使用阻塞赋值方式完成与前面的非阻塞赋值同样的功能,可采用两个always块来实现,两个always块是并发执行的。示例:
module blocking(reg_c,reg_d,data,clk);
input clk,data;
output reg_c,reg_d;
reg reg_c,reg_d;
always@(posedge clk)
begin
reg_c=data;
end
always@(posedge clk)
begin
reg_d=reg_c;
end
endmodule
· 非阻塞和阻塞赋值使用的注意事项:
在always块描述组合逻辑(电平敏感)时使用阻塞赋值;在使用always块描述时序逻辑(边沿敏感)时使用非阻塞赋值;建立latch模型时,采用非阻塞语句;在一个always块中同时有组合逻辑和时序逻辑时,采用非阻塞赋值语句。
不要在同一个always块内同时使用阻塞赋值和非阻塞赋值。无论是使用阻塞赋值还是非阻塞赋值,不要在不同的always块内为同一个变量赋值,因为很难保证不会引起赋值冲突。
4) 条件语句:
条件语句有if-else语句、case语句,都是顺序语句,应被放在always块中。
⑴if-else语句:
if语句作为一种条件语句,根据语句中所设置的一种或多种条件,有条件地执行指定的顺序语句。if语句大致有三种形式:
· 一个if形式:
if (表达式) 语句
· if-else形式:
if (表达式) 语句1;
else 语句2;
· if-else嵌套形式:
if (表达式1) 语句1;
else if(表达式2) 语句2;
else ......;
else if(表达式m) 语句m;
else 语句n;
上面三种形式中,if后面的表达式一般为逻辑表达式或关系表达式,有可能是一位的变量。执行if语句时,系统首先计算表达式的值,若结果为0、x、z按假处理,若结果为1按真处理,执行相应的语句,每条语句必须以分号结束。语句可以是单语句,也可以是多句,多句时使用begin-end块语句括起来,此时end后不需要再加分号。多分支示例:
always@(sela or selb or a or b or c)
begin
if (sela) q=a;;
else if(selb) q=b;
else q=c;
end
第3种形式,从第一个条件表达式开始依次判断,直到最后一个条件表达式判断完毕,如果所有的表达式都不成立,才会执行else后面的语句,这种判断上的先后顺序本身隐含一种优先级关系。上述代码也可以用多个if语句实现:
always@(sela or selb or a or b or c)
begin
q=c;
if (selb) q=b;;
if(sela) q=a;
end
设计时通常知道哪一个信号到达的时间要晚一些,使到达晚的信号离输出近一些,以提高逻辑性能。上例中,输入信号a处于选择链的最后一级,也就是说a最靠近输出。
在使用if-else嵌套时,要注意与if配对的else语句,通常else与最近的if语句配对。示例:
module interrupt(active,int0,int1,int2,int3);
input int0,int1,int2,int3;
output [3:0] active;
reg [3:0] active;
always@(int0 or int1 or int2 or int3)
begin
active[3:0]<=4'b0;
if (int0) active[0]<=1'b1;
else if(int1) active[1]<=1'b1;
else if(int2) active[2]<=1'b1;
else if(int3) active[3]<=1'b1;
end
endmodule
上述代码为一个中断优先级实现,int0、int1、int2、int3具有从高到低的优先级。
⑵case语句:
实际问题中常常需要用到多分支选择,Verilog HDL提供了case语句,常用于描述译码器、数据选择器、状态机及微处理器的指令译码等。一般形式为:
case(表达式)
分支表达式1:语句1;
分支表达式2:语句2;
......
分支表达式n:语句n;
default:语句n+1;
endcase
执行时,首先计算case后面的表达式,然后与各分支表达式的值进行比较,如果与分支表达式1的值相等就执行语句1,与分支表达式2的值相等就执行语句2,以此类推。如果与上面列出的分支表达式的值都不相同,就执行default后面的语句。每个分支项中的语句可以是单条语句,也可以是多条语句,如果是多条语句则必须用begin-end块语句括起来构成复合语句。执行完任何一条分支项的语句后,跳出该case语句结构,终止case语句的执行。多个条件分支处于同一个优先级。示例:
module decoder(sel,res);
input[2:0] sel;
output [7:0] res;
reg [7:0] res;
always@(sel or res)
begin
case(sel)
3'b000:res=8'b00000001;
3'b001:res=8'b00000010;
3'b010:res=8'b00000100;
3'b011:res=8'b00001000;
3'b100:res=8'b00010000;
3'b101:res=8'b00100000;
3'b110:res=8'b01000000;
default:res=8'b10000000;
endcase
end
endmodule
在case语句中,表达式与分支表达式之间的比较是全等比较,必须保证两者的对应位全等。如果表达式的值和分支表达式的值同时为不定值或者同时为高阻态,则认为相等。
case语句还有casex和casez两个变种。在casez语句中,将忽略比较过程中值为z的位,即如果比较的双方有一方的某一位的值是z,那么对这些位的比较就不予考虑,只需关注其他位的比较结果;而在casex语句中,则把这种处理方式进一步扩展到对x的处理,即将z和z均视为无关值。这可以使设计人员更加灵活地设置以对信号的某些位进行比较。示例:
casez(encode)
4'b1???:high_lvl=3;
4'b01??:high_lvl=2;
4'b001?:high_lvl=1;
4'b0001:high_lvl=0;
default:high_lvl=0;
endcase
上述代码中,如果最高位为1,其他位不予考虑,输出为3。case的真值表:
case | 0 | 1 | x | z | casez | 0 | 1 | x | z | casex | 0 | 1 | x | z |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 |
1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 |
x | 0 | 0 | 1 | 0 | x | 0 | 0 | 1 | 1 | x | 1 | 1 | 1 | 1 |
z | 0 | 0 | 0 | 1 | z | 1 | 1 | 1 | 1 | z | 1 | 1 | 1 | 1 |
使用条件操作符也能实现条件结构。示例:
assign out=(a>b)?1:0;
上述代码实现1位数值比较器。
⑷条件的描述完备性:
如果if和case语句的条件描述不完备,或造成不必要的锁存器保持原值,因此应注意列出所有分支。一般不可能列出所有分支,因为一个变量至少有0、1、x、z四种取值,可在if语句最后加上else,在case语句最后加上default语句。
当case语句的条件没有完全译码时,会引起具有优先级的电路结构,而且若某两个分支选项相互重叠时,case所暗含的优先级顺序就起作用,在前面的分支项优先级高,并在编译时会出现警告。如果想强制DC将电路综合成没有优先级的结构,可以使用DC的综合指令//synopsys parallel_case语句,告诉后面的电路综合成为平行的电路结构。
若用//synopsys full_case语句,则综合器会自动对没有列出的情况赋值,并且它赋的值有利于减少逻辑资源的消耗,对于不关心的情况就给它一个x,化简时既可以作为0,也可以作为1。加上综合指令后,综合的电路与实际Verilog HDL模型表示的电路可能不一致,因此使用时必须谨慎。
5) 循环语句:
Verilog HDL中存在4种类型的循环语句,forever、repeat、while、for,用来控制执行的执行次数,且只能在initial和always语句内部使用。其中,for语句能被大多数综合工具所支持,其他3种在仿真时用得较多,不一定能被综合工具支持。
⑴forever:连续的执行语句
forever表示永久循环,无条件地无限次执行其后的语句,相当于while(1),直到遇到系统任务$finish或$stop。如果需要从循环中退出,可以使用disable。格式:
forever 语句;
其中语句可以使用begin-end括起来的多句,即:
forever
begin
语句1;
语句2;
......
end
forever循环多用于生成时钟等周期性波型,不能独立写在程序中,而必须写在initial块中。示例:
initial
begin
clk=0;
forever #25 clk=~clk;
end
上例产生时钟波型,在0时刻首先被初始化为0,此后每隔25个时间单位,clk反相一次。主要用于测试,不能进行逻辑综合。
⑵repeat:连续执行一条语句n次
repeat语句执行其表达式所确定的固定次数的循环操作,循环次数表达式通常为常量表达式,用于指定循环次数。格式:
repeat (循环次数表达式) 语句;
其中语句也可以使用begin-end括起来的多句。如果循环次数表达式的值不确定,即x或z,则循环次数按0处理。使用repeat循环实现的0位二进制乘法示例:
module multi_acc(out,opa,opb);
parameter size=8,longsize=16;
input[size:1] opa,opb;
output[longsize:1] out;
wire[size:1] opa,opb;
reg[longsize:1] out;
reg[size:1] temp_opa,temp_opb;
always@(opa or opb)
begin
out=0;
temp_opa=opa;
temp_opb=opb;
repeat(size);
begin
if(temp_opb[1]) //如果opb的最低位为1
out=out+temp_opa;
temp_opa=temp_opa<<1; //opa左移一位
temp_opb=temp_opb>>1; //opb右移一位
end
end
endmodule
⑶while语句:执行一条语句直到某个条件不满足
while语句在执行时,首先判断表达式是否为真,若为真则执行后面的过程语句,否则就不执行循环体。如果条件表达式的值为x或z,则按0(假)处理。格式:
while(表达式) 语句;
其中语句也可以使用begin-end括起来的多句。while循环示例代码:
initial
begin
count=0;
while(count<101)
begin
$display("count=%d",count);
count=count+1;
end
end
上例代码实现从0到100计数并显示出来。
⑷for:有条件的循环语句
格式:
for(循环变量赋初值;条件表达式;循环变量增值)
begin:block_name
语句
end
其中语句必须使用begin-end括起来的单句或多句。for通过三个步骤来决定语句的循环执行:
· 先给控制循环次数的循环变量赋初值
· 判定控制循环的条件表达式的值,如为假则跳出循环语句,如为真则执行指定的语句后,转到下一步
· 执行一条赋值语句来修正控制循环变量次数的变量的值,然后返回上一步
示例:
integer i;
always@(inp or cnt)
begin
result[7:4]=0;
result[3:0]=inp;
if(cnt==1)
begin
for(i=4;i<7;i=i+1)
begin
result[i]=result[i-4];
end
result[3:0]=0;
end
end
⑸disable语句:
一般情况下,循环语句留有正常的出口用于退出循环,但在有些特殊情况下,仍需要强制退出循环,disable就是强制退出循环的方法。要使用disable强制退出循环,首先要给循环部分一个名字,是通过在begin后面添加“:名字”实现。事实上,disable可以中止任何有名字的begin-end块。示例:
begin:continue
for(i=0;i<5;i=i+1)
begin
sum=sum+1;
if(i==3) disable continue;
end
end
6. Verilog HDL的task和function:
Verilog HDL是分模块来对系统进行描述的,但有时这种划分并不一定方便,因此提供了任务和函数的描述方法。可在一个模块中,将一些重复描述部分或功能比较单一的部分,作为一个任务或函数相对独立地进行描述,从而简化程序的结构,增强代码的易懂性,便于理解和调试。task和function的定义及调用,都包含在一个module的内部。格式与module模块类似,一般用于行为建模,在编写测试验证程序时用得较多,但很多逻辑综合软件并不能很好地支持任务和函数。
1) 任务task:
task类似于一般编程语言中的过程process,它不带返回值,因此不能用于表达式中。task可以从描述的不同位置执行共同的代码,通常把需要共用的代码段定义为task,然后通过task调用来使用。
⑴task的定义:
task <任务名>;
//定义端口以及内部变量
input 输入端口名;
output 输出端口名;
inout 双向端口名;
wire 内部变量名;
reg 内部变量名;
//任务主体
begin
语句1;
语句2;
......
语句n;
end
endtask
任务定义结构不能出现在任何一个过程块的内部,在任务内部定义的变量,作用域是在task和endtask之间。任务只有在被调用时才执行。
⑵task的调用:
启动任务并传递输入输出变量的语法格式:
<任务名> (端口1,端口2,...,端口n);
task调用语句是过程性语句,因此只能出现在always和initial过程块中,调用task的输入与输出参数必须是寄存器类型的;调用时,参数列表必须与任务定义时的输入、输出和双向端口参数说明的顺序相匹配。示例:
任务定义:
task test;
input a,b,c;
output d,e;
d=a&b;
e=a|c;
endtask
任务调用:
task(in1,in2,in3,out1,out2);
调用任务test时,变量in1、in2、in3的值赋给a、b、c,而任务执行完后,d和e的值赋给了out1和out2。
交通灯时序控制示例:
module traffic_lights:
reg clock,red,amber,green;
parameter on=1,off=0,red_tics=350,amber_tics=30,gereen_tics=200;
//init
initial red=off;
initial amber=off;
initial green=off;
//控制时序
always
begin
red=on;
light(red,red_tics);
amber=on;
light(green,green_tics);
amber=on;
light(amber,amber_tics);
end
//定义开启时间的任务
task light;
output color;
input[31:0] tics;
begin
repeat(tics)@(posedge clock); //等待tics个时钟上升沿
color=off;
end
endtask
//产生时钟脉冲的always块
always
begin
#100 clock=0;
#100 clock=1;
end
endmodule
上例中描述了一个简单的交通灯时序控制,并且该交通灯有自己的时钟产生器。不过对于repeat(tics)@(posedge clock)这种语句,系统是综合不了的,即系统不能把它转换成具体的电路。
CPU总线控制的示例:
`timescale 1ns/10ps
module bus_ctl_tb:
reg[7:0] data;
reg data_valid,data_rd;
cpu u1(data_valid,data,data_rd);
initial
begin
cpu_driver(8'b0000_0000);
cpu_driver(8'b1010_1010);
cpu_driver(8'b0101_0101);
end
//定义任务
task cpu_driver;
input[7:0] data_in;
begin
#30 data_valid=1;
wait(data_rd==1);
#20 data=data_in;
wait(data_rd==0);
#20 data=8'hzz;
#30 data_valid=0;
end
endtask
endmodule
2) 函数function:
函数的目的是返回一个用于表达式的值。函数至少需要一个参数,且参数必须都为输入端口,不可以包含输出端口或双向端口;函数必须有一个返回值,返回值被赋值给和函数名同名的变量,这也决定了函数只能存在一个返回值。
⑴function的定义:
function [返回值位宽或类型说明] 函数名;
//定义端口及内部变量
input 输入端口名;
wire 内部变量名;
reg 内部变量名;
//函数主体
begin
语句1;
语句2;
......
语句n;
end
endfunction
函数定义结构不能出现在任何一个过程块的内部。在函数内部定义的变量,作用域是在function和endfunction之间;函数定义不能包含任何时间控制语句,即任何#、@或wait标识的语句;不含有非阻塞赋值语句;在函数的定义中必须有一条赋值语句给函数的一个内部变量赋值以函数的结果值,该内部变量具有和函数名相同的名字。示例:
function mux4to1;
//定义端口及内部变量
input [3:0] X;
input[1:0] S4;
case(S4)
0:mux4to1=X[0];
1:mux4to1=X[1];
2:mux4to1=X[2];
3:mux4to1=X[3];
endcase
endfunction
上例定义了四选一函数。
⑵function调用:
函数的调用是通过将函数作为表达式的操作数实现的。function调用既可以出现在过程块中,也可以出现在assign连续赋值语句中。格式:
<函数名>(<表达式> <表达式>);
示例:
module mux16to1(W,S,f);
input [15:0] W;
input [3:0] S;
output reg f;
always@(W,S)
case(S[3:2])
0:f=mux4to1(W[0:3],S[1:0]);
1:f=mux4to1(W[4:7],S[1:0]);
2:f=mux4to1(W[8:11],S[1:0]);
3:f=mux4to1(W[12:15],S[1:0]);
endcase
endmodule
上例是使用前面定义的四选一函数实现的十六选一。下面为乘累加器MAC示例:
module mac(out,a,b,clk,clr);
output [15:0] out;
input [7:0] a,b;
input clk,clr;
wire [15:0] sum;
reg [15:0] out;
function[15:0] mult;
input[7:0] a,b;
reg[15:0] result;
integer i;
begin
result=a[0]?b:0;
for(i=1;i<=7;i=i+1)
begin
if(a[i]==1) result=result+(b<<i);
end
mult=result;
endfunction
assign sum=mult(a,b)+out;
always@(posedge clk or posedge clr)
begin
if(clr) out<=0;
else out<=sum;
end
endmodule
3) task和function的不同:
function可以调用其他函数,但不能调用task,而task可以调用其他任务和函数。function只能与主模块共用一个仿真时间单位,而task可定义自己的仿真时间单位。
函数定义不能包含任何时间控制语句,而任务定义则可以包含时间控制语句。function至少要有一个输入变量,不能有输出和双向变量,而task可以没有变量或有多种任何类型的变量。function返回一个值,而task则不返回值,但因为task参数可以定义输出端口或双向端口,实际上任务可以返回多个值。
7. Verilog HDL的编译向导与宏:
Verilog HDL与C语言一样也提供了编译向导Compiler Directives的功能,就是在程序编译之前进行的预处理,然后将预处理结果和源程序一起进行编译处理。
Verilog HDL提供了十几条编译向导,都以反引号“`”开头,其作用范围为定义命令之后到文本结束或其他命令定义替代该命令为止。这里介绍常用的`define、`include、`timescale及条件编译指令。
1) 宏定义`define:
宏定义指定一个宏名代表一个字符串,在编译之前,编译器先将程序中出现的标识符全部替换为它所表示的字符串。一般形式为:
`define 标识符(宏名) 字符串(宏内容)
宏名可以用大写字母表示,也可以使用小写字母表示,建议使用大写字母,以与变量名区别。程序中,引用宏名的方法是在宏名前面加上反引号“`”。宏内容可以是空格,这种情况下宏内容为空,当引用这个宏名时,不会有内容替换。宏定义示例:
`define WORDSIZE 8
reg[1:`WORDSIZE] data;
上例中,当需要改变某个变量时,只需要改变`define命令行,可以提高程序的可移植性和可读性,作用类似parameter型变量。分频器示例:
`define N 3
`define DataWidth `N
module clk_div8(clkout,clkin);
input clkin;
output clkout;
reg clkout;
reg clkin;
reg[`DataWidth-1:0] counter;
always@(posedge clkin)
begin
if(counter==`N)
begin
counter<=0;
clkout<=~clkout;
end
else counter<=counter+1;
end
endmodule
偶数倍分频是最简单的一种分频模式,完全可以通过计数器计数实现,如要进行N倍偶数分频,可由待分频的时钟触发计数器计数。对一个2x分频的电路,counter上限值是N=x-1,通过修改N的值就能得到相应的偶数倍分频。
宏定义主要起到两个作用,一是使用有意义的标识符取代程序中反复出现的含义不明显的字符串,二是用一个较短的标识符替代反复出现的较长的字符串。示例:
`define sum1 ina+inb+inc+ind
module calculate(out1,out2,ina,inb,inc,ind,ine);
input ina,inb,inc,ind,ine;
output[2:0] out1,out2;
wire ina,inb,inc,ind,ine;
reg[2:0] out1,out2;
always@(ina or inb or inc or ine)
begin
out1=`sum1+ine;
out2=`sum1-ine;
end
endmodule
使用时需要注意,宏定义不是Verilog HDL语句,不必在行末加分号,如果加了分号会连分号一起置换。宏定义可以出现在程序的任意位置,通常写在模块定义的外面,在此程序内有效。如果对同一宏做了多次定义,则只有最后一次定义有效。在进行宏定义时,可以引用已定义的宏名,实现层层置换。
在编译之前,所有引用的宏名被替换为内容,这个过程只做简单置换,不做任何语法检查,只有在编译源程序时才会检查错误。
2) 文件包含语句`include:
文件包含语句可以将一个文件全部包含到另一个文件中。通常一个复杂的设计可能包含很多模块,各模块都单独保存为一个文件,当顶层模块调用子模块时,就需要到相应的文件中去寻找。Verilog HDL提供了`include命令用来实现文件包含操作,格式为:
`include "文件名"
`include命令可以出现在程序的任意位置,被包含的文件名可以是相对路径名,也可以是绝对路径名。示例:
`incluce "source1.v"
`incluce "source2.v"
`incluce "source3.v"
module top;
source1 source1(); //调用source1模块
source2 source2(); //调用source2模块
source3 source3(); //调用source3模块
endmodule
3) 条件编译:
一般情况下,Verilog HDL源程序中所有的行都将参加编译。但是有时希望对其中的一部分内容只有在满足某种条件下才进行编译,即条件编译。条件编译有几种形式。
`ifdef 宏名
程序段
`endif
这种形式,当宏名在程序中被定义过的话(使用`define定义),则编译下面的程序段,否则不编译。
`ifdef 宏名
程序段1
`else
程序段2
`endif
这种形式,当宏名在程序中已经被定义过的话(使用`define定义),则对程序段1进行编译,程序段2忽略;否则编译程序段2,程序段1忽略。
4) 时间尺度命令`timescale:
用于定义模块的仿真时间单位和时间精度。格式:
`timescale <time_unit>/<time_precision>
用于说明时间单位和时间精度的参量值的数字必须为整数,其有效数字为1、10、100,单位为s、ms、us、ns、ps、fs。
时间尺度命令在模块说明外部出现,并且影响后面的所有时延值。
8. Verilog HDL常用的建模方式:
Verilog HDL的基本功能是描述硬件逻辑电路。同一个硬件逻辑电路,可以采用不同的方式进行描述,根据描述方式不同,常用的建模方式可分为结构化描述方式、数据流描述方式和行为描述方式。
一个复杂的电路系统可划分为几个不同的抽象级别:系统级、算法级(也称行为级)、寄存器传输级(RTL级)、逻辑门级、晶体管开关级。用户也可以针对电路系统特点在不同抽象层次上对硬件电路进行建模。
1) 常用的建模描述方式:
为了实现系统的逻辑功能,对同一系统进行设计时可以采用多种描述方式,可以按照系统的电路图实例化已有的功能模块或原语,可以采用连续赋值语句assign,也可以使用always语句或initial语句块中的过程赋值语句,上面三种分别对应结构化建模描述方式、数据流建模描述方式和行为建模描述方式。
⑴结构化建模描述方式:
结构化建模描述方式通过对电路结构的描述来建模,即实例化Verilog HDL中内置的基本门级元件、实例化已有模块、实例化用户定义的原语,并使用线网来连接各器件,描述出逻辑电路图中的元件及元件之间的连接关系。
· 实例化基本门级元件:
Verilog HDL内置了12种类型的基本门级元件模型,包括多输入与门、多输入与非门、多输入或门、多输入或非门、多输入异或门、多输入异或非门、多输出缓冲器、多输出反相器、控制信号高/低电平有效的三态缓冲器、控制信号高/低电平有效的三态反相器等。
类型 | 元件符号 | 功能说明 |
---|---|---|
多输入门 | and | 多输入端的与门 |
nand | 多输入端的与非门 | |
or | 多输入端的或门 | |
nor | 多输入端的或非门 | |
xor | 多输入端的异或门 | |
xnor | 多输入端的异或非门(同或门) | |
多输出门 | buf | 多输出端的缓冲器 |
not | 多输出端的反相器 | |
三态门 | bufif0 | 控制信号低电平有效的三态缓冲器 |
bufif1 | 控制信号高电平有效的三态缓冲器 | |
nofif0 | 控制信号低电平有效的三态反相器 | |
nofif1 | 控制信号高电平有效的三态反相器 |
实例化多输入门元件一般形式为:
元件符号 实例化名(输出,输入);
示例:
nand NA1(out,in1,in2,in3);
xor XOR1(out,in1,in2,in3);
其中,实例化名NA1、XOR1可省略,括号中的输入变量、输出变量须与原理图对应且第1个变量必须是输出变量。
四选一多路选择器示例:
module mux4_to_1_struct(out,in0,in1,in2,in3,s0,s1);
output out;
input in0,in1,in2,in3;
input s0,s1;
wire s0n,s1n; //内部线网
wire t0,t1,t2,t3;
//实例化逻辑门级原语
not N0 (s0n,s0);
not N1 (s1n,s1);
and A0 (t0,in0,s1n,s0n); //实例化三输入与门
and A1 (t1,in1,s1n,s0);
and A2 (t2,in2,s1,s0n);
and A3 (t3,in3,s1,s0);
or OR (out,to,t1,t2,t3); //实例化三输入或门
endmodule
上例中的四选一多路选择器由2个多输出反相器、4个多输入与门、1个多输入或门组成。
· 实例化模块:
可以实例化用户定义的模块,这时不能省略实例化名。采用实例化一位加法器设计四位全加器的示例:
module FA_struct(A,B,Cin,Sum,Cout);
input A,B,Cin;
output Sum,Coutl
wire S1,T1,T2,T3; //内部线网
xor X1(S1,A,B); //实例化逻辑门级原语
xor X2(Sum,S1,Cin);
and A1(T1,A,B);
and A2(T2,B,Cin);
and A3(T3,A,Cin);
or OR1(Cout,T1,T2,T3);
endmodule
//
module Four_bit_FA(A,B,Cin,Sum,Cout);
input [3:0] A,B;
input Cin;
output [3:0] Sum;
output Cout;
wire c1,c2,c3; //内部线网
FA_struct FA0(.A(A[0]),.B(B[0]),.Cin(Cin),.Sum(Sum[0]),.Cout(c1));
FA_struct FA1(.A(A[1]),.B(B[1]),.Cin(Cin),.Sum(Sum[1]),.Cout(c2));
FA_struct FA2(.A(A[2]),.B(B[2]),.Cin(Cin),.Sum(Sum[2]),.Cout(c3));
FA_struct FA3(.A(A[3]),.B(B[3]),.Cin(Cin),.Sum(Sum[3]),.Cout(Cout));
endmodule
已有的设计模块FA_struct对于顶层模块相当于一个现成的器件,顶层模块只要对其实例化就可以。实例化中,端口映射采用名字关联,其中A表示实例化器件一位全加器FA_struct的管脚A,括号中的信号表示上层模块四位全加器Four_bit_FA接到该管脚A的具体信号。
器件的端口映射采用名字关联方式实例化模块时,端口的排列次序是任意的,也可以采用“FA_struct FA0
· 用户定义原语:
Verilog HDL不仅为设计者提供了一套标准的原语,而且还具有定义原语的能力,设计者可以根据需要编写自己的原语。
用户自定义原语主要由5个部分组成:UDP名和端口列表、端口说明语句、UDP初始化、UDP状态表、UDP定义的结束。具体格式:
primitive <udp_name> (<输出端口名>,<输入端口名>);
output <输出端口名>;
input <输入端口名>;
reg <输出端口名>;
initial <输出端口名>=<值>;
table
<状态表>
endtable
endprimitive
UDP定义以关键字primitive开始,以关键字endprimitive结束;在定义时序逻辑的UDP时输出端口必须声明为reg类型,而且需要一条initial语句对时序逻辑UDP的输出端口进行初始化;UDP的状态表是实现UDP功能最重要的部分,以关键字table开始,以endtable结束,定义了输入状态、当前状态和输出状态的对应关系,类似逻辑电路的真值表。
用户原语UDP允许有多个输入,但只允许有一个输出,而且输出口必须在端口列表的第一个位置;UDP不支持inout端口。表示时序逻辑的UDP需要保持状态,因此其输出口必须为reg类型。状态表的语法格式为:
<input1> <input2> ... <inputN>:<output>;
输入的顺序必须与端口列表中出现的顺序相同,输入与输出之间以冒号“:”分隔,每一行以分号结束。状态表中的内容与逻辑电路真值表相似,可根据真值表填写,其中的值可以为0、1、x,但不能处理z,传入的z当作x处理。
UDP与其他模块同级,不能在其他模块内定义UDP。在UDP中不能实例化其他模块或者原语,但可以在其他模块内部实例化UDP,实例化方法与门级原语的实例化方法相同。
两输入与非门的UDP示例:
primitive udp_nand(out,a,b);
output out;
input a,b;
table
//a b : out
0 0 : 1;
0 1 : 1;
1 0 : 1;
1 1 : 0;
endtable
endprimitive
上例中,当两个输入中一个为0,不论另外一个是何值,输出都是1。这个不影响输出值的另一个输入称为无关项,可用“?”表示,可自动展开为0、1、x。改写代码为:
primitive udp_nand(output out,input a,input b);
table
//a b : out
0 ? : 1;
? 0 : 1;
1 1 : 0;
endtable
endprimitive
上例中在端口列表中声明端口类型。
UDP有表示组合逻辑的和表示时序逻辑的两种。组合逻辑电路的输出仅仅取决于输入信号,时序逻辑电路的下一个输出值不但取决于当前的输入值,还跟电路的当前状态有关。表示时序逻辑的UDP状态表包括输出的当前状态,格式为:
<input1> <input2> ... <inputN>:<current state>:<next state>;
其中,输入部分、当前状态、下一个状态之间用冒号分隔。状态表的输入项可以是电平敏感或跳变沿敏感。状态表中必须列出所有的可能组合,以避免出现不确定的输出值。
电平敏感锁存器示例:
primitive udp_latch(q,d,clock);
output q;
reg q;
input d,clock;
initial q=0;
table
//d clock : q : q+
1 1 : ? : 1;
0 1 : ? : 0;
? 0 : ? : -;
endtable
endprimitive
下降沿触发D触发器示例:
primitive udp_edge_d(output reg q=0,input d,clock);
table
//d clock : q : q+
1 (10) : ? : 1; //在clock的下降沿将输入值1锁存到q
0 (10) : ? : 0; //在clock的下降沿将输入值0锁存到q
? (1x) : ? : -; //clock变化到不定状态时q保持不变
? (0?) : ? : -; //忽略clock正跳变
? (x1) : ? : -; //忽略clock正跳变
(??) ? : ? : -; //当clock为某个值,不变化时,忽略d的变化
endtable
endprimitive
Verilog HDL提供了常用的状态表缩写符号:
缩写符号 | 含义 | 解释 |
---|---|---|
? | 0,1,x | 不能用于输出 |
b | 0,1 | 不能用于输出 |
- | 维持原值不变 | 只能用于时序UDP的输出 |
r | (01) | 信号的上升沿 |
f | (10) | 信号的下降沿 |
p | (01),(0x),(x1) | 可能是信号的上升沿 |
n | (10),(1x),(x0) | 可能为信号的下降沿 |
* | (??) | 信号的任意变化 |
⑵数据流建模描述方式:
在数字电路中,信号经过逻辑电路的过程就像数据在电路中流动,即信号从输入流向输出,当输入变化时,总会在一定的时间之后在输出端呈现出来。模拟数字电路的这一特性,对其进行建模的方式称为数据流建模描述方式。数据流描述说明数据在寄存器间移动,描述的是硬件的寄存器级实现,与硬件电路中的器件相对应。数据流建模最基本的方法就是使用连续赋值语句assign。示例:
`timescale 1ns/100ps
module FA_dataflow(A,B,Cin,Sum,Cout);
input A,B,Cin;
output Sum,Cout;
wire S1,T1,T2,T3;
assign #2 S1=A^B;
assign #2 Sum=S1^Cin;
assign #2 T1=A&B;
assign #2 T2=A&Cin;
assign #2 T3=B&Cin;
assign #2 Cout=T1|T2|T3;
endmodule
在数据流描述方式中,可以借助Verilog HDL提供的一些运算符,如按位与&、按位或|等。也可以采用条件操作符语句等。
⑶行为建模描述方式:
Verilog HDL支持设计者从算法角度,即直接根据电路的外部行为进行建模,而与硬件电路无关,这种建模方式称为行为建模方式。行为建模方式,从一个抽象角度来表示电路,通过定义输入-输出响应的方式描述硬件行为,一般把initial块或always块描述的建模方法归为行为建模方式。示例:
module FA_behav1(A,B,Cin,Sum,Cout);
input A,B,Cin;
output Sum,Cout;
reg Sum,Cout;
reg S1,T1,T2,T3;
always@(A orB or Cin)
begin
Sum=(A^B)^Cin;
T1=A&Cin;
T2=B&Cin;
T3=A&B;
Cout=(T1|T2)|T3
end
endmodule
行为建模方式通常需要借助一些形为级的运算符,如加+、减-等。采用行为建模方式进行设计时,语句块中可使用if-else条件语句、case/casex分支语句、for/while/repeat循环语句。
⑷混合设计描述:
复杂数字逻辑电路和系统设计过程中,往往是多种设计模型的混合。对底层模块可采用结构描述、数据流描述、行为级描述,在模块内部可以将结构描述方式、数据流描述方式、行为描述方式自由混合。
2) 抽象分层建模方式:
现在一个芯片上可以集成几亿个晶体管,超大规模集成电路VLSI设计的最大挑战是管理系统的复杂性。将复杂的电路系统进行合理规划,划分为若干个可操作的模块,通过仿真验证无误后,再把这些模块分给多个设计者,可同时设计一个硬件系统中的不同模块。这种层次化、结构化的管理模式是集成电路设计的基本原则。HDL抽象层次描述表:
抽象层次\领域 | 行为领域 | 结构领域 | 物理领域 |
---|---|---|---|
系统级 | 性能描述 | 部件及其逻辑连接 | 芯片、模块、电路板及物理子系统 |
算法级 | I/O应答算法 | 硬件模块数据结构 | 部件之间的物理连接 |
寄存器传输级 | 并行操作寄存器状态表 | ALU、MUX、CPU等之间的物理连接方式 | 芯片、宏单元 |
逻辑门级 | 布尔方程描述 | 门电路 | 标准单元布图 |
开关电路级 | 微分方程表达 | 晶体管、电阻、电容、电感元件 | 晶体管布图 |
⑴系统级和算法级建模方式:
系统级是用高级语言结构实现系统运行的模型,是针对整个系统性能的描述,是系统的最高层次的抽象描述。算法级也称行为级或功能级,是高级语言结构实现设计算法的模型,对每一个功能模块完成行为描述。
系统级和算法级建模抽象层次高,往往不考虑硬件实现的具体细节,一般对特大型设计或有较复杂的算法时使用。建模时使用高级语言,对设计从系统的行为、算法方面进行描述,一般只用于仿真、验证系统功能,通常不支持综合,在仿真通过后再用RTL级进行设计。
对于复杂的系统芯片设计项目,最传统的方法是在系统级采用VHDL,在软件级采用C语言,但在实现级采用Verilog HDL。
⑵寄存器传输级建模方式:
RTL级是描述数据如何在寄存器之间流动和如何处理、控制这些数据流动的模型,它将算法或功能用数字电路来实现。寄存器传输级RTL是Verilog HDL设计中最常用的设计层次。
所谓RTL(Register Transfer Level)级就是在描述电路时,只需且必须关注寄存器本身以及寄存器到寄存器之间的逻辑功能,即关注寄存器中保存着的数据,而不用关注寄存器和组合逻辑的实现细节。RTL级的基本部件有计数器、触发器、锁存器和算术逻辑单元ALU等,这些基本部件也称为功能块,在采用RTL建模方式时可直接使用。
RTL级的描述是可综合的描述。所谓综合Synthesize是指将HDL语言、原理图等设计输入翻译成由与门、或门、非门等基本逻辑单元组成的门级连接,并根据设计目标和要求优化所生成的逻辑连接,输出门级网表文件。
采用RTL级建模方式对系统进行设计时,包含时钟描述、时序逻辑描述、组合逻辑描述3个基本要素。时序逻辑、组合逻辑的连接关系和拓扑结构决定了设计的性能。
⑶门级建模方式:
逻辑门级是将逻辑门作为基本单元,描述逻辑门以及逻辑门之间额连接模型,与逻辑电路有一一对应的连接关系。
⑷晶体管开关级建模方式:
晶体管开关级是把晶体管开关作为基本单元,从晶体管的层次描述电路,描述器件中晶体管以及它们之间连接的模型。若从开关级层次进行设计,则需要掌握工艺库元件和宏元件,一般是在版图级进行。
数字电路系统中,逻辑门是由一个个晶体管组成,在Verilog HDL中,有用于直接描述NMOS和PMOS的原语,即具有对MOS管级进行设计的能力,其中的晶体管仅被当作导通或截止的开关。
随着电路复杂性以及先进CAD工具的出现,以开关级为基础进行的设计正在逐渐萎缩,只有在很少的情况下设计者需定制自己的最基本元件时才使用。Verilog HDL内置的开关级建模元件主要有MOS开关(包括NMOS开关和PMOS开关)、CMOS开关、电源和地、双向开关、阻抗开关等。
3) 有限状态机设计:
有限状态机FSM(Finite State Mechine)就是一系列数量有限的状态组成的一个循环机制,几乎所有的数字系统硬件设计中都或多或少地使用了有限状态机思想。
时序电路由组合逻辑和时序逻辑组成。组合逻辑接收电路输入信号并输出结果,时序逻辑将组合逻辑的输出存储并反馈回组合逻辑,以此形成电路的当前状态,当前状态与电路输入信号经过组合逻辑作用形成电路的下一个状态传递给时序电路。
时序电路可以是同步时序电路,也可以是异步时序电路。同步时序电路是指电路的时序是由时钟来控制的,随着时钟跳变,电路从当前状态转变到下一状态。对于异步时序电路,是没有确定时钟的一种状态机,状态转移不是由唯一时钟跳变触发。一般来说,使用硬件描述语言设计的异步状态机难以有效综合,因此异步状态机一般使用电路图输入的方法。
9. Verilog HDL代码编写风格:
Verilog HDL代码编写规范,有助于提高书写代码的可读性、可修改性、可重用性,便于优化代码综合和仿真的结果。
1) 命名规范:
⑴文件名:
每个模块一般应存在单独的源文件中,便于模块修改,通常源文件名与所包含模块名相同。模块、文件名采用功能命名,模块层次尽可能不要超过4层,低层模块的命名要包含上层模块名,这样便于理解模块层次结构和功能(调用CORE模块除外)。
Verilog HDL代码必须存入某个文件夹中,而不要保存在根目录内或桌面上,要求非中文路径。
⑵模块名:
在系统设计阶段应为每个模块命名,最终的顶层模块应该以芯片的名称来命名。在顶层模块中,除I/O引脚和不需要综合的模块外,其余作为次级顶层模块建议以xx_core.v命名。
同一模块中调用同一子模块时,调用名采用整数索引或采用整数多次索引,以增加模块的可读性,避免混淆。
⑶信号名:
采用有意义的,能反映对象特征、出处、功能和性质的单词命名,可以达到望文生义,以增强程序的可读性。长的名字对书写和记忆会带来不便,甚至带来错误,所以要避免标识符过于冗长,对较长的单词应当采用适当的缩写形式。
在RTL源码设计中,任何元素,包括端口、信号、变量、函数、任务、模块等的命名都不能使用关键字。如果需要多个意义独立的字符串命名,字符串之间要用下划线隔开,便于维护,有助于对设计的理解。
总线由高位到低位命名,对来自于同一驱动源的信号,在不同的子模块中采用相同的名字,这就要求在芯片总体设计时就定义好顶层模块间连线的名字,端口和连接端口的信号尽可能采用相同的名字。
2) 格式规范:
⑴空行和空格:
适当地在代码的不同部分中插入空行,避免因程序拥挤不利阅读,如分节书写,各节之间加1到多空行。
不同变量,以及变量与符号、变量与括号之间都应当保留一个空格;使用//进行注释,在//后应当有一个空格;在表达式中插入空格,避免代码拥挤。
⑵对齐和缩进:
不要使用连续的空格来进行语句的对齐,要采用制表符Tab对语句对齐和缩进,Tab采用4个字符宽度,可在编辑器中设置。
各种嵌套语句,尤其是if-else语句,必须严格逐层缩进对齐;同一层次的所有语句左端对齐;initial和always块的begin关键字和相应的end关键字与initial和always对齐。
每行只写一条语句可增加程序的可读性,便于用设计工具进行代码的语法分析。
⑶注释:
必须加入详细、清晰的注释行以增强代码的可读性和可移植性,注释内容占码篇幅不应少于30%。使用//进行的注释行以分号结束,以/*...*/进行的注释,/*和*/各占一行,并且顶头。
代码行使用单行注释,不使用多行注释,代码行注释跟在注释代码之后,处于同一行。注释应简明扼要,避免使单行内容过长。如注释过长且难以简略,可以分行注释,放在下一行的注释应与前行注释左侧对齐。若代码本身较长,难以在同一行加以注释,可以在代码的前一行放置注释内容,注意这行注释要独占一行。
分行的注释内容要独占一行,该行不能有其他的代码。
⑷模块调用格式:
Verilog HDL中有两种模块调用方法,一种是位置映射法,严格按照模块定义的端口顺序来连接,不用注明原模块定义时规定的端口名;另一种为信号映射法,即利用“.”符号表明原模块定义时的端口名。
信号映射法同时将信号名和被引用端口名列出来,不必严格遵守端口顺序,不仅降低了代码的易错性,还提高了程序的可读性和移植性。因此在良好的代码中,全部采用信号映射法。
⑸大小写:
如无特别需要,模块名和信号名一律采用小写字母;为了醒目起见,自己定义的常数参数采用大写字母。
⑹参数化设计格式:
为了源代码的可读性和可移植性,不要在程序中直接写特定数值,尽可能采用`define语句或parameter语句定义常数或参数。
3) RTL可综合代码编写规范:
用HDL实现电路,设计人员对可综合风格的RTL描述的掌握不仅会影响到仿真和综合的一致性,也是逻辑综合后电路可靠性和质量好坏最主要的因素。为此要考虑以下几个方面:
· 每个模块尽可能只使用一个时钟,用一个时钟的上升沿或下降沿采样信号,不能一会儿用上升沿一会儿用下降沿,如果既要使用上升沿又要使用下降沿,则应分成两个模块设计。建议在顶层模块中对clock做一非门,在层次模块中如果要用时钟下降沿就可以用非门产生的时钟,这样做的好处是在整个设计中采用同一时钟触发,有利于综合。
· 代码描述应尽量简单,如果编码过程无法预计其最终的综合结果,那综合工具可能会花很长时间。
· 在内部逻辑中避免使用三态逻辑。
· 避免触发器在综合过程中生成锁存器。
· 尽量避免异步逻辑、带有反馈环的组合逻辑。
· 避免不必要的函数调用,重复的函数调用会增加综合次数,不仅造成电路面积的浪费,还会使综合时间变长。
不同的综合工具对Verilog HDL语法结构的支持不尽相同,但某些典型结构是很明确地被所有综合工具支持或不支持。
· 所有综合工具都支持的结构:
always、assign、begin、end、case、wire、tri、supply0、supply1、reg、integer、inout、input、instantitation、module、negedge、posedge、operators、output、parameter、default、for、function、and、nand、or、nor、xor、xnor、buf、not、bufif0、bufif1、notif0、notif1、if
· 有些综合工具支持有些综合工具不支持的结构:
casex、casez、wand、triand、repeat、task、while、wor、trior、real、disable、forever、arrays、memories
· 所有综合工具都不支持的结构:
time、defparam、$finish、fork、join、initial、delays、UDP、wait
4) 项目目录规范:
采用合理、条理清晰的设计目录结构有利于提高设计的效率和维护。建议目录结构:
· src:源代码、测试代码
· syn:综合
· sim:仿真
· par:布线
不同版本放在同一设计的不同的目录中,以版本命名目录。
5) 常见错误:
· 对包含多条语句的块语句缺少begin-end语句,或不匹配
· 对连续赋值的左边不声明为线网变量,对过程赋值的左边不声明为寄存器变量
· 对二进制数缺少“'b”,导致编译器看作十进制数
· 在伪指令中使用的不是反引号,在数字基使用的不是单引号
· 语句结尾缺少分号
10. Verilog HDL代码编写风格:
测试平台为RTL代码或门级网表的功能验证提供验证平台,包括待验证的设计DUT、激励信号产生器和输出显示控制等。在仿真时,测试平台用来产生测试激励给DUT,同时检查DUT的输出是否与预期的一致,从而达到验证设计功能的目的。
开发测试平台是集成电路设计过程中的一个重要步骤。在开发测试平台时,需要先指定一个测试方案,如需要验证的电路的特征,如何在测试平台上测试等。以下为测试平台的一般结构:
module DUT_tb()
reg ...
wire ...
DUT u_DUT(...) //待测设计实例化
initial $monitor(); //以文本形式监测并显示信号描述
initial #time_out $finish //确保模拟终端停止观测
initial
begin
//仿真激励的施加
end
endmodule
1) 仿真激励语法:
产生激励并施加到设计有多种方法,常用方法有从initial块中施加线激励,从一个循环或always块中施加激励,从一个向量或整型数组中施加激励,记录一个仿真过程然后在另一个仿真中回放施加激励等。
⑴initial语句和always语句施加激励:
initial和always是两种基本的过程结构语句,在仿真开始时并行执行,被动检测响应时使用always语句,主动产生激励时使用initial语句。initial语句只能执行一次,而always语句可以不断重复执行。
⑵产生时钟信号:
由于时钟信号是周期性的,通常使用always语句产生。
⑶复位信号:
复位信号为被测设计提供一定宽度的高电平或低电平脉冲,然后变为低电平或高电平,并保持不变,因而用initial语句可以很方便地实现。复位信号包括异步复位信号和同步复位信号。
⑷并行激励:
如果希望在仿真的某一时刻启动多个任务,即需要并行激励,可以采用fork-join语法结构。
⑸循环激励:
很多时候,测试激励产生的方法或规律是相同的,只是测试激励的结果不同,这时候可以使用for循环语句,在每一次循环修改同一组激励变量,这就是循环激励。循环激励使得时序关系规则,代码更加紧凑。
⑹数组激励:
如果已经定义了数组,测试激励也可以直接从数组中获得,而且激励数组可以直接从文件中读取。
⑺强制激励:
在过程块中,可以使用两种持续赋值语句驱动一个值或表达式到一个信号。过程持续赋值通常不可综合,所以通常用于测试基准描述。对每一种持续赋值,都有相应的命令停止信号赋值,不允许在赋值语句内部出现时序控制。
assign是强制性为寄存器赋确定的值,对一个寄存器使用assign和deassign,将覆盖所有其他在该信号上的赋值。
force是强制性为变量赋确定的值,在register和net上使用force和release将覆盖该信号上的所有其他驱动。
可以强制force并释放一个信号的指定位、部分位或连接,但位的指定不能是一个变量;不能对register的一位或部分位使用assign或deassign;对同一个信号,force覆盖assign;后面的assign或force覆盖以前相同类型的语句。
如果对同一信号先assign然后force,它将保持force值,在对其进行release后,信号为assign值。如果在一个信号上force多个值,然后release该信号,则不出现任何force值。
强制激励并不常用,有时可以利用该语句和仿真工具进行简单的交互操作。
⑻包含文件和文件读写:
包含文件用于读取代码的重复部分或公共数据。在写测试激励时,经常需要从已有的文件中读入数据,或把数据写入文件中,以便做进一步的分析。
⑼矢量采样和矢量回放:
在仿真过程中,可以对激励和响应矢量进行采样,作为其他仿真的激励和期望结果。保存在文件中的矢量反过来可以作为激励,称为矢量回放。
⑽Matlab:
进行数字信号处理是Matlab的强项,在做数字信号处理算法验证时借助Matlab会加快算法验证的速度。
2) 系统函数和系统任务:
编写测试平台时,一些系统函数和系统任务可以帮助产生测试激励,显示调试信息,协助定位等。系统函数和任务一般以$开头,通常在initial或always过程块中调用。在使用不同的Verilog HDL仿真工具,如VCS、Verilog-XL、ModelSim等,这些系统函数和系统任务在使用方法上可能存在差异,应根据使用手册来使用。
⑴$display、$write和$strobe:
$display输出参数列表中信号的当前值,输出时自动换行,格式类似C语言中的print函数。$write在输出结束时不会自动换行;$strobe在仿真时间发生改变时,并在所有事件都已处理完毕后,才将结果输出。也就是$display和$write可以显示信号的中间状态值,而$strobe显示稳定状态信号值。
$write和$strobe都支持多种数基,如$writeb、$writeo、$writeh、$strobeb、$strobeo、$strobeh等,默认为十进制。
通过%m选项可以显示任何级别的层次,无需任何参数。
⑵$monitor:
$monitor提供了监控和输出参数列表中的表达式或变量值的功能,其参数列表中的输出控制格式字符串和输出列表的规则与$display一样。在$monitor中,参数可以是$time系统函数,这样参数列表中变量或表达式的值同时变化时可以通过标明同一时刻的多行输出来显示。
$monitor是唯一不断输出信号值的系统任务,其他系统任务在返回值后就结束。$monitor显示的也是参数列表中信号的稳定状态,在同一时刻,参数列表中信号值的任何变化将触发$monitor,任何后续的$monitor覆盖前面调用的$monitor,只有新的$monitor的参数列表中的信号被监控,而前面的$monitor的参数则不被监控。
可以通过$monitoron和$monitoroff来打通和关闭监控标志来控制监控任务$monitor的启动和终止,使用户可以在仿真时只监视特定时间段的信号。$monitor支持多种数基,如$monitorb、$monitoro、$monitorh,默认十进制。
⑶$fopen、$fclose、$fdisplay和$fmonitor:
Verilog HDL的输出结果通常输出到标准输出文件verilog.log中,可以通过系统任务把输出定向到指定的文件。
$fopen用于打开参数中指定的文件并返回一个32位无符号整数MCD,MCD是与文件一一对应的多通道描述符,可以看作由32个标志构成的组,每个标志代表一个单一的输出通道。如果文件不能打开并进行写操作,返回0。
输出信息到log文件和标准输出的4个格式化显示任务,$display、$write、$strobe、$monitor,都有相对应的任务用于向指定文件输出,分别为$fdisplay、$fwrite、$fstrobe、$fmonitor,其参数形式与对应的任务相同,但其第1个参数必须为一个指定哪个文件的MCD。
⑷$readmemb和$readmemh:
用来从一个文本文件读取数据并写入存储器,这两个系统任务可以在仿真的任何时刻被执行。如果数据为二进制使用$readmemb,如果数据为十六进制使用$readmemh。
⑸$finish和$stop:
系统任务$finish是退出仿真器,结束仿真过程,可以带参数0、1、2。系统任务$stop暂停仿真,并在仿真界面给出一个交互式的命令提示符,把控制权交给用户,该命令也可带参数,参数数值越大输出的信息越多。
⑹$random:
提供一个产生随机数的方法,当函数调用时返回一个32位的随机数,为一个带符号的整数。
⑺$time和$realtime:
$time返回一个64位整数,表示当前的仿真时刻值,该时刻以模块的仿真时间尺度为基准。$realtime返回的是一个实数值,该数值同样以时间尺度为基准。
⑻值变存储文件:
值变存储文件VCD是一个ASCII文件,该文件包含仿真时间、范围与信号的定义,以及仿真运行过程中信号值的变化等信息。设计中的所有信号或选定的信号,在仿真过程中都可以被写入VCD文件。对于大规模设计的仿真,可以把选定的信号转储到VCD文件中,并用后处理工具去调试、分析和验证仿真结果。
Verilog HDL提供了相关系统任务,$dumpvars选择要转储的模块或块信号,$dumpfile选择VCD文件的名称,$dumpon和$dumpoff选择转储过程的起点和终点,$dumpall选择生成检测点。
一些仿真工具,如Synopsys VCS、Cadence Verilog-XL等,还提供了$vcspluson、$vcsplusoff等系统任务。
11. 逻辑综合与静态时序分析:
逻辑综合,就是在标准单元库和特定的设计约束基础上,把设计的高层次描述转化为优化的门级网表的过程。目前,基于RTL的逻辑综合已经成为IC设计流程的一个重要步骤。
综合包括转换和编译两个阶段,转换阶段对于Synopsys的综合工具DC来说,就是使用gtech.db库中的门级单元来组成HDL语言描述的电路,从而构成初始的未优化的电路;编译阶段则包括优化与映射过程,是综合工具对已有的初始电路进行分析,去掉电路中的冗余单元,并对不满足限制条件的路径进行优化,然后将优化之后的电路映射到由用户提供的工艺库上。
12. Verilog-2001标准:
2001年3月,IEEE批准了IEEE1364-2001标准,目前几乎所有的综合器和仿真器都能很好地支持这个标准。
1) ANSI C风格的模块声明:
允许将端口声明和数据类型声明放在同一条语句中,可用于module、task和function。示例:
module fifo_2001
#(parameter MSB=3,DEPTH=4)
(input[MSB:0] in,
input clk,read,write,reset,
output reg[MSB:0] out,
output reg full,empty);
2) 敏感信号列表:
⑴逗号分隔的敏感信号列表:
可用逗号分隔敏感信号。示例:
always @(a,b,cin)
{cout,sum}=a+b+cin;
always @(posedge clock,negedge clr)
if(!clr) q<=0;else q<=d;
⑵在组合逻辑敏感信号列表中使用通配符*:
用always过程块描述组合逻辑时,应在敏感信号列表中列出所有的输入信号,可用通配符“*”来表示包含该过程块的所有输入信号变量。示例:
always @ *
......
3) generate语句:
通过generate循环可以产生一个对象的多个例化,为可变尺度的设计提供了便利。对象包括module、primitive、variable、net、task、function、assign、initial、always。generate一般与循环语句、条件语句一起使用,为此增加了关键字generate、endgenerate、genvar、localparam。其中genvar是一个新的数据类型,用在generate循环中的标尺变量必须定义为genvar类型。
而对应地for循环的内容必须加begin和end,即使只有一条语句,且必须给begin和end块语句起个名字。
行波进位加法器示例:
module add_ripple #(parameter size=4)
(input[SIZE-1:0] a,b,
input cin,
output[SIZE-1:0] sum,
putput cout);
wire[SIZE:0] c;
assign c[0]=cin;
generate
genevar i;
for(i=0;i<SIZE;i=i+1)
begin:add
wire n1,n2,n3;
xor g1(n1,a[i],b[i]);
xor g2(sum[i],n1,c[i]);
and g3(n2,a[i],b[i]);
and g4(n3,n1,c[i]);
or g5(c[i+1],n2,n3);
end
endgenerate
assign cout=c[SIZE];
endmodule
可扩展乘法器示例:
module multiplier (a,b,product)
parameter a_width=8,b_width=8;
localparam product_width=a_width+b_width;
input [a_width-1:0] a;
input [a_width-1:0] b;
output[product_width-1:0] product;
generate
if((a_width<8)||(b_width<8))
CLA_multiplier #(a_width,b_width)
u1(a,b,product);
else
WALLACE_multiplier #(a_width,b_width)
u1(a,b,product);
endgenerate
assign cout=c[SIZE];
endmodule
4) 有符号的算术扩展:
用signed来定义有符号的数据类型、端口、整数、函数等。
⑴wire和reg型变量可以声明为有符号变量:
wire signed[7:0] a,b;
reg signed[15:0] data;
output signed[15:0] sum;
⑵任何进制的整数都可以有符号,参数也可以有符号:
12'sh54f //12位的十六进制有符号整数54f
parameter p0=2`sb00,p1=2`sb01;
⑶函数的返回值可以有符号:
function signed[31:0] alu;
⑷增加了算术移位操作符<<<和>>>:
对于有符号数,在执行算术移位操作时,用符号位填补移出的位,以保持数值的符号。
⑸新增了系统函数$signed()和$unsigned():
可以将数值强制转换为有符号的值或无符号的值。
5) 指数运算符:
增加了指数运算符“**”,一般使用的底为2的指数。示例:
parameter WIDTH=16;
parameter DEPTH=8;
reg [WIDTH-1:0] data [0:(2**DEPTH)-1]; //定义一个位宽16位,256单元的存储器
6) 变量声明时赋值:
可以在变量声明时赋值,所赋的值必须为常量,并且在下次赋值之前变量会保持此值不变。但这种方法不适用于矩阵。示例:
reg[3:0] a=4'h4;
也可同时声明多个变量,为其中的一个或几个赋值:
integer i=0,j,k=1;
real r1=2.5,n300k=3E6;
7) 常数函数:
常数函数与其他函数的定义相同,但其赋值是在编译或详细描述时被确定的。常数函数有助于创建可改变维度和规模的可重用模型。
常数函数只能调用常数函数,不能调用系统函数,常数函数内部用到的参数必须在此常数函数被调用之前定义。
8) 向量的位选和域选:
在Verilog HDL的1995标准中,可以从向量中取出一个或相连的若干比特,称为位选和域选,但被选择的部分必须是固定的。2001标准对此进行了扩展,增加了索引的部分选择,形式为:
[base_expr +: width_expr] //起始表达式 正偏移 位宽
[base_expr -: width_expr] //起始表达式 负偏移 位宽
其中,位宽必须为常数,而起始表达式可以为变量。示例:
reg [63:0] word;
reg [3:0] byte_num;
wire [7:0] byteN=word[byte_num*8 +: 8];
9) 数组的功能扩展:
⑴多维数组:
新标准允许使用多维数组,数组单元的数据类型也扩展至Variable型和Net型。示例:
reg [7:0] array1 [0:255];
wire [7:0] out1=array1 [address];
wire [7:0] array3 [0:255] [0:255] [0:15];
wire [7:0] out3=array3 [addr1] [addr2] [addr3]; //三维数组,存储单元为wire型
⑵数组的位选择和部分选择:
新标准可以直接访问数组的某个单元的一位或几位。示例:
reg [31:0] array2 [0:255] [0:15];
wire [7:0] out2=attay2[100][7][31:24];
10) 模块实例化时的参数重载:
当模块实例化时,其内部定义的参数parameter的值是可以改变的,称为参数重载。过去标准中使用defparam显示重载或者模块实例化时使用#符号隐式重载,新标准中增加了一种在线显示重载参数方式,允许在线参数值按照任意顺序排列。示例:
defparam ram1.SIZE=1023;
RAM #(8,1023) ram2(...);
RAM #(.SIZE(1023)) ram3 (...);
11) register改为variable:
过去一直使用register表示存储的数据类型,但因为很容易将其与硬件中的寄存器概念混淆,实际中这种数据类型的变量也常常被综合器映射为组合逻辑电路。新标准将其改为了variable。
12)条件编译增加`elsif和`ifndef:
13) 超过32位的自动宽度扩展:
过去版本,对超过32位的总线赋高阻时,如果不指定位宽,会只将低32位赋高阻,高位则补0。如果要将所有位都置为高阻,必须明确指定位宽。
新标准更改了规则,将高阻z或不定态x赋给未指定位宽的信号时,可以自动扩展到信号的整个位宽范围。
14) 可重入任务和递归函数:
增加了一个关键字automatic,可用于任务和函数的定义。
⑴可重入任务:
任务本质上是静态的,同时并发执行的多个任务共享存储区。当某个任务在模块中的多个地方被同时调用时,这两个任务对同一块地址空间进行操作,结果可能是错误的。新增关键字automatic,空间是动态分配的,使任务成为可重入的。
⑵递归函数:
如将automatic用于函数,则表示函数的迭代调用。
15) 文件和行编译指示:
Verilog HDL编译器和仿真工具需要不断跟踪源代码的行号与文件名,但如果代码经过其他工具的处理,源代码的行号和文件名可能丢失,因此增加了`line,用来标定源代码的行号和文件名。
16) 增强文件输入/输出操作:
旧版Verilog HDL在文件操作方面通常借助C语言的文件输入/输出库来访问处理,并且规定同时打开的I/O文件数目不能超过31个。新标准增加了新的系统函数和任务,并可同时打开文件的数目扩展到230个。新增的文件输入/输出系统任务和函数包括$ferror、$fgets、$fflush、$fread、$fscanf、$fseek、$ftel、$rewind、$ungetc,还有读写字符串的系统任务,包括$sformat、$swrite、$swriteb、$swriteh、$fwriteo、$sscanf,用于生成格式化的字符串或从字符串中读取信息。
增加了命令行输入任务$test$plusargs和$value$plusargs。
13. Intel FPGA/CPLD家族系列:
Intel的FPGA/CPLD分为高端、中端和低成本系列,每个系列又不断更新换代。
1) Stratix高端FPGA家族系列:
Stratix:2002年推出,1.5V、130nm全铜工艺。
Stratix II:2004年推出。1.2V、90nm工艺,15600~17940个等效LE,达9Mb嵌入式RAM,500MHz内部时钟。
Stratix III:2006年推出,65nm工艺,达338000个逻辑单元,9kb分布式RAM和144kb RAM块,可调内核电压、自动功耗/速率调整。分为3个系列:标准系列、L系列侧重DSP应用、GX系列集成高速串行收发单元。
Stratix IV:2008年推出,40nm工艺,集成11.3Gbps收发器,可实现SoC。
Stratix V:2010年推出,28nm工艺,达到119万逻辑单元LE或14.3M个逻辑门,集成28.05Gbps和14.1Gbps收发器,1066MHz的6×72DDR3存储器接口,能提供PCI Express Gen3/2/1硬核和嵌入式集成内核。
Stratix 10:2013年推出,14nm工艺,达550万逻辑单元,并可集成1.5GHz四核64位ARM Cortex-A53硬核处理器,能提供144个收发器,数据率达30Gbps,支持2666Mbps的DDR4。
2) Arria中端FPGA家族:
用于成本和功耗敏感的收发器及嵌入式应用。
Arria GX:2007年推出,90nm工艺,收发器速率3.125Gpbs,支持PCIE、以太网、Serial RapidIO等多种协议。
Arria II:2009年推出,40nm工艺,包括ALM、DSP模块和嵌入式RAM,还有PCIE硬核。包括GX和GZ两个型号,2010年推出的GZ功能更强。
Arria III:2011年推出,28nm工艺,低静态功耗,收发器速率10.3125Gpbs,集成了包括处理器外设和存储器控制器的HPS。包括GX、GT、SX、GZ四个型号,2012年推出的GZ收发器速率达到12.5Gbps,每通道功耗不到200mW。
Arria 10:2013年推出,20nm工艺,串行接口速率达28.05Gpbs,硬核浮点DSP模块速率达每秒1500G次浮点运算(GFLOPS)。
3) Cyclone低成本家族:
Cyclone:2002年推出,130nm工艺,已停产。
Cyclone II:2004年推出,90nm工艺,已停产。
Cyclone III:2007年推出,65nm工艺,含有5000~12万逻辑单元、288个DSP乘法器,每个RAM到9kb,最大容量4Mb,18位乘法器数量达288个。
Cyclone IV:2009年推出,60nm工艺,包括GX和E两个型号。GX具有150k逻辑单元、6.5Mb RAM和360个乘法器,还有8个支持主流协议的3.125Gbps收发器,为PCIE提供硬核,封装11×11mm;E器件不带收发器,内核电压1.0V,功耗更低。
Cyclone V:2011年推出,28nm工艺,提供集成收发器型号及具有基于ARM的硬核处理器系统的型号。
Cyclone 10:2017年推出,20nm工艺,包括GX和LP两个型号。GX支持12.5Gbps收发器、1.4Gbps的LVDS和最高72位宽、1866Mbps DDR3 SDRAM接口,逻辑容量85k~220k个LE,适用于成本敏感的高带宽、高性能应用,如工业视觉、机器人和车载娱乐多媒体系统。LP逻辑容量6k~120k个LE单元,静态功能只有上一代产品的一半,适用于不需要高速收发器的低功耗、低成本应用。
4) CPLD家族:
MAX7000S/MAX3000A:1995~2002年推出,0.5~0.3um工艺,采用EEPROM,集成32~512个宏单元,工作电压多为5V。
MAX II:2004年推出,0.18um Flash工艺,基于查找表LUT结构,嵌入8kb的Flash存储器。
MAX IIZ:2007年推出,0.18um Flash工艺。
MAX V:2010年推出,0.18um 工艺,集成闪存、RAM、振荡器、锁相环,静态功耗45uW。
MAX 10:2014年推出,55nm NOR闪存工艺,使用单核或双核电压供电,2000~5000个LE单元,3×3mm封装,集成ADC和双配置闪存,支持Nios II软核、DSP模块和软核DDR3存储控制器,具有736kB用户闪存,集成温度传感器和模拟模块。