http://zh.wikipedia.org/wiki/Verilog#.E6.95.B0.E7.BB.84
Verilogs是由Gateway设计自动化公司的工程师于1983年末创立的。当时Gateway设计自动化公司还叫做自动集成设计系统(Automated Integrated Design Systems),1985年公司将名字改成了前者。该公司的菲尔·莫比(Phil Moorby)完成了Verilog的主要设计工作。1990年,Gateway设计自动化被Cadence公司收购。[7]
1990年代初,开放Verilog国际(Open Verilog International, OVI,即现在的Accellera)组织成立,Verilog面向公有领域开放。[8]1992年,该组织寻求将Verilog纳入IEEE标准 。最终,Verilog成为了IEEE 1364-1995,即通常所说的Verilog-95。[9]
设计人员在使用这个版本的Verilog的过程中发现了一些可改进之处。为了解决用户在使用此版本Verilog过程中反映的问题,Verilog进行了修正和扩展,这部分内容后来再次被提交给电气电子工程师学会。这个扩展后的版本后来成为了IEEE 1364-2001,即通常所说的Verilog-2001。Verilog-2001是对Verilog-95的一个重大改进版本,它具备一些新的实用功能,例如敏感列表、多维数组、生成语句块、命名端口连接等。[10]目前,Verilog-2001是Verilog的最主流版本,被大多数商业电子设计自动化软件包支持。
2005年,Verilog再次进行了更新,即IEEE 1364-2005标准。该本本只是对上一版本的细微修正。这个版本还包括了一个相对独立的新部分,即Verilog-AMS。[11]这个扩展使得传统的Verilog可以对集成的模拟和混合信号系统进行建模。[12]:255容易与IEEE 1364-2005混淆的是SystemVerilog硬件验证语言(IEEE 1800-2005),它是Verilog-2005的一个超集,它是集成硬件描述语言、硬件验证语言的一个集成。[13]
2009年,IEEE 1364-2005和IEEE 1800-2005两个部分合并为IEEE 1800-2009,成为了一个新的、统一的SystemVerilog硬件描述验证语言(hardware description and verification language, HDVL)。[14][15]
描述复杂的硬件电路,设计人员总是将复杂的功能划分为简单的功能,模块是提供每个简单功能的基本结构。设计人员可以采取“自顶向下”的思路,将复杂的功能模块划分为低层次的模块。[16]:58这一步通常是由系统级的总设计师完成,而低层次的模块则由下一级的设计人员完成。自顶向下的设计方式有利于系统级别层次划分和管理,并提高了效率、降低了成本。[17]“自底向上”方式是“自顶向下”方式的逆过程。
使用Verilog描述硬件的基本设计单元是模块(module)。[18]构建复杂的电子电路,主要是通过模块的相互连接调用来实现的。模块被包含在关键字module
、endmodule
之内。[16]:59实际的电路元件。Verilog中的模块类似C语言中的函数,它能够提供输入、输出端口,可以实例调用其他模块,也可以被其他模块实例调用。模块中可以包括数据流(组合逻辑)部分、行为(时序)部分。[18]例如,四选一的多路选择器,就可以用模块进行描述。它具有两个位选输入信号、四个数据输入,一个输出端,在Verilog中可以表示为:
module mux (out, select, in0, in1, in2, in3); output out; input [1:0] select; input in0, in1, in2, in3; //具体数据流、行为描述 endmodule
设计人员可以使用一个顶层模块,通过实例调用上面这个模块的方式来进行测试。这个顶层模块常被称为“测试平台(Testbench)”。[19]为了最大程度地对电路的逻辑进行功能验证,测试代码需要尽可能多地覆盖系统所涉及的语句、分支、条件、路径、触发、状态机状态,[20]:141-144验证人员需要在测试平台里创建足够多的输入激励,[21]:8并连接到被测模块的输入端,然后检测其输出端的表现是否符合预期(诸如SystemVerilog的硬件验证语言能够提供针对验证专门优化的数据结构,以随机测试的方式进行验证,这对于高度复杂的集成电路设计验证可以起到关键作用)。实例调用模块时,需要将端口的连接情况按照这个模块声明时的顺序排列。这个顶层模块由于不需要再被外界调用,因此没有输入输出端口:[3]:4
module tester; reg [1:0] SELECT; reg IN0, IN1, IN2, IN3; wire OUT; mux my_mux (OUT, SELECT, IN0, IN1, IN2, IN3); //实例调用mux模块,这个实例被命名为my_mux initial //需要仿真的激励代码 begin end endmodule
在这个测试平台模块里,设计人员可以设定仿真时的输入信号以及信号监视程序,然后观察仿真时的输出情况是否符合要求,这样就可以了解设计是否达到了预期。[19]
示例中的对模块进行实例引用时,按照原模块声明时的顺序罗列了输入变量。除此之外,还可以使用或者采用命名端口连接的方式。使用这种方式,端口的排列顺序可以与原模块声明时不同,甚至可以不连接某些端口:
mux my_mux (.out(OUT), .select(SELECT), .in0(IN0), .in1(IN1), .in2(IN2), .in3(IN3)); //使用命名端口连接,括号外面是模块声明时的端口,括号内是实际的端口连接 //括号外相当于C语言的形式参数,括号内相当于实际参数 endmodule
上面所述的情况是,测试平台顶层模块的测试变量直接连接了所设计的功能模块。测试平台还可以是另一种形式,即测试平台并不直接连接所设计的功能模块,而是在这个测试平台之下,将激励模块和功能模块以相同的抽象级别,通过线网相互连接。[22]这两种形式的测试平台都可以完成对功能模块的测试。大型的电路系统,正是由各个层次不同模块之间的连接、调用,来实现复杂的功能的。[3]:11
Verilog的设计初衷是成为一种基本语法与C语言相近的硬件描述语言。[2]:18这是因为C语言在Verilog设计之初,已经在许多领域得到广泛应用,C语言的许多语言要素已经被许多人习惯。一种与C语言相似的硬件描述语言,可以让电路设计人员更容易学习和接受。不过,Verilog与C语言还是存在许多差别。另外,作为一种与普通计算机编程语言不同的硬件描述语言,它还具有一些独特的语言要素,例如向量形式的线网和寄存器、过程中的非阻塞赋值等。总的来说,具备C语言的设计人员将能够很快掌握Verilog硬件描述语言。[23]
空白符是指代码中的空格(对应的转义标识符为\b
)、制表符(\t
)和换行(\n
)。[16]:17如果这些空白符出现在字符串里,那么它们不可忽略。除此之外,代码中的其他空白符在编译的时候都将会被视为分隔标识符,即使用2个空格或者1个空格并无影响。[24]不过,在代码中使用合适的空格,可以让上下行代码的外观一致(例如使赋值运算符位于同一个竖直列),从而提高代码的可读性。
为了方便代码的修改或其他人的阅读,设计人员通常会在代码中加入注释。与C语言一样,有两种方式书写注释。第一种为多行注释,即注释从/*
开始,直到*/
才结束;另一种为单行注释,注释从//
开始,从这里到这一行末尾的内容会被系统识别为注释。[25]
Verilog是一种大小写敏感的硬件描述语言。其中,它的所有系统关键字都是小写的。[3]:29[26]
Verilog代码中用来定义语言结构名称的字符称为标识符,包括变量名、端口名、模块名等等。标识符可以由字母、数字、下划线以及美元符($
)来表示。但是标识符的第一个字符只能是字母、数字或者下划线,不能为美元符,这是因为以美元符开始的标识符和系统任务的保留字冲突。[27]:19
和其他许多编程语言类似,Verilog也有许多保留字(或称为关键字),用户定义的标识符不能够和保留字相同。Verilog的保留字均为小写。[27]:19变量类型中的wire
、reg
、integer
等、表示过程的initial
、always
等,以及所有其他的系统任务、编译指令,都是关键字。[28]可以查阅官方文献以完整的关键字的列表。
转义标识符(又称转义字符),是由\
开始,以空白符结束的一种特殊编程语言结构。[28]这种结构可以用来表示那些容易与系统语言结构相同的内容(例如"
在系统中被用来表示字符串,如果字符串本身的内容包含一个与之形式相同的双引号,那么就必须使用转义标识符)。下面列出了常用的几种转义标识符。除此之外,在反斜线之后也可以加上字符的ASCII,这种转义标识符相当于一个字符。常用的转义标识符有\n
(换行)、\t
(制表位)、\b
(空格)、\\
(反斜杠)和\"
(英文的双引号)等。
|
|
上面列出了Verilog采用的具有八种信号强度的四值逻辑(four-valued logic),数字电路中的信号可以用逻辑值、信号强度加以描述。当系统遇到信号之间的竞争时,需要考虑各组信号的状态和强度。如果驱动统一线网的信号强度不同,则输出结果是信号强度高的值;如果两个强度相同的信号之间连接到同一个线网,将会发生竞争,结果为不确定值x
。[27]:20
Verilog所用到的所有变量都属于两个基本的类型:线网类型和寄存器类型。[3]:30
线网与我们实际使用的电线类似,它的数值一般只能通过连续赋值(continuous assignment),由赋值符右侧连接的驱动源决定。[29]线网在初始化之前的值为x(trireg
类型的线网是一个例外,它相当于能够储存电荷的电容器[22]:105)。如果未连接驱动源,则该线网变量的当前数值为z
,即高阻态。线网类型的变量有以下几种:wire
、tri
、wor
、trior
、wand
、triand
、tri0
、tri1
、supply0
、supply1
、trireg
,其中wire
作为一般的电路连线使用最为普遍,[30]而其他几种用于构建总线,即多个驱动源连接到一条线网的情况,[3]:79-80或搭建电源、接地等。当进行模块的端口声明时,如果没有明确指出其类型,那么这个端口会被隐含地声明为wire
类型。因此,在声明输出端口时应该注意是否有必要加上reg
关键字。以下面的代码片段为例:
module my_moule (out1, out2, in1, in2); //该模块具有两个输出端口 output reg out1; //out1端口被声明为为reg类型,它可以保存当前值 output out2; //out2端口隐含地被声明为为wire类型,它的数值必须依赖连续赋值语句维持 endmodule
寄存器与之不同,它可以保存当前的数值,直到另一个数值被赋值给它。在保持当前数值的过程中,不需要驱动源对它进行作用。[29]如果未对寄存器变量赋值,它的初始值则为x
。Verilog中所说的寄存器类型变量与真实的硬件寄存器是不同的,它是指一个储存数值的变量。如果要在一个过程(initial
过程或always
过程)里对变量赋值,这个变量必须是寄存器类型的。寄存器类型的变量有以下几种:reg
(普通寄存器)、integer
(整数)、time
(时间)、real
(实数),其中reg
作为一般的寄存器使用最为普遍。[31]利用寄存器变量的数组,还可以对ROM进行建模。[29]
关于选择线网类型还是寄存器类型,需要符合一定的规定。模块的输入端口可以与外界的线网或寄存器类型的变量连接,但是这个模块输出端口只能连接到外界的线网。再简单点,就是在两个模块的信号连接点,提供信号的一方可以是寄存器或者线网,但是接受信号的一方只能是线网。[27]:35-36此外,在initial
、always
过程行为描述语句中赋值的变量必须是寄存器类型的,[27]:58[3]:30而连续赋值的对象只能是线网类型的变量。[27]:81[3]:81
在Verilog里,当一个变量的类型确定,即已经知道它是寄存器类型或者是线网类型,当把具体的数值赋值给它时,需要利用下面所述的数字表示方法。数字表示的基本语法结构为<位宽>'<数制的符号><数值>
。[3]:31其中,位宽是与数据的二进制数位的位数加上占位所用0的位数,这个位数需要使用十进制来表示。位宽是可选项,如果没有指明位宽,则默认的数据位宽与仿真器有关(最小32位);数制需要用字母来表示,h
对应十六进制,d
对应十进制,o
对应八进制,b
对应二进制。如果没有指明数制,则默认数据为十进制数。[27]:18例如:
如果某个数的最高位为x
或z
,那么系统会自动使用x
或z
来填充没有占据的更高位。如果最高位为其他情况,系统会自动使用0来填充没有占据的更高位。
另外,如果需要使用reg
表示负数,可以在位宽之前添加一个负号,但是需要注意后面的数值为所需负数的二进制补码。[3]:31为了防止出错,可以直接使用整数integer
或实数real
,二者都是带符号数,再利用省略位宽和数制的十进制数来表示负数。[27]:63
向量形式的数据是Verilog相对C语言较为特殊的一种数据,但是这种数据在硬件描述语言中十分重要。在Verilog中,标量的意思是只具有一个二进制位的变量,而向量表示具有多个二进制位的变量。如果没有特别指明位宽,系统默认它为标量。[27]:21
在真实的数字电路,例如将两个四位二进制数相加的进位加法器中,我们可以发现,其中一个数是通过四条电线(每条线表示四位中的某一位)连接到加法器上的。我们可以用一个向量来表示这个多位数,分别用这个向量的各个分量来表示“四条电线”,即四位中的某一位。这样做的好处是,可以方便地在Verilog代码的其他地方选择其中的一位(位选)或多位(域选)。[27]:22当然,如果没有进行位选或域选,则这个多位数整体被选择。
向量的表示需要使用方括号,方括号里的第一个数字为向量第一个分量的序号,第二个数字为向量最后一个分量的序号,中间用冒号隔开。向量分量的序号不像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中的几种寄存器类型的数据,[3]:32包括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的存储器。
可以通过parameter
关键字声明参数。参数与常数的意义类似,不能够通过赋值运算改变它的数值。在模块进行实例化时,可以能够通过defparam
,即参数重载语句块来改变模块实例的参数。另一种方法是在模块实例化时,使用#()
将所需的实例参数覆盖模块的默认参数。局部参数可以用localparam
关键字声明,它不能够进行参数重载。[27]:25
Verilog中的字符串总体来说与C语言中的字符串较为类似,其中每个字符以ASCII表示,占8位。[32]字符串存储在位宽足够的向量寄存器中。字符串中的空格、换行等特殊内容,以转义标识符(参见前面提到过的转义标识符)的形式表示。
为了使设计人员方便地使用行为级描述,Verilog提供了多种流程控制结构,包括if
、if...else
、if...else if...else
等形式的条件结构,case
分支结构,for
、while
循环结构。这些流程控制结构与C语言有着相似的用法。不同的循环结构可能造成不同的逻辑综合结果。[33]Verilog也提供了一些C语言中没有的流程控制结构以适应硬件描述语言的需要,例如casex
、casez
两种选择结构,前者可以条件数值中的x
、z
均作为无关值,后者仅将z
作为无关值;[22]:53[3]:87-88此外还提供了forever
、repeat
两种循环结构,分别用于无限循环和指定次数循环。[2]:56-65[16]:109-126数字电路的行为描述常常使用到这些流程控制结构,例如,case
结构可以清晰地描述一个数据选择器。
Verilog的许多运算符和C语言类似,但是有一部分运算符是特有的,例如拼接运算符、缩减运算符、带有无关位的相等运算符等。[34]
系统任务可以被用来执行一些系统设计所需的输入、输出、时序检查、仿真控制操作。[3]:51-53所有的系统任务名称前都带有美元符号$
使之与用户定义的任务和函数相区分。[35]例如,$display
用于显示指定的字符串,然后自动换行(用法类似C语言中的printf
函数);$monitor
用于监视变量,一般变量生变化,会显示示指定的字符串;而$time
可以提取当前的仿真时间。完整的列表请查阅参考工具、Verilog手册或标准文档。[36]
Verilog具有一些编译指令,它们的基本格式为`
,注意第一个符号不是单引号,而是键盘上数字1左边那个键对应的撇号。常用的编译指令有文本宏预定义`define
、`include
,它们的功能与C语言中类似,分别提供文本替换、文件包含的功能。Verilog还提供了`ifdef
、`ifndef
等一系列条件编译指令,设计人员可以使得代码在满足一定条件的情况下才进行编译。此外,`timescale
指令可以对时间单位进行定义。[27]:132详细的编译指令清单请参阅相关参考书籍。
在Verilog中,可以声明两种不同的过程:always过程和initial过程。过程可以是系统行为级的描述,而不包含时序的过程还可以表达组合逻辑。[22]:170always过程从关键字always
开始,可以连续多次运行,当过程的最后一行代码执行完成后,再次从第一行代码开始执行。如果没有使用系统任务$finish
,always过程将不断循环执行。initial过程从关键字initial
开始,它只能执行一次。[37]:140-145
一个模块中可以包含多个过程,各个过程相互之间是并发执行的。不过,过程不能够嵌套使用。如果过程中有多个语句,则需要使用关键字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),其运算符为<=
。在顺序代码块中使用阻塞赋值语句,如果这一句没有执行完成,那么后面的语句不会执行;如果在顺序代码块中使用非阻塞赋值,则执行这一句的同时,并不会阻碍下一句代码的执行。[3]:47而且,如果后一个语句涉及前面一个非阻塞赋值语句中的变量,由于这两个语句“同时”执行,因此后一个语句所用到的是前面一个语句执行前变化的数值。非阻塞赋值是Verilog作为硬件描述语言与普通编程语言的一个重大区别。
带有两个触发器输出端的简单示例如下
always @ (posedge reset or posedge clock) begin a <= b; b <= a; end endmodule
上面的例子如果没有使用非阻塞赋值,而使用阻塞赋值,那么flop1
和flop2
的数值就不能被交换。flop1
和flop2
在执行完毕后的数值都与之前flop2
的数值相同。在传统的编程语言中,可能需要一个临时的变量,或者使用指针,才能够达到交换两个变量的目的。这里使用了非阻塞赋值,相当于引入了一个隐含的临时变量。第二个非阻塞赋值右边的a
是第一句赋值之前的数值,变量交换的目的得以实现。[27]:84信号边缘敏感的行为语句块内常使用非阻塞赋值,使语句块的诸赋值语句同时进行,虽然功能上似乎可以用阻塞赋值实现,但是仿真时会产生不正常的结果。[22]:180
通常的过程赋值语句往往只有在触发或循环等情况,即赋值语句被执行到时候,才会使左边的寄存器变量改变一次;而线网变量的连续赋值则一直“监视”右边表达式的变化,一旦其结果发生变化,立即会左边的线网变量更新为此结果。[22]如果需要对寄存器变量进行过程连续赋值,则可以使用Verilog提供的assign
或force
关键字“强制地”将赋值运算符右边表达式的结果连续不断地施加在左边的寄存器变量上。
对线网类型变量的连续赋值是数字电路数据流建模的重要步骤,数字系统不含时的组合逻辑部分可以使用线网的连续赋值描述。线网不能够像寄存器那样储存当前数值,它需要驱动源提供信号,这种驱动是连续不断的,因此线网变量的赋值称为连续赋值,这与寄存器变量在过程中的单次赋值不同,而且所用的运算符也有区别。在Verilog里,线网连续赋值的关键字为assgin
,下面为一个例子:
module and wire out; wire in1, in2; assign out = in1 & in2;
在这个例子中,线网变量out
在系统运行过程中总为两个输入线网变量in1
和in2
逻辑与的结果。
线网的连续赋值可以在关键字assgin
附加延迟信息,[3]:43例如上面的代码可以改为:
assign #5 out = in1 & in2; //in1和in2逻辑与的结果在5个时间周期后才施加在out上
Verilog能够描述过程中的时序特性,这也是硬件描述语言与普通计算机编程语言的重要差别之一。过程的时序控制可以通过三种方式实现:延迟时序控制、事件时序控制以及电平敏感时序控制。
过程中的时序控制可以控制代码的执行时间。在Verilog中,除了过程中的时序控制,还可以定义元件、路径的延迟。这些延迟请参见本条目后面有关逻辑门级延迟的部分。
在代码中使用关键字#
和延迟的时间,就可以通过延迟来进行时序控制。延迟的时间可以是数字、变量或者表达式。延迟时序控制又分为两种:常规延迟和内嵌延迟。[16]:89
常规延迟在赋值语句的左边,系统执行到这一行代码时,系统先进行延迟,延迟完成后,再计算表达式,并将结果赋值给左边的变量;而内嵌延迟在赋值语句的右边,系统执行到这一行代码时,系统先立即计算表达式,再进行延迟,最后把表达式的结果赋值给左边的变量。在上述两种延迟方式中,设计人员需要注意表达式的自变量在延迟过程中可能发生变化。常规延迟是先延迟再计算表达式,这时表达式的自变量可能已经发生了变化;而内嵌延迟在延迟前就已经进行了计算,表达式的自变量在延迟过程中发生的变化,对已经计算的表达式结果没有影响,延迟只是指这个结果需要等待一段时间再赋值给左边的变量。[16]:89-93
下面的代码片段分别展示了常规延迟和内嵌延迟:
parameter latency = 8; initial begin 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
和触发符号->
定义自己所需要的命名事件触发:
event bigger_than_two; always @(posedge clock) begin if(a > 2) ->bigger_than_two; //如果a大于2,则事件bigger_than_two被触发 end always @(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)
,以此类推。如果需要监视的变量很多,则可以使用@*
或@(*)
,它表示对之后代码块中的所有输入变量敏感。此外,敏感列表中除了变量,还可以是前面所提到过的常规事件、命名事件。[27]:87-89
Verilog中还有一种电平敏感时序控制方式,即使用wait(a)
,当变量a
为真,则执行后面的代码块。[27]:69
begin
、end
组合代表了这个代码块的各行代码是顺序执行的,这种代码块称为顺序代码块;[16]:66-67后面的fork
、join
代表了这个代码块的各行代码是并发执行的,这种代码块称为并行代码块。[16]:68-69与模块、过程不同,两种代码块是可以嵌套,即顺序代码块中可以包含并行代码块。[16]:69-72下面的例子展示了这两种代码块嵌套使用的效果:
initial fork x = 1; y = 2; begin z = 3; w = 4; end join
由于这个initial
过程使用了关键字fork
、join
,其中x
、y
、z
的赋值同时于系统零时刻发生,而z
和w
由于位于一个顺序代码块中,因此w
的赋值在z
的赋值后才进行。
在使用并行代码块的时候,有可能引起代码的竞争,例如两个语句对一个变量同时进行赋值。虽然理论上两个语句同时执行,但是具体的情况是必然有一句先执行,但这与顺序语句块的“先后”有本质区别。实际的先后顺序取决于所用的仿真系统。[27]:95这并不是Verilog硬件描述语言本身的缺陷,并行语句块是一种人为设定的功能,这可以让设计人员更容易地描述某些过程,当然他们必须认真考虑竞争带来的潜在问题。
如果某部分代码需要在不同地方多次使用,可以在模块中定义任务或函数。
任务通过关键字task
来声明。任务可以有零个或者多个输入变量,但是没有输出返回值。调用任务时,将按照任务内指定的方式处理这些变量。由于它相当于一个子过程,因此任务中赋值的变量只能是寄存器类型的,而且只能使用行为级语句。任务可以具有时序结构,例如延迟、非阻塞赋值等。任务中可以调用任务和函数。[2]:79与模块的声明不同,任务的声明没有类似模块端口列表的输入变量列表。尽管如此,调用任务的时候,还是需要在括号里按照任务声明时的顺序罗列输入变量。[22]:57在某种程度上,任务和C语言中没有返回值的函数有些类似。
函数通过关键字function
来声明。任务不仅有输入变量,还有一个返回值作为输出变量,这个返回值的名称与函数的名称相同。函数与任务不同,它是一个只有逻辑功能的部分,不能包含时序结构。函数中只能调用函数。[2]:79Verilog中的函数与C语言中有返回值的函数有些类似。通常将函数放在赋值运算符的右边,它的返回值被赋值给左边的变量。[38]
如果任务或函数同时在多个地方被调用,则需要使用automatic
关键字声明,这样系统可以为不同地方的调用分配独立的内存空间。[27]:118[27]:121-122
逻辑门级描述的抽象级别较低,仅次于开关级(晶体管级)。实际的硬件电路往往都是以逻辑门级网表作为基础构建的,而设计人员常常会在进行更高抽象级别的设计。尽管如此,逻辑门级的设计还是更接近真实电路形式。Verilog提供了一系列逻辑门原语(Primitive)供用户使用。例如,非(not
)、与门(and
)、或门(or
)、与非门(nand
)、或非(nor
)、异或(xor
)、同或(xnor
)。逻辑门原语和模块类似,可以通过实例引用的方式使用。[16]:156
Verilog能够在低抽象级别对电路进行描述,是它的一个重要特点。Verilog中提供了多种元件类型,包括N型金属氧化物半导体场效应管(关键字为nmos
)、包括P型金属氧化物半导体场效应管(关键字为pmos
)、互补式金属氧化物半导体(关键字为cmos
)几种双向开关、带阻抗的开关(例如rcmos
)、电源(关键字为supply1
)、接地(关键字为supply0
)。所有的开关元件都可以设置延迟属性。[3]:9设计人员可以利用这些低级元件构建所需要的逻辑门或直接构成其他高级组件。[27]:158-167
真实的硬件电路不可避免地都存在延迟现象。在Verilog中,可以对逻辑门、开关这些元件的延迟信息进行描述。可以为元件的延迟指定一个时间,则上升、下降、关断的延迟都使用这个时间;也可以按照先后顺序分别指定上升延迟、下降延迟,而关断延迟取二者较小值;当然也可以为上升、下降、关断各指定一个时间。[27]:50-51例如,下面的代码为与门实例添加了三个延迟时间,分别对应上升、下降、关断:
and #(1, 2, 3) my_and (out, in1, in2);
逻辑门和开关的延迟被称为“惯性延迟”。它的意思是,逻辑门和开关获得外部输入之后,延迟指定的时间后,才会将结果呈现在输出端上。在延迟期间,如果输入改变,但是这个信号的持续时间小于指定延迟的时间,则不会影响逻辑门和开关的输出;如果这个信号的持续时间大于指定延迟的时间,则之前的结果将不会呈现在输出端,改变输入信号后的结果将经过延迟后将呈现在输出端[22]:143-144
Verilog还允许设计人员为每个延迟时间设置最大值、典型值、最小值,在编译阶段可以通过编译代码选择其中一个。[27]:51-52
在声明线网或对线网进行连续赋值的时候,可以为线网添加延迟信息。这样,所有连续赋值给线网的表达式都会立即计算出结果,但是这个结果在延迟时间后才会赋值给线网。[3]:43如果在这段延迟时间内,右侧表达式的结果发生变化,则用于赋值的表达式结果取变化后的。另外,如果如果输入变量变化的脉冲宽度小于延迟的时间,其变化不会对输出造成影响。这种延迟被称为“惯性延迟”,[27]:59逻辑门和开关的延迟也是这种情况。
过程延迟在前面的延迟时序控制一部分讲述过。过程赋值语句中的延迟主要分为常规延迟(又称为外部延迟)和内嵌延迟(又称为内部延迟)两种,其中前者先延迟,再计算表达式、赋值给左边的变量;而后者先立即计算表达式,经过延迟后再将结果赋值给左边的变量。[16]:89-93
设计人员可以在模块中关键字specify
、endspecify
之间对路径延迟进行描述。与元件的延迟不同,路径延迟是指信号在某两个寄存器类型或线网类型变量之间传递所需的延迟时间。在specify
代码块中可以使用条件结构来根据情况选择所需的延迟时间值。与元件延迟相同的是,延迟的时间值可以指定上升、下降、关断的情况,同时也可以包含最大值、典型值、最小值。[27]:148-153
设计人员编写的Verilog代码通常是在较高抽象级别的,例如寄存器传输级。这一抽象级别包含了对电路数据流级和行为级的描述。但是逻辑门级的网表,即逻辑门的相互连接形式,才最接近真实的硬件电路。这一形式与寄存器传输级的描述,在功能上是等效的。为了给后续硬件制造人员提供这种低抽象级别的描述,需要将高抽象级别的Verilog代码转换为低抽象级别的逻辑门级网表。这一过程称为逻辑综合(Logic Synthesis)。[3]:5[12]:253
在自动化逻辑综合工具出现之前,尽管人们可以用硬件描述语言进行设计,但是还是需要人工进行逻辑综合。例如,如果电路模块只有少数几个输入端,我们可以使用类似卡诺图的方法来对逻辑函数进行化简。随着电路规模不断增加,人工逻辑综合的容易出错、耗费大量时间的缺点逐渐凸显。同时,在某种特殊器件工艺下最优化的综合结果不一定在另一种工艺下还合适,如果需要采用另外的工艺,设计人员需要花费很长时间重新进行逻辑综合。随着自动化逻辑综合工具的出现,硬件描述语言、所需器件工艺信息(工艺库)可以直接被逻辑综合工具读取,通过其内部的算法,输出符合设计约束(通常包括时序、功耗、面积的约束)的逻辑门级网表。设计人员可以将更多的精力放在高抽象级别的硬件描述语言设计。[27]:203-204
逻辑综合工具不能接受所有的Verilog代码。设计人员需要确保硬件描述语言代码是周期到周期的寄存器传输级描述。诸如while
的循环结构必须通过信号边缘的形式(如@(posedge clock)
)提供终止条件;initial
结构可能也不能被转换。如果不指名数字的位宽,那么系统可能默认它为一个较大的值(如32位),这就可能产生规模非常庞大的逻辑门级网表,其中一部分是不必要的,这将造成资源的浪费。与未知逻辑x
、高阻态z
有关的运算符不能被转换,例如===
、!==
此外,条件结构如果只有if
而没有对else
的情况进行设计,或者选择结构缺少默认情况default
,很可能产生预期之外的锁存器。[3]:93由于需要使用与工艺相关的逻辑门,因此用户自定义的原语很可能不能被转换。[3]:91设计人员需要采取良好的代码风格,以获得更优化的逻辑综合结果。[27]:204-219为了适应符合可重用设计思想的系统芯片、IP核设计,设计人员还应该遵循更严格的编码规范。[39]
结构类型 | 注 |
---|---|
initial |
只用于仿真测试文件(test bench) |
events |
Events 对于同步测试文件的各个组件比较有意义 |
real |
Real 数据类型不可综合 |
time |
Time 数据类型不可综合 |
force 和release |
Force 和release 不可综合 |
assign 和deassign |
reg 类型的assign 和deassign 操作不可综合,但是wire 类型的assign 操作可以综合 |
fork join |
使用非阻塞赋值可以获得同样效果 |
primitive |
只有门级的原语(primitives)可综合 |
table |
用户自定义原语(UDP)及table 不可综合 |
#1 |
延迟只用于仿真,综合器一般直接忽略延迟 |
除了系统提供的26种逻辑门、开关原语,[22]:103-104Verilog也提供用户自定义原语(User Defined Primitive, UDP)。原语与模块的层次结构类似,但是原语的输入输出关系是完全通过查表实现的。组合逻辑的用户自定义原语的核心是真值表,时序逻辑的用户自定义原语的核心是激励表。设计人员需要在状态表中罗列可能出现的输入和输出情况。如果在实际使用过程中,遇到状态表中没有定义的情况,则输出不确定值x
。[3]:62使用自定义原语很直观,但是如果输入变量较多,状态表就会变得很复杂。在很多情况中,用户自定义原语并不能被逻辑综合工具转换。[3]:91[22]:200
编程语言接口(Program Language Interface, PLI)提供了通过C语言函数对Verilog数据结构进行存储、读取操作的途径。[3]:9
Verilog编程语言接口的发展先后经过了三代,其中第一代为任务或函数子程序,它可以在C程序和Verilog设计之间传递数据;第二代为存取子程序,它可以在用户自定义C程序和Verilog的内部数据表示的接口上被使用;第三代为Verilog过程接口,它进一步扩展了前两代编程语言接口的功能。[27]:199-200
通过使用编程语言接口,设计人员可以自定义接口的功能,然后通过类似调用系统任务的方式调用这些自定义功能。这样,设计人员可以很大程度地扩展他们能使用的功能,例如监视、激励、调试功能,或者用它来提取设计信息、显示输出等。[27]:184-186
Verilog作为业界使用最广泛的硬件描述语言之一,有大量的电子设计自动化工具对它予以支持。通过使用集成开发环境,设计人员可以在常见的Windows或其他图形化系统中进行设计、仿真、验证,例如Verilog-XL和免费的Icarus Verilog等。[41]
Verilog硬件描述语言(Verilog Hardware Description Language)的英语缩写Verilog HDL和另一种类似的硬件描述语言VHDL在名称上容易混淆,实际上二者是两种不同的硬件描述语言。[42]一些高级的电子设计自动化工具支持用户在项目内同时使用Verilog和VHDL来进行硬件设计。[43]:18
VHDL是由美国国防部主持研发的硬件描述语言,成为了第一个成为IEEE标准的硬件描述语言,美国政府相关的项目都是基于VHDL;而Verilog由民间商业公司的私有产品发展为IEEE标准的,因此在商用领域的市场占有量更大,设计人员和支持资源比VHDL更广。在美国大约有10万设计人员、200所大学教授采用Verilog硬件描述语言。[44]
Verilog和VHDL作为业界广泛认可、同为IEEE标准的硬件描述语言,有着各自的特点。VHDL的设计之初就更加针对标准化进行设计,Verilog则具有简明、高效的代码风格。[16]:4两种语言都能够在多个抽象层次对数字电路建模,并且可以与验证、仿真、综合工具协同工作。其中,Verilog的逻辑门级、开关级电路描述能力更强,VHDL不具备这样低级的描述能力,但是另一方面,VHDL的系统级抽象描述能力则比Verilog强。[12]:255另外,由于Verilog与C语言在语法上有相似之处,因此具有C语言基础的设计人员更容易掌握它,[37]:11而VHDL设计人员需要具有Ada语言编程基础,并且学习周期比Verilog更长。相关学术文献显示,在美国的高级数字系统设计领域,Verilog和VHDL的使用比率大约分别为80%和20%,这项比率在日本和台湾地区和美国相似。[44]随着Verilog-A被合并到Verilog标准之中,而该部分后来成为了Verilog-AMS的一部分,该语言增加了对模拟电子系统的描述能力,因此它在混合信号集成电路中有着更广泛的应用。[5]