Verilog HDL是一种
硬件描述语言,以
文本形式来描述
数字系统硬件的结构和行为的语言,用它可以表示逻辑
电路图、
逻辑表达式,还可以表示数字
逻辑系统所完成的逻辑功能。 Verilog HDL和VHDL是世界上最流行的两种硬件描述语言,都是在20世纪80年代中期开发出来的。前者由Gateway Design Automation公司(该公司于1989年被
Cadence公司收购)开发。两种HDL均为
IEEE标准。
介绍
Verilog HDL是一种硬件描述语言,用于从
算法级、门级到开关级的多种抽象设计层次的数字系统建模。被建模的
数字系统对象的复杂性可以介于简单的门和完整的电子数字系统之间。数字系统能够按层次描述,并可在相同描述中显式地进行时序建模。
Verilog HDL 语言具有下述描述能力:设计的行为特性、设计的数据流特性、设计的结构组成以及包含响应监控和设计验证方面的
时延和波形产生机制。所有这些都使用同一种
建模语言。此外,Verilog HDL语言提供了编程语言接口,通过该接口可以在模拟、验证期间从设计外部访问设计,包括模拟的具体控制和运行。
Verilog HDL语言不仅定义了语法,而且对每个语法结构都定义了清晰的模拟、仿真语义。因此,用这种语言编写的模型能够使用Verilog仿真器进行验证。语言从C编程语言中继承了多种
操作符和结构。Verilog HDL提供了扩展的建模能力,其中许多扩展最初很难理解。但是,Verilog HDL语言的核心子集非常易于学习和使用,这对大多数建模应用来说已经足够。当然,完整的硬件描述语言足以对从最复杂的芯片到完整的
电子系统进行描述。
发展历史
Verilog是由
Gateway设计自动化公司的工程师于1983年末创立的。当时Gateway设计
自动化公司还叫做自动集成设计系统(Automated Integrated Design Systems),1985年公司将名字改成了前者。该公司的菲尔·莫比(Phil Moorby)完成了Verilog的主要设计工作。1990年,Gateway设计自动化被
Cadence公司收购。
1990年代初,开放Verilog国际(Open Verilog International, OVI)组织(即现在的Accellera)成立,Verilog面向
公有领域开放。1992年,该组织寻求将Verilog纳入
电气电子工程师学会标准 。最终,Verilog成为了电气电子工程师学会1364-1995标准,即通常所说的Verilog-95。
设计人员在使用这个版本的Verilog的过程中发现了一些可改进之处。为了解决用户在使用此版本Verilog过程中反映的问题,Verilog进行了修正和扩展,这部分内容后来再次被提交给电气电子工程师学会。这个扩展后的版本后来成为了电气电子工程师学会1364-2001标准,即通常所说的Verilog-2001。Verilog-2001是对Verilog-95的一个重大改进版本,它具备一些新的实用功能,例如敏感列表、
多维数组、生成语句块、命名端口连接等。目前,Verilog-2001是Verilog的
最主流版本,被大多数商业
电子设计自动化软件包支持。
1995年,IEEE 制定了 Verilog HDL 的第一个
国际标准,即 IEEE Std 1364-1995,也称之为 Verilog 1.0。
2001 年,IEEE 发布 Verilog 第二个标准(Verilog 2.0),即 IEEE Std 1364-2001, 简称为 Verilog-2001 标准。
2005年,Verilog再次进行了更新,即电气电子工程师学会1364-2005标准。该版本只是对上一版本的细微修正。这个版本还包括了一个相对独立的新部分,即Verilog-AMS。这个扩展使得传统的Verilog可以对集成的模拟和混合
信号系统进行建模。容易与电气电子工程师学会1364-2005标准混淆的是加强
硬件验证语言特性的
SystemVerilog(电气电子工程师学会1800-2005标准),它是Verilog-2005的一个
超集,它是
硬件描述语言、硬件验证语言(针对验证的需求,特别加强了
面向对象特性)的一个集成。
2009年,IEEE 1364-2005和IEEE 1800-2005两个部分合并为IEEE 1800-2009,成为了一个新的、统一的SystemVerilog硬件描述验证语言(hardware description and verification language, HDVL)。
设计
描述复杂的硬件电路,设计人员总是将复杂的功能划分为简单的功能,模块是提供每个简单功能的
基本结构。设计人员可以采取“自顶向下”的思路,将复杂的
功能模块划分为低层次的模块。这一步通常是由系统级的总设计师完成,而低层次的模块则由下一级的设计人员完成。自顶向下的设计方式有利于系统级别层次划分和管理,并提高了效率、降低了成本。“
自底向上”方式是“自顶向下”方式的逆过程。
使用Verilog描述硬件的基本设计单元是模块(module)。构建复杂的
电子电路,主要是通过模块的相互连接调用来实现的。模块被包含在关键字module、endmodule之内。实际的
电路元件。Verilog中的模块类似C语言中的函数,它能够提供输入、输出端口,可以实例调用其他模块,也可以被其他模块实例调用。模块中可以包括
组合逻辑部分、过程时序部分。例如,四选一的
多路选择器,就可以用模块进行描述。它具有两个位选
输入信号、四个
数据输入,一个输出端,在Verilog中可以表示为:
module mux (out, select, in0, in1, in2, in3);
output out;
input [1:0] select;
input in0, in1, in2, in3;
endmodule
设计人员可以使用一个顶层模块,通过实例调用上面这个模块的方式来进行测试。这个顶层模块常被称为“
测试平台(Testbench)”。为了最大程度地对电路的逻辑进行
功能验证,测试代码需要尽可能多地覆盖系统所涉及的语句、分支、条件、路径、触发、
状态机状态,验证人员需要在测试平台里创建足够多的输入激励,并连接到被测模块的输入端,然后检测其输出端的表现是否符合预期(诸如
SystemVerilog的
硬件验证语言能够提供针对验证专门优化的
数据结构,以
随机测试的方式进行验证,这对于高度复杂的集成电路设计验证可以起到关键作用)。实例调用模块时,需要将端口的连接情况按照这个模块声明时的
顺序排列。这个顶层模块由于不需要再被外界调用,因此没有
输入输出端口:
module tester;
reg IN0, IN1, IN2, IN3;
wire OUT;
mux my_mux (OUT, SELECT, IN0, IN1, IN2, IN3); //实例调用mux模块,这个实例被命名为my_mux
initial //需要仿真的激励代码
begin
*******
end
endmodule
在这个测试平台模块里,设计人员可以设定仿真时的输入信号以及信号监视程序,然后观察仿真时的输出情况是否符合要求,这样就可以了解设计是否达到了预期。
示例中的对模块进行实例引用时,按照原模块声明时的顺序罗列了
输入变量。除此之外,还可以使用或者采用命名端口连接的方式。使用这种方式,端口的排列顺序可以与原模块声明时不同,甚至可以不连接某些端口:
mux my_mux (
.out(OUT),
.select(SELECT),
.in0(IN0),
.in1(IN1),
.in2(IN2),
.in3(IN3));//使用命名端口连接,括号外面是模块声明时的端口,括号内是实际的端口连接//括号外相当于C语言的
形式参数,括号内相当于
实际参数endmodule
上面所述的情况是,测试平台顶层模块的测试变量
直接连接了所设计的功能模块。测试平台还可以是另一种形式,即测试平台并不直接连接所设计的功能模块,而是在这个测试平台之下,将激励模块和功能模块以相同的抽象级别,通过线网相互连接。这两种形式的测试平台都可以完成对功能模块的测试。大型的电路系统,正是由各个层次不同模块之间的连接、调用,来实现复杂的功能的。
语言要素
Verilog的设计初衷是成为一种基本语法与
C语言相近的硬件描述语言。这是因为C语言在Verilog设计之初,已经在许多领域得到广泛应用,C语言的许多语言要素已经被许多人习惯。一种与C语言相似的硬件描述语言,可以让电路设计人员更容易学习和接受。不过,Verilog与C语言还是存在许多差别。另外,作为一种与普通
计算机编程语言不同的硬件描述语言,它还具有一些独特的语言要素,例如向量形式的线网和寄存器、过程中的非阻塞赋值等。总的来说,具备C语言的设计人员将能够很快掌握Verilog硬件描述语言。
基本规范
空白符是指代码中的空格(对应的转义
标识符为)、
制表符( )和换行(
)。如果这些空白符出现在
字符串里,那么它们可忽略。除此之外,代码中的其他空白符在编译的时候都将会被视为分隔标识符,即使用2个空格或者1个空格并无影响。不过,在代码中使用合适的空格,可以让上下行代码的外观一致(例如使
赋值运算符位于同一个竖直列),从而提高代码的可读性。
注释
为了方便代码的修改或其他人的阅读,设计人员通常会在代码中加入注释。与C语言一样,有两种方式书写注释。第一种为多行注释,即注释从/*开始,直到*/才结束;另一种为
单行注释,注释从//开始,从这里到这一行末尾的内容会被
系统识别为注释。
某些电子设计自动化工具,会识别出代码中以特殊格式书写、含有某些预先约定关键词的注释,并从这些注释所提取有用的信息。这些注释不是供人阅读,而是向第三方工具提供有关设计项目的额外信息。例如,某些
逻辑综合工具可以从注释中读取综合的约束信息。
Verilog是一种大
小写敏感的硬件描述语言。其中,它的所有系统关键字都是小写的。
Verilog代码中用来定义
语言结构名称的字符称为标识符,包括变量名、
端口名、模块名等等。标识符可以由
字母、数字、
下划线以及
美元符($)来表示。但是标识符的第一个字符只能是字母、数字或者下划线,不能为美元符,这是因为以美元符开始的标识符和系统任务的保留字冲突。
和其他许多编程语言类似,Verilog也有许多
保留字(或称为关键字),用户定义的标识符不能够和保留字相同。Verilog的保留字均为小写。变量类型中的wire、reg、
integer等、表示过程的initial、always等,以及所有其他的系统任务、编译指令,都是关键字。可以查阅官方文献以完整的关键字的列表。
转义标识符
转义标识符(又称
转义字符双引号,那么就必须使用转义标识符)。下面列出了常用的几种转义标识符。除此之外,在反斜线之后也可以加上字符的
ASCII,这种转义标识符相当于一个字符。常用的转义标识符有
(换行)、 (
制表位)、(空格)、\(反斜杠)和"(英文的双引号)等。
数据类型
四值逻辑
上面列出了Verilog采用的具有八种
信号强度的四值逻辑(four-valued logic),数字电路中的信号可以用
逻辑值、信号强度加以描述。当系统遇到信号之间的竞争时,需要考虑各组信号的状态和强度。如果驱动统一线网的信号强度不同,则输出结果是信号强度高的值;如果两个强度相同的信号之间连接到同一个线网,将会发生竞争,结果为不确定值x。
线网与寄存器
Verilog所用到的所有变量都属于两个基本的类型:线网类型和
寄存器类型。
线网与我们实际使用的电线类似,它的数值一般只能通过连续赋值(continuous assignment),由赋值符右侧连接的驱动源决定。线网在初始化之前的值为x(trireg类型的线网是一个例外,它相当于能够储存电荷的电容器)。如果未连接驱动源,则该线网变量的当前数值为z,即高阻态。线网类型的变量有以下几种:wire、tri、wor、trior、wand、triand、tri0、tri1、supply0、supply1、trireg,其中wire作为一般的电路连线使用最为普遍,而其他几种用于构建总线,即多个驱动源连接到
一条线网的情况,或搭建电源、接地等。当进行模块的端口声明时,如果没有明确指出其类型,那么这个端口会被隐含地声明为wire类型。因此,在声明输出端口时应该注意是否有必要加上reg关键字。以下面的代码片段为例:
module my_moule (out1, out2, in1, in2); //该模块具有两个输出端口
output reg out1; //out1端口被声明为为reg类型,它可以保存
当前值output out2; //out2端口隐含地被声明为为wire类型,它的数值必须依赖连续赋值语句维持
endmodule
寄存器与之不同,它可以保存当前的数值,直到另一个数值被赋值给它。在保持当前数值的过程中,不需要驱动源对它进行作用。如果未对
寄存器变量赋值,它的初始值则为x。Verilog中所说的寄存器类型变量与真实的
硬件寄存器是不同的,它是指一个储存数值的变量。如果要在一个过程(initial过程或always过程)里对
变量赋值,这个变量必须是寄存器类型的。寄存器类型的变量有以下几种:reg(普通寄存器)、integer(整数)、time(时间)、real(实数),其中reg作为一般的寄存器使用最为普遍。利用寄存器变量的数组,还可以对ROM进行建模。
关于选择线网类型还是寄存器类型,需要符合一定的规定。模块的输入端口可以与外界的线网或寄存器类型的变量连接,但是这个模块输出端口只能连接到外界的线网。再简单点,就是在两个模块的信号
连接点,提供信号的一方可以是寄存器或者线网,但是接受信号的一方只能是线网。此外,在initial、always过程代码块中赋值的变量必须是寄存器类型的,而连续赋值的对象只能是线网类型的变量。
数字的表示
在Verilog里,当一个变量的类型确定,即已经知道它是寄存器类型或者是线网类型,当把具体的数值赋值给它时,需要利用下面所述的数字表示方法。数字表示的基本语法结构为<位宽>'<
数制的符号><数值>。其中,位宽是与数据大小相等的对应
二进制数的位数加上占位所用0的位数,这个位数需要使用
十进制来表示。位宽是可选项,如果没有指明位宽,则默认的数据位宽与仿真器有关(最小32位);数制需要用字母来表示,h对应
十六进制,d对应十进制,o对应
八进制,b对应
二进制。如果没有指明数制,则默认数据为
十进制数。例如:
如果某个数的最高位为x或z,那么系统会自动使用x或z来填充没有占据的更高位。如果最高位为其他情况,系统会自动使用0来填充没有占据的更高位。
另外,如果需要使用reg表示负数,可以在位宽之前添加一个
负号,但是需要注意后面的数值为所需负数的二进制
补码。为了防止出错,可以直接使用整数integer或实数real,二者都是
带符号数,再利用省略位宽和数制的十进制数来表示负数。
向量
向量形式的数据是Verilog相对C语言较为特殊的一种数据,但是这种数据在硬件描述语言中十分重要。在Verilog中,
标量的意思是只具有一个
二进制位的变量,而向量表示具有多个二进制位的变量。如果没有特别指明位宽,系统默认它为标量。
在真实的数字电路,例如将两个四位二进制数相加的进位
加法器中,我们可以发现,其中一个数是通过四条电线(每条线表示四位中的某一位)连接到加法器上的。我们可以用一个向量来表示这个
多位数,分别用这个向量的各个分量来表示“四条电线”,即四位中的某一位。这样做的好处是,可以方便地在Verilog代码的其他地方选择其中的一位(位选)或多位(域选)。当然,如果没有进行位选或域选,则这个多位数整体被选择。
向量的表示需要使用
方括号,方括号里的第一个数字为向量第一个分量的序号,第二个数字为向量最后一个分量的序号,中间用
冒号隔开。向量分量的序号不像C语言的数组一样必须从0开始,不过为了和数字电路里二进制数高低位的表示方法一致,我们常常让最低位为0(即对于四位二进制数,其最高位为第3位,次高位为第2位,次低位为第1位,最低位为第0位),当然这只是一种习惯。例如,上面提到的四位二进制数用向量表示为:
wire [3:0] input_add; //声明名为input_add的4位wire型向量
wire [4:1] input_add1; //也是4位wire型向量,但是分量序号从4到1
wire [0:3] input_
add2; //也是4位wire型向量,但是分量序号从0到3
上面的向量声明之后,我们就可以方便地选择其中的某几个分量进行操作。请注意用于域选的方括号的位置在向量名称之后,方括号内的数字为所需的位数。例如我们可以进行以下操作:
input_add [3] = 1'b1; //将1赋值给input_add向量的第三位(最高位)
input_add [1:0] = 2'b01; //将0和1分别赋值给input_add向量的第1、0位(最低两位)
当对向量进行赋值时,如果右边的数值位宽大于左边的变量,则多出来的位被丢弃;如果右边的数值位宽小于左边的变量,则不够的位用0填补。
数组
Verilog中的几种寄存器类型的数据,包括reg、integer、time、real,以及由这几种数据构成的向量,都可以构成数组。
声明数组时,方括号位于数组名的后面,括号内的第一个数字为第一个元素的序号,第二个数字为最后一个元素的序号,中间用冒号隔开。如果数组是由向量构成的,则数组的其中某个元素是向量。同样,出于习惯考虑,我们一般让数组第一个元素的序号为0,后面元素的序号依次递增。此外,和C语言类似,用户可以声明多维数组。例如:
integer number [0:100]; //声明一个有101个元素的整数数组
number [25] = 1234; //将1234赋值给25号(第26个)元素
reg [7:0] my_input [65535:0]; //声明一个有65536个元素的8位向量寄存器
my_input [97] = 8'b10110101; //将10110101分别赋值给97号(第2个)元素的7至0位
reg my_reg [0:3][0:4]; //声明一个具有20个元素的二维寄存器数组
my_reg [1][2] = 1'b1; //将1赋值给上述
二维数组的第2行、第3列元素
由于数组和向量的表示都使用了方括号,因此使用时需要注意这个变量或向量的名称在最初被声明为何种类型的数据。上面第三行的例子是65536个8位向量组成的向量数组,它可以描述一个64KB的
存储器。
表示数组某个元素时,允许使用变量来表示元素的索引(如number [i] = 1234;),但是表示一个向量的一位或者几位时,只允许使用数字来表示位的索引;此外,使用数组时一次只能对一个元素进行操作,而不能像向量那样同时对连续的几个位进行操作,例如my_input [65535][7:4] = 4'b1010;将一个四位二进制数赋值给第65536个元素的高四位。
参数
可以通过parameter关键字声明参数。参数与常数的意义类似,不能够通过赋值运算改变它的数值。在模块进行实例化时,可以能够通过defparam,即参数重载语句块来改变模块实例的参数。另一种方法是在模块实例化时,使用#()将所需的实例参数覆盖模块的
默认参数。
局部参数可以用localparam关键字声明,它不能够进行参数重载。
在设计中使用参数,可以使得模块代码在不同条件下被重复利用,例如四位数
全加器和十六位数全加器可以通过参数实例化同一个通用全加器模块。
字符串
Verilog中的字符串总体来说与C语言中的字符串较为类似,其中每个字符以
ASCII表示,占8位。字符串存储在位宽足够的向量寄存器中。字符串中的空格、换行等特殊内容,以转义标识符(参见前面提到过的转义标识符)的形式表示。
流程控制
为了使设计人员方便地使用
寄存器传输级描述,Verilog提供了多种流程
控制结构,包括if、if...else、if...
else if...else等形式的条件结构,case
分支结构,for、while循环结构。这些流程控制结构与C语言有着相似的用法。不同的循环结构可能造成不同的逻辑综合结果。
Verilog也提供了一些C语言中没有的流程控制结构以适应硬件描述语言的需要,例如casex、casez两种选择结构,前者可以条件数值中的x、z均作为无关值,后者仅将z作为无关值;此外还提供了forever、repeat两种循环结构,分别用于
无限循环和指定次数循环。数字电路的逻辑功能描述常常使用到这些流程控制结构,例如,case结构可以清晰地描述一个
数据选择器。
运算符
Verilog的许多运算符和C语言类似,但是有一部分运算符是特有的,例如拼接运算符、缩减运算符、带有无关位的
相等运算符等。
Verilog的常见运算符隐藏▲
逻辑
缩减
算术
关系
移位
拼接({,}):2个操作数分别作为高低位进行拼,例如:{2'b10,2'b11}的结果是a'b1011
重复({n{m}}):将操作数m重复n次,拼接成一个多位的数。例如:A=2'b01,则{2{A}}的结果是4'b0101
条件(?:):根据?前的表达式是否为真,选择执行后面位于:左右两个语句。例如:(a>b)?(a=a-1):(b=b-2),如果a大于b,则将a-1的值赋给a,否则将b-2的值赋给b
系统任务
系统任务可以被用来执行一些系统设计所需的输入、输出、
时序检查、仿真控制操作。所有的系统任务名称前都带有美元符号$使之与用户定义的任务和函数相区分。
例如,用于显示指定的字符串,然后
自动换行(用法类似C语言中的
printf函数);用于监视变量,一旦被监视的变量发生变化,会显示指定的字符串;而可以提取当前的仿真时间。完整的列表请查阅参考工具、Verilog手册或标准文档。
编译指令
Verilog具有一些编译指令,它们的基本格式为`
,注意第一个符号不是单引号,而是键盘上数字1左边那个键对应的撇号。常用的编译指令有文本宏预定义`define、`include,它们的功能与C语言中类似,分别提供文本替换、文件包含的功能。Verilog还提供了`ifdef、`ifndef等一系列条件编译指令,设计人员可以使得代码在满足一定条件的情况下才进行编译。此外,`timescale指令可以对时间单位进行定义。详细的编译指令清单请参阅相关参考书籍。描述
两种过程
在Verilog中,可以声明两种不同的过程:always过程和initial过程。过程可以是包含时序的
过程描述,而不包含时序的过程还可以表达组合逻辑。always过程从关键字always开始,可以连续多次运行,当过程的最后一行代码执行完成后,再次从第一行代码开始执行。如果没有使用系统任务,always过程将不断循环执行。initial过程从关键字initial开始,它只能执行一次。
一个模块中可以包含多个过程,各个过程相互之间是并发执行的。不过,过程不能够嵌套使用。如果过程中有多个语句,则需要使用关键字begin、end或
fork、join将它们组成一个代码块。这两种关键字组合代表着顺序代码块和并行代码块,后面的部分会讲述这两种结构。
例如,利用always过程循环执行的特点,可以为模块提供一个时间脉冲(注意第一个initial过程为时钟的初始化,这个过程只需要进行一次):
initial a = 1'b0;
always #1 a=~a;
end
虽然,always代码块和
while语句、forever语句都能提供循环功能,但是alway代码块的循环更侧重过程的循环执行,而后二者更侧重代码的循环执行。因此,为了使代码更具条理,过程的循环应当用always语句描述。当然,在实际使用过程中,强制使用其中的某一种在功能实现上都是可行的。
寄存器变量的过程赋值
在Verilog中,有两种赋值运算,一种叫做
阻塞赋值(blocking assignment),其运算符为=;另一种叫做
非阻塞赋值(non-blocking assignment),其运算符为<=。在顺序代码块中使用阻塞赋值语句,如果这一句没有执行完成,那么后面的语句不会执行;如果在顺序代码块中使用非阻塞赋值,则执行这一句的同时,并不会阻碍下一句代码的执行。而且,如果后一个语句涉及前面一个非阻塞赋值语句中的变量,由于这两个语句“同时”执行,因此后一个语句所用到的是前面一个语句执行前变化的数值。非阻塞赋值是Verilog作为硬件描述语言与普通编程语言的一个重大区别。
always @ (posedge reset or posedge clock)
begin
a <= b;
b <= a;
end
endmodule
上面的例子如果没有使用非阻塞
赋值,而使用阻塞赋值,那么a和b的数值就不能被交换。a和b在执行完毕后的数值都与之前b的数值相同。在传统的编程语言中,可能需要一个临时的变量,或者使用指针,才能够达到交换两个变量的目的。这里使用了非阻塞赋值,相当于引入了一个隐含的临时变量。第二个非阻塞赋值右边的a是第一句赋值之前的数值,变量交换的目的得以实现。信号边缘敏感的过程
语句块内常使用非阻塞赋值,使语句块的诸赋值语句同时进行,虽然功能上似乎可以用阻塞赋值实现,但是仿真时会产生不正常的结果。
通常的过程赋值语句往往只有在触发或循环等情况,即赋值语句被执行到时候,才会使左边的
寄存器变量改变一次;而线网变量的连续赋值则一直“监视”右边表达式的变化,一旦其结果发生变化,立即会左边的线网变量更新为此结果。如果需要对寄存器变量进行过程连续赋值,则可以使用Verilog提供的assign或force关键字“强制地”将
赋值运算符右边表达式的结果连续不断地施加在左边的寄存器变量上。
线网变量的连续赋值
对线网类型变量的连续赋值是
数字电路数据流建模的重要步骤,
数字系统不含时的组合逻辑部分可以使用线网的连续赋值描述。线网不能够像寄存器那样储存当前数值,它需要驱动源提供信号,这种驱动是连续不断的,因此线网变量的赋值称为连续赋值,这与寄存器变量在过程中的单次赋值不同,而且所用的运算符也有区别。在Verilog里,线网连续赋值的关键字为assign,下面为一个例子:
module and
wire out;
wire in1, in2;
assign out = in1 & in2;
在这个例子中,线网变量out在系统运行过程中总为两个输入线网变量in1和in2
逻辑与的结果。
线网的连续赋值可以在关键字assgin附加延迟信息,例如上面的代码可以改为:
assign #5 out = in1 & in2; //in1和in2逻辑与的结果在5个
时间周期后才施加在out上
时序控制
Verilog能够描述过程中的时序特性,这也是硬件描述语言与普通
计算机编程语言的重要差别之一。过程的时序控制可以通过三种方式实现:延迟时序控制、事件时序控制以及
电平敏感时序控制。
过程中的时序控制可以控制代码的执行时间。在Verilog中,除了过程中的时序控制,还可以定义元件、路径的延迟。这些延迟请参见本条目后面有关逻辑门级延迟的部分。
延迟时序控制
在代码中使用关键字#和延迟的时间,就可以通过延迟来进行时序控制。延迟的时间可以是数字、变量或者表达式。延迟时序控制又分为两种:常规延迟和内嵌延迟。
常规延迟在赋值语句的左边,系统执行到这一行代码时,系统先进行延迟,延迟完成后,再计算表达式,并将结果赋值给左边的变量;而内嵌延迟在赋值语句的右边,系统执行到这一行代码时,系统先立即计算表达式,再进行延迟,最后把表达式的结果赋值给左边的变量。在上述两种延迟方式中,设计人员需要注意表达式的
自变量在延迟过程中
可能发生变化。常规延迟是先延迟再计算表达式,这时表达式的自变量可能已经发生了变化;而内嵌延迟在延迟前就已经进行了计算,表达式的自变量在延迟过程中发生的变化,对已经计算的表达式结果没有影响,延迟只是指这个结果需要等待一段时间再赋值给左边的变量。
下面的代码片段分别展示了常规延迟和内嵌延迟:
parameter
latency = 8;
initialbegin x = 1;
y = 2;
#5 x = 3; //使用常规延迟:等待5个系统周期后对x赋值
#latency y = 4; //使用变量进行常规延迟,再等待8个系统周期后对y赋值
z = #10 (x+y); //使用内嵌延迟:先用当前时刻的x、y
数值计算(x+y),再等待10个系统周期后对z赋值
end //z的最终数值为3
在顺序语句块(begin...end)中,由于语句是从上到下、一行一行地执行,而所有常规延迟时间都是实际执行时间相对于这一句本来应该开始执行的时间(也是上一句执行完成之时)的延迟值。因此,在上面的代码示例中,对变量y的赋值时间相对于上一句结束延迟了8个系统周期,而上一句相对系统零时刻已经延迟了5个系统周期,因此对y的赋值发生在第13个系统周期。不过,如果顺序语句块中存在非阻塞赋值,由于这个结构有着类似并行语句块的特点,因此需要特别考虑。
在并行语句块(fork...join)中,由于所有语句都是并发执行的,而所有常规延迟时间都是实际执行时间相对于这一句本来应该开始执行的时间(也是上一句执行完成之时)的延迟值,因此各个常规延迟所指的时间都是相对于系统零时刻。
事件时序控制
事件时序控制的意思是,如果指定的事件发生,则代码被触发执行。它的关键字为@,后面可以加变量或者事件名称。参见下面的例子:
@(clk) x = 1; //当变量clk发生变化,则将1赋值给x
@(posedge clk) y = 2; //在变量clk的
上升沿,将2赋值给y
z = @(negedge clk) (x+y); //先立即计算表达式(x+y),然后在变量clk
下降沿,将表达式的结果赋值给z
上面@后面括号里的是常规事件。Verilog允许设计人员通过关键字event和触发符号->定义自己所需要的命名事件触发:
always @(posedge clock)
begin
if(a > 2) ->bigger_than_two; //如果a大于2,则事件bigger_than_two被触发
endalways
@(bigger_than_two) //当bigger_than_two被触发,执行下面的过程
begin
//过程的代码
end
一种经典的用法结构如下,可以理解为“在整个
仿真过程中,一旦某变量发生变化,就执行某操作”:
always @(a)
begin
x = x+1;
end
另一种用法称为OR事件时序控制,其代码结构为@(a or b)或@(a, b),即当a或b其中任意一个变量发生变化时,代码或代码块才被触发执行。监视的变量如果有3个,则其代码结构变为@(a or b or c)或@(a, b, c),以此类推。如果需要监视的变量很多,则可以使用@*或@(*),它表示对之后代码块中的所有输入变量敏感。此外,敏感列表中除了变量,还可以是前面所提到过的常规事件、命名事件。
电平敏感时序控制
Verilog中还有一种电平敏感时序控制方式,即使用wait(a),当变量a为真,则执行后面的代码块。
顺序块与并行块
begin、end组合代表了这个代码块的各行代码是
顺序执行的,这种代码块称为顺序代码块;后面的fork、join代表了这个代码块的各行代码是并发执行的,这种代码块称为并行代码块。与模块、过程不同,两种代码块是可以嵌套,即顺序代码块中可以包含并行代码块。下面的例子展示了这两种代码块嵌套使用的效果:
initial
fork
x = 1;
y = 2;
begin
z = 3;
w = 4;
end
join
由于这个initial过程使用了关键字fork、join,其中x、y的赋值同时于系统零时刻发生,而z和w由于位于一个顺序代码块中,因此w的赋值在z的赋值后才进行。
在使用并行代码块的时候,有可能引起代码的竞争,例如两个语句对一个变量同时进行赋值。虽然理论上两个语句同时执行,但是具体的情况是必然有一句先执行,但这与顺序语句块的“先后”有本质区别。实际的先后顺序取决于所用的仿真系统。这并不是Verilog硬件描述语言本身的缺陷,并行语句块是一种人为设定的功能,这可以让设计人员更容易地描述某些过程,当然他们必须认真考虑竞争带来的潜在问题。
任务和函数
如果某部分代码需要在不同地方多次使用,可以在模块中定义任务或函数。
任务通过关键字task来声明。任务可以有零个或者多个输入变量,但是没有输出
返回值。调用任务时,将按照任务内指定的方式处理这些变量。由于它相当于一个子过程,因此任务中赋值的变量只能是寄存器类型的,而且只能使用过程赋值语句。任务可以具有时序结构,例如延迟、非阻塞赋值等。任务中可以调用任务和函数。与模块的声明不同,任务的声明没有类似模块端口列表的输入变量列表。尽管如此,调用任务的时候,还是需要在括号里按照任务声明时的顺序罗列输入变量。在某种程度上,任务和C语言中没有返回值的函数有些类似。
函数通过关键字function来声明。任务不仅有输入变量,还有一个返回值作为
输出变量,这个返回值的名称与函数的名称相同。函数与任务不同,它是一个只有逻辑功能的部分,不能包含时序结构。函数中只能调用函数。Verilog中的函数与C语言中有返回值的函数有些类似。通常将函数放在
赋值运算符的右边,它的返回值被赋值给左边的变量。
如果任务或函数同时在多个地方被调用,则需要使用automatic关键字声明,这样系统可以为不同地方的调用分配独立的
内存空间。
逻辑门级描述
逻辑门级描述的抽象级别较低,仅次于晶体管级。实际的硬件电路往往都是以逻辑门级网表作为基础构建的,而设计人员常常会在进行更高抽象级别的设计。尽管如此,逻辑门级的设计还是更接近真实电路形式。Verilog提供了一系列逻辑门原语(Primitive)供用户使用。例如,非(
not)、
与门(and)、
或门(or)、
与非门(nand)、或非(nor)、
异或(xor)、同或(
xnor)。逻辑门原语和模块类似,可以通过实例引用的方式使用。
晶体管级描述
Verilog能够在低抽象级别对电路进行描述,是它的一个重要特点。Verilog中提供了多种晶体管级(也称开关级)元件类型,包括N型金属
氧化物半导体场效应管(关键字为nmos)、P型金属氧化物半导体场效应管(关键字为
pmos)、互补式金属氧化物半导体(关键字为cmos)、带
阻抗的互补式金属氧化物半导体(关键字为rcmos)、电源单元(关键字为
supply1)、接地单元(关键字为supply0)等。所有的晶体管都可以设置延迟属性。设计人员可以利用这些低抽象级元件构建所需要的逻辑门或直接构成其他高级组件。
延迟
逻辑门和晶体管的延迟
真实的硬件电路不可避免地都存在延迟现象。在Verilog中,可以对逻辑门、晶体管这些元件的延迟信息进行描述。可以为元件的延迟指定一个时间,则上升、下降、关断的延迟都使用这个时间;也可以按照先后顺序分别指定上升延迟、下降延迟,而关断延迟取二者较小值;当然也可以为上升、下降、关断各指定一个时间。例如,下面的代码为
与门实例添加了三个
延迟时间,分别对应上升、下降、关断:
and #(1, 2, 3) my_and (out, in1, in2);
逻辑门和晶体管的延迟属于“惯性延迟”。它的意思是,逻辑门和晶体管获得外部输入之后,延迟指定的时间后,才会将结果呈现在输出端上。在延迟期间,如果输入改变,但是这个信号的持续时间小于指定延迟的时间,则不会影响逻辑门和晶体管的输出;如果这个信号的持续时间大于指定延迟的时间,则之前的结果将不会呈现在输出端,改变输入信号后的结果将经过延迟后将呈现在输出端
Verilog还允许设计人员为每个延迟时间设置
最大值、
典型值、
最小值,在编译阶段可以通过编译代码选择其中一个。
线网延迟
在声明线网或对线网进行
连续赋值的时候,可以为线网添加延迟信息。这样,所有连续赋值给线网的
表达式都会立即计算出结果,但是这个结果在延迟时间后才会赋值给线网。如果在这段延迟时间内,右侧表达式的结果发生变化,则用于赋值的表达式结果取变化后的。另外,如果如果输入变量变化的
脉冲宽度小于延迟的时间,其变化不会对输出造成影响。这种延迟被称为“惯性延迟”,逻辑门和晶体管的延迟也是这种情况。
过程延迟
过程延迟在前面的延迟
时序控制一部分讲述过。过程
赋值语句中的延迟主要分为常规延迟(又称为外部延迟)和内嵌延迟(又称为内部延迟)两种,其中前者先延迟,再计算表达式、赋值给左边的变量;而后者先立即计算表达式,经过延迟后再将结果赋值给左边的变量。
路径延迟
设计人员可以在模块中关键字
specify、endspecify之间对路径延迟进行描述。与元件的延迟不同,路径延迟是指信号在某两个寄存器类型或线网类型变量之间传递所需的延迟时间。在specify代码块中可以使用条件结构来根据情况选择所需的延迟时间值。与元件延迟相同的是,延迟的时间值可以指定上升、下降、关断的情况,同时也可以包含最大值、典型值、最小值。
逻辑综合
概念
设计人员编写的Verilog代码通常是在较高抽象级别的,例如
寄存器传输级。这一抽象级别包含了对电路信号在
寄存器之间传输情况的描述。但是逻辑门级的
网表,即逻辑门的相互连接形式,才最接近真实的硬件电路。这一形式与寄存器传输级的描述,在功能上是等效的。为了给后续硬件制造人员提供这种低抽象级别的描述,需要将高抽象级别的Verilog代码转换为低抽象级别的逻辑
门级网表。这一过程称为
逻辑综合(Logic Synthesis)。
在自动化逻辑综合工具出现之前,尽管人们可以用硬件描述语言进行设计,但是还是需要人工进行逻辑综合。例如,如果电路模块只有少数几个输入端,我们可以使用类似
卡诺图的方法来对
逻辑函数进行化简。随着电路规模不断增加,人工逻辑综合的容易出错、耗费大量时间的缺点逐渐凸显。同时,在某种
特殊器件工艺下最优化的综合结果不一定在另一种工艺下还合适,如果需要采用另外的工艺,设计人员需要花费很长时间重新进行逻辑综合。随着自动化逻辑综合工具的出现,硬件描述语言、所需器件工艺信息(工艺库)可以直接被逻辑综合工具读取,通过其内部的自动综合算法,输出符合
设计约束(通常包括时序、功耗、面积的约束)的逻辑门级网表。借助自动综合工具,设计人员可以将更多的精力放在高抽象级别的硬件描述语言设计。
可综合代码
逻辑综合工具不能接受所有的Verilog代码。设计人员需要确保硬件描述语言代码是周期到周期的
寄存器传输级描述。诸如while的
循环结构必须通过
信号边缘的形式(如@(posedge clock))提供终止条件;initial结构可能也不能被转换。如果不指明数字的
位宽,那么系统可能默认它为一个较大的值(如32位),这就可能产生规模非常庞大的逻辑门级网表,其中一部分是不必要的,这将造成资源的浪费。与未知逻辑x、高阻态z有关的
运算符不能被转换,例如===、!==此外,条件结构如果只有if而没有对else的情况进行设计,或者
选择结构缺少默认情况default,很可能产生预期之外的
锁存器。由于需要使用与工艺相关的逻辑门,因此用户自定义的原语很可能不能被转换。设计人员需要采取良好的
代码风格,以获得更优化的逻辑综合结果。为了适应符合可重用
设计思想的
系统芯片、
IP核设计,设计人员还应该遵循更严格的编码规范。
高级功能
自定义原语
除了系统提供的26种
逻辑门、
晶体管原语,Verilog也提供用户自定义原语(User Defined Primitive, UDP)。原语与模块的
层次结构类似,但是原语的输入输出关系是完全通过查表实现的。组合逻辑的用户自定义原语的核心是
真值表,
时序逻辑的用户自定义原语的核心是激励表。设计人员需要在状态表中罗列可能出现的输入和输出情况。如果在实际使用过程中,遇到状态表中没有定义的情况,则输出不确定值x。使用自定义原语很直观,但是如果输入变量较多,状态表就会变得很复杂。在很多情况中,用户自定义原语并不能被
逻辑综合工具转换。
编程语言接口
编程语言接口(Program Language Interface, PLI)提供了通过
C语言函数对Verilog数据结构进行存储、读取操作的途径。
Verilog编程语言接口的发展先后经过了三代,其中第一代为任务或函数
子程序,它可以在C程序和Verilog设计之间传递数据;第二代为存取子程序,它可以在用户自定义C程序和Verilog的内部
数据表示的接口上被使用;第三代为Verilog过程接口,它进一步扩展了前两代编程语言接口的功能。
通过使用编程语言接口,设计人员可以自定义接口的功能,然后通过类似调用系统任务的方式调用这些自定义功能。这样,设计人员可以很大程度地扩展他们能使用的功能,例如监视、激励、调试功能,或者用它来提取
设计信息、
显示输出等。
相关工具
Verilog作为业界使用最广泛的硬件描述语言之一,有大量的电子设计自动化工具对它予以支持。通过使用
集成开发环境,设计人员可以在常见的Windows或其他图形化系统中进行设计、仿真、验证,例如
Cadence和
Synopsys等公司提供的集成电路计算机辅助设计系统。
与VHDL比较
VHDL——VHSIC(Very High Speed Integrated Circuit) HDL,由美国DOD支持开发的HDL,1987年成为
IEEE 1076-1987 标准,后修订为IEEE 1076-1993 标准。
Verilog来自C 语言,易学易用,编程风格灵活、简洁,使用者众多,特别在
ASIC领域流行;
VHDL 来自
ADA,语法严谨,比较难学,在欧洲和国内有较多使用者;
两者描述的设计层次有所不同:
VerilogHDL:行为级、RTL 级、门级、开关级,不支持:电路级(
spice)、版图级(GDSII/CIF)
用途
Verilog HDL就是在用途最广泛的C语言的基础上发展起来的一种硬件描述语言,它是由GDA(Gateway Design Automation)公司的PhilMoorby在1983年末首创的,最初只设计了一个仿真与验证工具,之后又陆续开发了相关的故障模拟与
时序分析工具。1985年Moorby推出它的第三个商用
仿真器Verilog-XL,获得了巨大的成功,从而使得Verilog HDL迅速得到推广应用。1989年CADENCE公司收购了GDA公司,使得VerilogHDL成为了该公司的独家专利。1990年CADENCE公司
公开发表了Verilog HDL,并成立LVI组织以促进Verilog HDL成为IEEE标准,即IEEE Standard 1364-1995.
Verilog HDL的最大特点就是易学易用,如果有C语言的编程经验,可以在一个较短的时间内很快的学习和掌握,因而可以把Verilog HDL内容安排在与
ASIC设计等
相关课程内部进行讲授,由于HDL语言本身是专门面向硬件与
系统设计的,这样的安排可以使学习者同时获得设计
实际电路的经验。与之相比,VHDL的学习要困难一些。但Verilog HDL较自由的语法,也容易造成初学者犯一些错误,这一点要注意。