Verilog HDL是一种硬件描述语言,用于从算法级、门级到开关级的多种抽象设计层次的数字系统建模。Verilog HDL语言不仅定义了语法,而且对每个语法结构都定义了清晰的模拟、仿真语义。因此,用这种语言编写的模型能够使用Verilog仿真器进行验证。语言从C编程语言中继承了多种操作符和结构。
使用Verilog描述硬件的基本设计单元是模块(module)。构建复杂的电子电路,主要是通过模块的相互连接调用来实现的。模块被包含在关键字module、endmodule之内。
例如,四选一的多路选择器,就可以用模块进行描述。它具有两个位选输入信号、四个数据输入,一个输出端,在Verilog中可以表示为:
module mux (out, select, in0, in1, in2, in3);
output out;
input [1:0] select;
input in0, in1, in2, in3;
//具体的寄存器传输级代码
endmodule
实例调用模块时,需要将端口的连接情况按照这个模块声明时的顺序排列。这个顶层模块由于不需要再被外界调用,因此没有输入输出端口:
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
在这个测试平台模块里,设计人员可以设定仿真时的输入信号以及信号监视程序,然后观察仿真时的输出情况是否符合要求,这样就可以了解设计是否达到了预期。
示例中的对模块进行实例引用时,按照原模块声明时的顺序罗列了输入变量。除此之外,还可以使用或者采用命名端口连接的方式。使用这种方式,端口的排列顺序可以与原模块声明时不同,甚至可以不连接某些端口:
mux my_mux (
.out(OUT),
.select(SELECT),
.in0(IN0),
.in1(IN1),
.in2(IN2),
.in3(IN3));//使用命名端口连接,括号外面是模块声明时的端口,括号内是实际的端口连接//括号外相当于C语言的形式参数,括号内相当于实际参数
endmodule
上面所述的情况是,测试平台顶层模块的测试变量直接连接了所设计的功能模块。测试平台还可以是另一种形式,即测试平台并不直接连接所设计的功能模块,而是在这个测试平台之下,将激励模块和功能模块以相同的抽象级别,通过线网相互连接。这两种形式的测试平台都可以完成对功能模块的测试。大型的电路系统,正是由各个层次不同模块之间的连接、调用,来实现复杂的功能的。
Verilog代码中用来定义语言结构名称的字符称为标识符,包括变量名、端口名、模块名等等。标识符可以由字母、数字、下划线以及美元符($)来表示。但是标识符的第一个字符只能是字母、数字或者下划线,不能为美元符,这是因为以美元符开始的标识符和系统任务的保留字冲突。
和其他许多编程语言类似,Verilog也有许多保留字(或称为关键字),用户定义的标识符不能够和保留字相同。Verilog的保留字均为小写。变量类型中的wire、reg、integer等、表示过程的initial、always等,以及所有其他的系统任务、编译指令,都是关键字。可以查阅官方文献以完整的关键字的列表。
上面列出了Verilog采用的具有八种信号强度的四值逻辑(four-valued logic),数字电路中的信号可以用逻辑值、信号强度加以描述。当系统遇到信号之间的竞争时,需要考虑各组信号的状态和强度。如果驱动统一线网的信号强度不同,则输出结果是信号强度高的值;如果两个强度相同的信号之间连接到同一个线网,将会发生竞争,结果为不确定值x。
Verilog所用到的所有变量都属于两个基本的类型:线网类型和寄存器类型。
线网与我们实际使用的电线类似,它的数值一般只能通过连续赋值(continuous assignment),由赋值符右侧连接的驱动源决定。线网在初始化之前的值为x(trireg类型的线网是一个例外,它相当于能够储存电荷的电容器)。如果未连接驱动源,则该线网变量的当前数值为z,即高阻态。线网类型的变量有以下几种:
wire作为一般的电路连线使用最为普遍,而其他几种用于构建总线,即多个驱动源连接到一条线网的情况,或搭建电源、接地等。当进行模块的端口声明时,如果没有明确指出其类型,那么这个端口会被隐含地声明为wire类型。
寄存器与之不同,它可以保存当前的数值,直到另一个数值被赋值给它。在保持当前数值的过程中,不需要驱动源对它进行作用。如果未对寄存器变量赋值,它的初始值则为x。Verilog中所说的寄存器类型变量与真实的硬件寄存器是不同的,它是指一个储存数值的变量。如果要在一个过程(initial过程或always过程)里对变量赋值,这个变量必须是寄存器类型的。寄存器类型的变量有以下几种:
其中reg作为一般的寄存器使用最为普遍。利用寄存器变量的数组,还可以对ROM进行建模。
因此,在声明输出端口时应该注意是否有必要加上reg关键字。以下面的代码片段为例:
module my_moule (out1, out2, in1, in2); //该模块具有两个输出端口
output reg out1; //out1端口被声明为为reg类型,它可以保存当前值
output out2; //out2端口隐含地被声明为为wire类型,它的数值必须依赖连续赋值语句维持
endmodule
关于选择线网类型还是寄存器类型,需要符合一定的规定。在两个模块的信号连接点,提供信号的一方可以是寄存器或者线网,但是接受信号的一方只能是线网。此外,在initial、always过程代码块中赋值的变量必须是寄存器类型的,而连续赋值的对象只能是线网类型的变量。
基本语法结构为<位宽>’<数制的符号><数值>
宽是可选项,如果没有指明位宽,则默认的数据位宽与仿真器有关(最小32位);数制需要用字母来表示,h对应十六进制,d对应十进制,o对应八进制,b对应二进制。如果没有指明数制,则默认数据为十进制数。
如果某个数的最高位为x或z,那么系统会自动使用x或z来填充没有占据的更高位。如果最高位为其他情况,系统会自动使用0来填充没有占据的更高位。
另外,如果需要使用reg表示负数,可以在位宽之前添加一个负号,但是需要注意后面的数值为所需负数的二进制补码。为了防止出错,可以直接使用整数integer或实数real,二者都是带符号数,再利用省略位宽和数制的十进制数来表示负数。
在Verilog中,标量的意思是只具有一个二进制位的变量,而向量表示具有多个二进制位的变量。如果没有特别指明位宽,系统默认它为标量。
在真实的数字电路,例如将两个四位二进制数相加的进位加法器中,我们可以发现,其中一个数是通过四条电线(每条线表示四位中的某一位)连接到加法器上的。我们可以用一个向量来表示这个多位数,分别用这个向量的各个分量来表示“四条电线”,即四位中的某一位。这样做的好处是,可以方便地在Verilog代码的其他地方选择其中的一位(位选)或多位(域选)。当然,如果没有进行位选或域选,则这个多位数整体被选择。
向量的表示需要使用方括号,方括号里的第一个数字为向量第一个分量的序号,第二个数字为向量最后一个分量的序号,中间用冒号隔开。向量分量的序号不像C语言的数组一样必须从0开始,不过为了和数字电路里二进制数高低位的表示方法一致,我们常常让最低位为0(即对于四位二进制数,其最高位为第3位,次高位为第2位,次低位为第1位,最低位为第0位)。例如,上面提到的四位二进制数用向量表示为:
当对向量进行赋值时,如果右边的数值位宽大于左边的变量,则多出来的位被丢弃;如果右边的数值位宽小于左边的变量,则不够的位用0填补。
Verilog中的几种寄存器类型的数据,包括reg、integer、time、real,以及由这几种数据构成的向量,都可以构成数组。声明数组时,方括号位于数组名的后面,括号内的第一个数字为第一个元素的序号,第二个数字为最后一个元素的序号,中间用冒号隔开。如果数组是由向量构成的,则数组的其中某个元素是向量。同样,出于习惯考虑,我们一般让数组第一个元素的序号为0,后面元素的序号依次递增。此外,和C语言类似,用户可以声明多维数组。例如:
上面第三行的例子是65536个8位向量组成的向量数组,它可以描述一个64KB的存储器。
向量与数组的区别:
可以通过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结构可以清晰地描述一个数据选择器。
按位
逻辑
缩减
算术
关系
移位
拼接({,}):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
系统任务可以被用来执行一些系统设计所需的输入、输出、时序检查、仿真控制操作。所有的系统任务名称前都带有美元符号$ 使之与用户定义的任务和函数相区分。
例如,$ display用于显示指定的字符串,然后自动换行(用法类似C语言中的printf函数);$ monitor用于监视变量,一旦被监视的变量发生变化,会显示指定的字符串;而$time可以提取当前的仿真时间。完整的列表请查阅参考工具、Verilog手册或标准文档。
Verilog具有一些编译指令,它们的基本格式为 `< keyword> ,注意第一个符号不是单引号,而是键盘上数字1左边那个键对应的撇号。常用的编译指令有文本宏预定义`define、`include,它们的功能与C语言中类似,分别提供文本替换、文件包含的功能。Verilog还提供了`ifdef、`ifndef等一系列条件编译指令,设计人员可以使得代码在满足一定条件的情况下才进行编译。此外,`timescale指令可以对时间单位进行定义。详细的编译指令清单请参阅相关参考书籍。
一个模块中可以包含多个过程,各个过程相互之间是并发执行的。不过,过程不能够嵌套使用。如果过程中有多个语句,则需要使用关键字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上
延迟时序控制
在代码中使用关键字#和延迟的时间,就可以通过延迟来进行时序控制。延迟的时间可以是数字、变量或者表达式。延迟时序控制又分为两种:常规延迟和内嵌延迟。
常规延迟在赋值语句的左边,系统执行到这一行代码时,系统先进行延迟,延迟完成后,再计算表达式,并将结果赋值给左边的变量;而内嵌延迟在赋值语句的右边,系统执行到这一行代码时,系统先立即计算表达式,再进行延迟,最后把表达式的结果赋值给左边的变量。在上述两种延迟方式中,设计人员需要注意表达式的自变量在延迟过程中可能发生变化。常规延迟是先延迟再计算表达式,这时表达式的自变量可能已经发生了变化;而内嵌延迟在延迟前就已经进行了计算,表达式的自变量在延迟过程中发生的变化,对已经计算的表达式结果没有影响,延迟只是指这个结果需要等待一段时间再赋值给左边的变量。
下面的代码片段分别展示了常规延迟和内嵌延迟:
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和触发符号->定义自己所需要的命名事件触发:
event bigger_than_two;
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)等。所有的晶体管都可以设置延迟属性。设计人员可以利用这些低抽象级元件构建所需要的逻辑门或直接构成其他高级组件。