fpga_feixiang 发表于 2017-12-11 09:20:15

Verilog HDL的基本语法

case语句



case语句是一种多分支选择语句,if语句只有两个分支可供选择,而实际问题中常常需要用到多分支选择,Verilog语言提供的case语句直接处理多分支选择。case语句通常用于微处理器的指令译码,它的一般形式如下:



1)   case(表达式)      <case分支项>    endcase

2)   casez(表达式) <case分支项>    endcase

3)   casex(表达式) <case分支项>    endcase



case分支项的一般格式如下:

分支表达式:   语句

缺省项(default项):语句

说明:

a)   case括弧内的表达式称为控制表达式,case分支项中的表达式称为分支表达式。控制表达式通常表示为控制信号的某些位,分支表达式则用这些控制信号的具体状态值来表示,因此分支表达式又可以称为常量表达式。

b)   当控制表达式的值与分支表达式的值相等时,就执行分支表达式后面的语句。如果所有的分支表达式的值都没有与控制表达式的值相匹配的,就执行default后面的语句。

c)   default项可有可无,一个case语句里只准有一个default项。下面是一个简单的使用case语句的例子。该例子中对寄存器rega译码以确定result的值。



reg rega;

reg    result;

case(rega)

16 'd0:result = 10 'b0111111111;

16 'd1:result = 10 'b1011111111;

16 'd2:result = 10 'b1101111111;

16 'd3:result = 10 'b1110111111;

16 'd4:result = 10 'b1111011111;

16 'd5:result = 10 'b1111101111;

16 'd6:result = 10 'b1111110111;

16 'd7:result = 10 'b1111111011;

16 'd8:result = 10 'b1111111101;

16 'd9:result = 10 'b1111111110;

default:result = 'bx;

endcase



d)   每一个case分项的分支表达式的值必须互不相同,否则就会出现矛盾现象(对表达式的同一个值,有多种执行方案)。

e)   执行完case分项后的语句,则跳出该case语句结构,终止case语句的执行。

f)   在用case语句表达式进行比较的过程中,只有当信号的对应位的值能明确进行比较时,比较才能成功。因此要注意详细说明case分项的分支表达式的值。

g)   case语句的所有表达式的值的位宽必须相等,只有这样控制表达式和分支表达式才能进行对应位的比较。一个经常犯的错误是用'bx, 'bz 来替代 n'bx, n'bz,这样写是不对的,因为信号x, z的缺省宽度是机器的字节宽度,通常是32位(此处 n 是case控制表达式的位宽)。








下面将给出 case, casez, casex 的真值表:

case语句与if_else_if语句的区别主要有两点:



1)   与case语句中的控制表达式和多分支表达式这种比较结构相比,if_else_if结构中的条件表达式更为直观一些。

2)   对于那些分支表达式中存在不定值x和高阻值z位时,case语句提供了处理这种情况的手段。下面的两个例子介绍了处理x,z值位的case语句。

[例1]:

case ( select )

2 'b00:result = 0;

2 'b01:result = flaga;

2 'b0x,

2 'b0z:result = flaga? 'bx : 0;

2 'b10:result = flagb;

2 'bx0,

2 'bz0:result = flagb? 'bx : 0;

default: result = 'bx;

endcase



[例2]:

case(sig)

1 'bz:    $display("signal is floating");

1 'bx:    $display("signal is unknown");

default:$display("signal is %b", sig);

endcase



Verilog HDL针对电路的特性提供了case语句的其它两种形式用来处理case语句比较过程中的不必考虑的情况( don't care condition )。其中casez语句用来处理不考虑高阻值z的比较过程,casex语句则将高阻值z和不定值都视为不必关心的情况。所谓不必关心的情况,即在表达式进行比较时,不将该位的状态考虑在内。这样在case语句表达式进行比较时,就可以灵活地设置以对信号的某些位进行比较。见下面的两个例子:



[例3]: reg ir;

casez(ir)

8 'b1???????: instruction1(ir);

8 'b01??????: instruction2(ir);

8 'b00010???: instruction3(ir);

8 'b000001??: instruction4(ir);

endcase



[例4]: reg r, mask;

mask = 8'bx0x0x0x0;

casex(r^mask)

8 'b001100xx: stat1;

8 'b1100xx00: stat2;

8 'b00xx0011: stat3;

8 'bxx001100: stat4;

endcase





3.5.3.由于使用条件语句不当在设计中生成了原本没想到有的锁存器



Verilog HDL设计中容易犯的一个通病是由于不正确使用语言,生成了并不想要的锁存器。下面我们给出了一个在“always"块中不正确使用if语句,造成这种错误的例子。








检查一下左边的"always"块,if语句保证了只有当al=1时,q才取d的值。这段程序没有写出 al = 0 时的结果, 那么当al=0时会怎么样呢?



在"always"块内,如果在给定的条件下变量没有赋值,这个变量将保持原值,也就是说会生成一个锁存器!



如果设计人员希望当 al = 0 时q的值为0,else项就必不可少了,请注意看右边的"always"块,整个Verilog程序模块综合出来后,"always"块对应的部分不会生成锁存器。



Verilog HDL程序另一种偶然生成锁存器是在使用case语句时缺少default项的情况下发生的。



case语句的功能是:在某个信号(本例中的sel)取不同的值时,给另一个信号(本例中的q)赋不同的值。注意看下图左边的例子,如果sel=0,q取a值,而sel=11,q取b的值。这个例子中不清楚的是:如果sel取00和11以外的值时q将被赋予什么值?在下面左边的这个例子中,程序是用Verilog HDL写的,即默认为q保持原值,这就会自动生成锁存器。






右边的例子很明确,程序中的case语句有default项,指明了如果sel不取00或11时,编译器或仿真器应赋给q的值。程序所示情况下,q赋为0,因此不需要锁存器。



以上就是怎样来避免偶然生成锁存器的错误。如果用到if语句,最好写上else项。如果用case语句,最好写上default项。遵循上面两条原则,就可以避免发生这种错误,使设计者更加明确设计目标,同时也增强了Verilog程序的可读性。





3.6.循环语句



在Verilog HDL中存在着四种类型的循环语句,用来控制执行语句的执行次数。



1)forever    连续的执行语句。

2)repeat 连续执行一条语句 n 次。

3)while执行一条语句直到某个条件不满足。如果一开始条件即不满足(为假),

            则语句一次也不能被执行。

4)for通过以下三个步骤来决定语句的循环执行。

a)   先给控制循环次数的变量赋初值。

b)   判定控制循环的表达式的值,如为假则跳出循环语句,如为真则执行指定的语句后,转到第三步。

c)   执行一条赋值语句来修正控制循环变量次数的变量的值,然后返回第二步。



下面对各种循环语句详细的进行介绍。





3.6.1.forever语句

forever语句的格式如下:



forever语句;或

foreverbegin   多条语句 end



forever循环语句常用于产生周期性的波形,用来作为仿真测试信号。它与always语句不同处在于不能独立写在程序中,而必须写在initial块中。其具体使用方法将在"事件控制"这一小节里详细地加以说明。





3.6.2.repeat语句

repeat语句的格式如下:



repeat(表达式) 语句; 或

repeat(表达式)   begin 多条语句 end



在repeat语句中,其表达式通常为常量表达式。下面的例子中使用repeat循环语句及加法和移位操作来实现一个乘法器。



parameter size=8,longsize=16;

reg opa, opb;

reg result;



begin: mult

reg shift_opa, shift_opb;

shift_opa = opa;

shift_opb = opb;

result = 0;

repeat(size)

begin

if(shift_opb)

result = result + shift_opa;



shift_opa = shift_opa <<1;

shift_opb = shift_opb >>1;

end

end





3.6.3.while语句

while语句的格式如下:



while(表达式)语句

或用如下格式:

while(表达式)begin多条语句   end



下面举一个while语句的例子,该例子用while循环语句对rega这个八位二进制数中值为1的位进行计数。

begin:count1s

reg tempreg;

count=0;

tempreg = rega;

while(tempreg)

begin

if(tempreg)count = count + 1;

tempreg = tempreg>>1;

end

end





3.6.4.for语句

for语句的一般形式为:



for(表达式1;表达式2;表达式3)语句



它的执行过程如下:

1)   先求解表达式1;

2)   求解表达式2,若其值为真(非0),则执行for语句中指定的内嵌语句,然后执行下面的第3步。若为假(0),则结束循环,转到第5步。

3)   若表达式为真,在执行指定的语句后,求解表达式3。

4)   转回上面的第2步骤继续执行。

5)   执行for语句下面的语句。

for语句最简单的应用形式是很易理解的,其形式如下:

for(循环变量赋初值;循环结束条件;循环变量增值)

执行语句

for循环语句实际上相当于采用while循环语句建立以下的循环结构:

begin

循环变量赋初值;

while(循环结束条件)

begin

执行语句

循环变量增值;

end

end

这样对于需要8条语句才能完成的一个循环控制,for循环语句只需两条即可。



下面分别举两个使用for循环语句的例子。例1用for语句来初始化memory。例2则用for循环语句来实现前面用repeat语句实现的乘法器。



[例1]:begin:   init_mem

reg tempi;

for(tempi=0;tempi<memsize;tempi=tempi+1)

memory=0;

end



[例2]:parametersize = 8, longsize = 16;

reg opa, opb;

reg result;



begin:mult

integer bindex;

result=0;

for( bindex=1; bindex<=size; bindex=bindex+1 )

if(opb)

result = result + (opa<<(bindex-1));

end



在for语句中,循环变量增值表达式可以不必是一般的常规加法或减法表达式。下面是对rega这个八位二进制数中值为1的位进行计数的另一种方法。见下例:



begin: count1s

reg tempreg;

count=0;

for( tempreg=rega; tempreg; tempreg=tempreg>>1 )

if(tempreg)

count=count+1;

end







3.7.结构说明语句

Verilog语言中的任何过程模块都从属于以下四种结构的说明语句。



1)   initial说明语句

2)   always说明语句

3)   task说明语句

4)   function说明语句



initial和always说明语句在仿真的一开始即开始执行。initial语句只执行一次。相反,always语句则是不断地重复执行,直到仿真过程结束。在一个模块中,使用initial和always语句的次数是不受限制的。task和function语句可以在程序模块中的一处或多处调用。其具体使用方法以后再详细地加以介绍。这里只对initial和always语句加以介绍。



3.7.1.initial语句

initial语句的格式如下:



initial

begin

语句1;

语句2;

......

语句n;

end



举例说明:

[例1]:

initial

begin

areg=0;   //初始化寄存器areg

for(index=0;index<size;index=index+1)

memory=0;//初始化一个memory

end

在这个例子中用initial语句在仿真开始时对各变量进行初始化。



[例2]:

initial

begin

inputs = 'b000000;//初始时刻为0

#10 inputs = 'b011001;

#10 inputs = 'b011011;

#10 inputs = 'b011000;

#10 inputs = 'b001000;

end



从这个例子中,我们可以看到initial语句的另一用途,即用initial语句来生成激励波形作为电路的测试仿真信号。一个模块中可以有多个initial块,它们都是并行运行的。initial块常用于测试文件和虚拟模块的编写,用来产生仿真测试信号和设置信号记录等仿真环境。





3.7.2.always语句



always语句在仿真过程中是不断重复执行的。

其声明格式如下:



always <时序控制><语句>



always语句由于其不断重复执行的特性,只有和一定的时序控制结合在一起才有用。如果一个always语句没有时序控制,则这个always语句将会发成一个仿真死锁。见下例:



[例1]:alwaysareg = ~areg;



这个always语句将会生成一个0延迟的无限循环跳变过程,这时会发生仿真死锁。如果加上时序控制,则这个always语句将变为一条非常有用的描述语句。见下例:



[例2]:always #half_periodareg = ~areg;



这个例子生成了一个周期为:period(=2*half_period) 的无限延续的信号波形,常用这种方法来描述时钟信号,作为激励信号来测试所设计的电路。



[例3]:reg counter;

       reg tick;

       always @(posedge areg)

begin

tick = ~tick;

counter = counter + 1;

end



这个例子中,每当areg信号的上升沿出现时把tick信号反相,并且把counter增加1。这种时间控制是always语句最常用的。



always 的时间控制可以是沿触发也可以是电平触发的,可以单个信号也可以多个信号,中间需要用关键字 or 连接,如:



   always @(posedge clock or posedge reset)//由两个沿触发的always块

      begin

      ……

      end

         

   always @( a or b or c )               //由多个电平触发的always块

            begin

             ……

            end



沿触发的always块常常描述时序逻辑,如果符合可综合风格要求可用综合工具自动转换为表示时序逻辑的寄存器组和门级逻辑,而电平触发的always块常常用来描述组合逻辑和带锁存器的组合逻辑,如果符合可综合风格要求可转换为表示组合逻辑的门级逻辑或带锁存器的组合逻辑。一个模块中可以有多个always块,它们都是并行运行的。





3.7.3.task和function说明语句



task和function说明语句分别用来定义任务和函数。利用任务和函数可以把一个很大的程序模块分解成许多较小的任务和函数便于理解和调试。输入、输出和总线信号的值可以传入、传出任务和函数。任务和函数往往还是大的程序模块中在不同地点多次用到的相同的程序段。学会使用task和function语句可以简化程序的结构,使程序明白易懂,是编写较大型模块的基本功。



一.task和function说明语句的不同点



任务和函数有些不同,主要的不同有以下四点:



1)函数只能与主模块共用同一个仿真时间单位,而任务可以定义自己的仿真时间单位。

2)函数不能启动任务,而任务能启动其它任务和函数。

3)函数至少要有一个输入变量,而任务可以没有或有多个任何类型的变量。

4)函数返回一个值,而任务则不返回值。



函数的目的是通过返回一个值来响应输入信号的值。任务却能支持多种目的,能计算多个结果值,这些结果值只能通过被调用的任务的输出或总线端口送出。Verilog HDL模块使用函数时是把它当作表达式中的操作符,这个操作的结果值就是这个函数的返回值。下面让我们用例子来说明:



例如,定义一任务或函数对一个16位的字进行操作让高字节与低字节互换,把它变为另一个字(假定这个任务或函数名为: switch_bytes)。



任务返回的新字是通过输出端口的变量,因此16位字字节互换任务的调用源码是这样的:



switch_bytes(old_word,new_word);



任务switch_bytes把输入old_word的字的高、低字节互换放入new_word端口输出。



而函数返回的新字是通过函数本身的返回值,因此16位字字节互换函数的调用源码是这样的:



new_word = switch_bytes(old_word);



下面分两节分别介绍任务和函数语句的要点。



二. task说明语句



如果传给任务的变量值和任务完成后接收结果的变量已定义,就可以用一条语句启动任务。任务完成以后控制就传回启动过程。如任务内部有定时控制,则启动的时间可以与控制返回的时间不同。任务可以启动其它的任务,其它任务又可以启动别的任务,可以启动的任务数是没有限制的。不管有多少任务启动,只有当所有的启动任务完成以后,控制才能返回。



1) 任务的定义

定义任务的语法如下:

任务:

task <任务名>;

<端口及数据类型声明语句>

<语句1>

<语句2>

.....

<语句n>

endtask

这些声明语句的语法与模块定义中的对应声明语句的语法是一致的。

2) 任务的调用及变量的传递

启动任务并传递输入输出变量的声明语句的语法如下:

任务的调用:

<任务名>(端口1,端口2,...,端口n);



下面的例子说明怎样定义任务和调用任务:

任务定义:

taskmy_task;

input a, b;

inoutc;

output d, e;



<语句>   //执行任务工作相应的语句



c = foo1;    //赋初始值

d = foo2;    //对任务的输出变量赋值t

e = foo3;

endtask



任务调用:

my_task(v,w,x,y,z);



任务调用变量(v,w,x,y,z)和任务定义的I/O变量(a,b,c,d,e)之间是一一对应的。当任务启动时,由v,w,和x.传入的变量赋给了a,b,和c,而当任务完成后的输出又通过c,d和e赋给了x,y和z。下面是一个具体的例子用来说明怎样在模块的设计中使用任务,使程序容易读懂:



module traffic_lights;

regclock, red, amber, green;

parameteron=1, off=0, red_tics=350,

amber_tics=30,green_tics=200;

//交通灯初始化

initial    red=off;

initial    amber=off;

initial    green=off;

//交通灯控制时序

always

begin

red=on;   //开红灯

light(red,red_tics);    //调用等待任务

green=on;       //开绿灯

light(green,green_tics);    //等待

amber=on;       //开黄灯

light(amber,amber_tics);    //等待

end

//定义交通灯开启时间的任务

tasklight(color,tics);

outputcolor;

input tics;

begin

repeat(tics) @(posedge clock);//等待tics个时钟的上升沿

color=off;//关灯

end

endtask

//产生时钟脉冲的always块

always

begin

#100 clock=0;

#100 clock=1;

end

endmodule

这个例子描述了一个简单的交通灯的时序控制,并且该交通灯有它自己的时钟产生器。





二.function说明语句



函数的目的是返回一个用于表达式的值。



?定义函数的语法:

function <返回值的类型或范围> (函数名);

<端口说明语句>

<变量类型说明语句>

begin

<语句>

........

end

endfunction



请注意<返回值的类型或范围>这一项是可选项,如缺省则返回值为一位寄存器类型数据。下面用例子说明:

function getbyte;

input address;

begin

<说明语句>       //从地址字中提取低字节的程序

getbyte = result_expression; //把结果赋予函数的返回字节

end

endfunction



?从函数返回的值

函数的定义蕴含声明了与函数同名的、函数内部的寄存器。如在函数的声明语句中<返回值的类型或范围>为缺省,则这个寄存器是一位的,否则是与函数定义中<返回值的类型或范围>一致的寄存器。函数的定义把函数返回值所赋值寄存器的名称初始化为与函数同名的内部变量。下面的例子说明了这个概念:getbyte被赋予的值就是函数的返回值。



?函数的调用

函数的调用是通过将函数作为表达式中的操作数来实现的。

其调用格式如下:

<函数名> (<表达式><,<表达式>>*)

其中函数名作为确认符。下面的例子中通过对两次调用函数getbyte的结果值进行位拼接运算来生成一个字。

word = control? {getbyte(msbyte),getbyte(lsbyte)} : 0;



?函数的使用规则

与任务相比较函数的使用有较多的约束,下面给出的是函数的使用规则:

1)   函数的定义不能包含有任何的时间控制语句,即任何用#、@、或wait来标识的语句。

2)   函数不能启动任务。

3)   定义函数时至少要有一个输入参量。

4)   在函数的定义中必须有一条赋值语句给函数中的一个内部变量赋以函数的结果值,该内部变量具有和函数名相同的名字。



?举例说明

下面的例子中定义了一个可进行阶乘运算的名为factorial的函数,该函数返回一个32位的寄存器类型的值,该函数可后向调用自身,并且打印出部分结果值。

moduletryfact;

//函数的定义-------------------------------

functionfactorial;

inputoperand;

regindex;

begin

factorial = operand? 1 : 0;

for(index=2;index<=operand;index=index+1)

factorial = index * factorial;

end

endfunction

//函数的测试-------------------------------------

regresult;

regn;

initial

begin

result=1;

for(n=2;n<=9;n=n+1)

begin

$display("Partial result n= %d result= %d", n, result);

result = n * factorial(n)/((n*2)+1);

end

$display("Finalresult=%d",result);

end

endmodule//模块结束



前面我们已经介绍了足够的语句类型可以编写一些完整的模块。在下一章里,我们将举许多实际的例子进行介绍。这些例子都给出了完整的模块描述,因此可以对它们进行仿真测试和结果检验。通过学习和练习我们就能逐步掌握利用Verilog HDL设计数字系统的方法和技术。







3.8.系统函数和任务

Verilog HDL语言中共有以下一些系统函数和任务:

$bitstoreal, $rtoi, $display, $setup, $finish, $skew, $hold,

$setuphold, $itor, $strobe, $period, $time, $printtimescale,

$timefoemat, $realtime, $width, $real tobits, $write, $recovery,

在Verilog HDL语言中每个系统函数和任务前面都用一个标识符$来加以确认。这些系统函数和任务提供了非常强大的功能。有兴趣的同学可以参阅附录:Verilog语言参考手册。下面对一些常用的系统函数和任务逐一加以介绍。



3.8.1.$display和$write任务

格式:



$display(p1,p2,....pn);

$write(p1,p2,....pn);



这两个函数和系统任务的作用是用来输出信息,即将参数p2到pn按参数p1给定的格式输出。参数p1通常称为“格式控制”,参数p2至pn通常称为“输出表列”。这两个任务的作用基本相同。$display自动地在输出后进行换行,$write则不是这样。如果想在一行里输出多个信息,可以使用$write。在$display和$write中,其输出格式控制是用双引号括起来的字符串,它包括两种信息:



·         格式说明,由"%"和格式字符组成。它的作用是将输出的数据转换成指定的格式输出。格式说明总是由“%”字符开始的。对于不同类型的数据用不同的格式输出。表一中给出了常用的几种输出格式。

表一



输出格式



说明

%h或%H

以十六进制数的形式输出

%d或%D

以十进制数的形式输出

%o或%O

以八进制数的形式输出

%b或%B

以二进制数的形式输出

%c或%C

以ASCII码字符的形式输出

%v或%V

输出网络型数据信号强度

%m或%M

输出等级层次的名字

%s或%S

以字符串的形式输出

%t或%T

以当前的时间格式输出

%e或%E

以指数的形式输出实型数

%f或%F

以十进制数的形式输出实型数

%g或%G

以指数或十进制数的形式输出实型数

无论何种格式都以较短的结果输出







·         普通字符,即需要原样输出的字符。其中一些特殊的字符可以通过表二中的转换序列来输出。下面表中的字符形式用于格式字符串参数中,用来显示特殊的字符。











表二:

换码序列

功能

\n

换行

\t

横向跳格(即跳到下一个输出区)

\\

反斜杠字符\

\"

双引号字符"

\o

1到3位八进制数代表的字符

%%

百分符号%



在$display和$write的参数列表中,其“输出表列”是需要输出的一些数据,可以是表达式。下面举几个例子说明一下。



[例1]:moduledisp;

initial

begin

$display("\\\t%%\n\"\123");

end

endmodule



输出结果为

\%

"S

从上面的这个例子中可以看到一些特殊字符的输出形式(八进制数123就是字符S)。



[例2]:module disp;

reg rval;

pulldown(pd);

initial

begin

rval=101;

$display("rval=%h hex %d decimal", rval, rval);

$display("rval=%o otal %b binary", rval, rval);

$display("rval has %c ascii character value",rval);

$display("pd strength value is %v",pd);

$display("current scope is %m");

$display("%s is ascii value for 101",101);

$display("simulation time is %t",$time);

end

endmodule



其输出结果为:

rval=00000065 hex 101 decimal

rval=00000000145 octal 00000000000000000000000001100101 binary

rval has e ascii character value

pd strength value is StX

current scope is disp

e is ascii value for 101

simulation time is 0



输出数据的显示宽度



在$display中,输出列表中数据的显示宽度是自动按照输出格式进行调整的。这样在显示输出数据时,在经过格式转换以后,总是用表达式的最大可能值所占的位数来显示表达式的当前值。在用十进制数格式输出时,输出结果前面的0值用空格来代替。对于其它进制,输出结果前面的0仍然显示出来。例如对于一个值的位宽为12位的表达式,如按照十六进制数输出,则输出结果占3个字符的位置,如按照十进制数输出,则输出结果占4个字符的位置。这是因为这个表达式的最大可能值为FFF(十六进制)、4095(十进制)。可以通过在%和表示进制的字符中间插入一个0自动调整显示输出数据宽度的方式。见下例:



$display("d=%0h a=%0h",data,addr);



这样在显示输出数据时,在经过格式转换以后,总是用最少的位数来显示表达式的当前值。下面举例说明:



[例3]:module printval;

regr1;

initial

begin

    r1=10;

    $display("Printing with maximum size=%d=%h",r1,r1);

    $display("Printing with minimum size=%0d=%0h",r1,r1);

end

enmodule

输出结果为:

Printing with maximum size=10=00a:

printing with minimum size=10=a;



如果输出列表中表达式的值包含有不确定的值或高阻值,其结果输出遵循以下规则:

(1).在输出格式为十进制的情况下:

·                                             如果表达式值的所有位均为不定值,则输出结果为小写的x。

·                                             如果表达式值的所有位均为高阻值,则输出结果为小写的z。

·                                             如果表达式值的部分位为不定值,则输出结果为大写的X。

·                                             如果表达式值的部分位为高阻值,则输出结果为大写的Z。



(2).在输出格式为十六进制和八进制的情况下:

·         每4位二进制数为一组代表一位十六进制数,每3位二进制数为一组代表一位八进制数。

·         如果表达式值相对应的某进制数的所有位均为不定值,则该位进制数的输出的结果为小写的x。

·         如果表达式值相对应的某进制数的所有位均为高阻值,则该位进制数的输出结果为小写的z。

·         如果表达式值相对应的某进制数的部分位为不定值,则该位进制数输出结果为大写的X。

·         如果表达式值相对应的某进制数的部分位为高阻值,则该位进制数输出结果为大写的Z。



对于二进制输出格式,表达式值的每一位的输出结果为0、1、x、z。下面举例说明:

语句输出结果:

$display("%d", 1'bx);                                 输出结果为:x

$display("%h", 14'bx0_1010);                              输出结果为:xxXa

$display("%h %o",12'b001x_xx10_1x01,12'b001_xxx_101_x01);      输出结果为:XXX 1x5X

注意:因为$write在输出时不换行,要注意它的使用。可以在$write中加入换行符\n,以确保明确的输出显示格式。





3.8.2.系统任务$monitor



格式:

$monitor(p1,p2,.....,pn);

$monitor;

$monitoron;

$monitoroff;



任务$monitor提供了监控和输出参数列表中的表达式或变量值的功能。其参数列表中输出控制格式字符串和输出表列的规则和$display中的一样。当启动一个带有一个或多个参数的$monitor任务时,仿真器则建立一个处理机制,使得每当参数列表中变量或表达式的值发生变化时,整个参数列表中变量或表达式的值都将输出显示。如果同一时刻,两个或多个参数的值发生变化,则在该时刻只输出显示一次。但在$monitor中,参数可以是$time系统函数。这样参数列表中变量或表达式的值同时发生变化的时刻可以通过标明同一时刻的多行输出来显示。如:   

$monitor($time,,"rxd=%b txd=%b",rxd,txd);

在$display中也可以这样使用。注意在上面的语句中,“,,"代表一个空参数。空参数在输出时显示为空格。

$monitoron和$monitoroff任务的作用是通过打开和关闭监控标志来控制监控任务$monitor的启动和停止,这样使得程序员可以很容易的控制$monitor何时发生。其中$monitoroff任务用于关闭监控标志,停止监控任务$monitor,$monitoron则用于打开监控标志,启动监控任务$monitor。通常在通过调用$monitoron启动$monitor时,不管$monitor参数列表中的值是否发生变化,总是立刻输出显示当前时刻参数列表中的值,这用于在监控的初始时刻设定初始比较值。在缺省情况下,控制标志在仿真的起始时刻就已经打开了。在多模块调试的情况下,许多模块中都调用了$monitor,因为任何时刻只能有一个$monitor起作用,因此需配合$monitoron与$monitoroff使用,把需要监视的模块用$monitoron打开,在监视完毕后及时用$monitoroff关闭,以便把$monitor 让给其他模块使用。$monitor与$display的不同处还在于$monitor往往在initial块中调用,只要不调用$monitoroff,$monitor便不间断地对所设定的信号进行监视。





3.8.3.时间度量系统函数$time



在Verilog HDL中有两种类型的时间系统函数:$time和$realtime。用这两个时间系统函数可以得到当前的仿真时刻。



·         系统函数$time



$time可以返回一个64比特的整数来表示的当前仿真时刻值。该时刻是以模块的仿真时间尺度为基准的。下面举例说明。



[例1]:`timescale10ns/1ns

moduletest;

regset;

parameterp=1.6;

initial

begin

$monitor($time,,"set=",set);

#p set=0;

#p set=1;

end

endmodule



输出结果为:

0 set=x

2 set=0

3 set=1



在这个例子中,模块test想在时刻为16ns时设置寄存器set为0,在时刻为32ns时设置寄存器set为1。但是由$time记录的set变化时刻却和预想的不一样。这是由下面两个原因引起的:



1)   $time显示时刻受时间尺度比例的影响。在上面的例子中,时间尺度是10ns,因为$time输出的时刻总是时间尺度的倍数,这样将16ns和32ns输出为1.6和3.2。

2)   因为$time总是输出整数,所以在将经过尺度比例变换的数字输出时,要先进行取整。在上面的例子中,1.6和3.2经取整后为2和3输出。注意:时间的精确度并不影响数字的取整。



·      $realtime系统函数

$realtime和$time的作用是一样的,只是$realtime返回的时间数字是一个实型数,该数字也是以时间尺度为基准的。下面举例说明:



[例2]: `timescale10ns/1ns

module test;

reg set;

parameterp=1.55;

initial

begin

$monitor($realtime,,"set=",set);

#p set=0;

#p set=1;

end

endmodule



输出结果为:

0 set=x

1.6 set=0

3.2 set=1



从上面的例子可以看出,$realtime将仿真时刻经过尺度变换以后即输出,不需进行取整操作。所以$realtime返回的时刻是实型数。





3.8.4.系统任务$finish

格式:

$finish;

$finish(n);



系统任务$finish的作用是退出仿真器,返回主操作系统,也就是结束仿真过程。任务$finish可以带参数,根据参数的值输出不同的特征信息。如果不带参数,默认$finish的参数值为1。下面给出了对于不同的参数值,系统输出的特征信息:



0不输出任何信息

1输出当前仿真时刻和位置

2输出当前仿真时刻,位置和在仿真过程中

所用memory及CPU时间的统计





3.8.5.系统任务$stop

格式:

$stop;

$stop(n);



$stop任务的作用是把EDA工具(例如仿真器)置成暂停模式,在仿真环境下给出一个交互式的命令提示符,将控制权交给用户。这个任务可以带有参数表达式。根据参数值(0,1或2)的不同,输出不同的信息。参数值越大,输出的信息越多。





3.8.6.系统任务$readmemb和$readmemh



在Verilog HDL程序中有两个系统任务$readmemb和$readmemh用来从文件中读取数据到存贮器中。这两个系统任务可以在仿真的任何时刻被执行使用,其使用格式共有以下六种:



      1)$readmemb("<数据文件名>",<存贮器名>);

      2)$readmemb("<数据文件名>",<存贮器名>,<起始地址>);

      3)$readmemb("<数据文件名>",<存贮器名>,<起始地址>,<结束地址>);

      4)$readmemh("<数据文件名>",<存贮器名>);

      5)$readmemh("<数据文件名>",<存贮器名>,<起始地址>);

      6)$readmemh("<数据文件名>",<存贮器名>,<起始地址>,<结束地址>);



在这两个系统任务中,被读取的数据文件的内容只能包含:空白位置(空格,换行,制表格(tab)和form-feeds),注释行(//形式的和/*...*/形式的都允许),二进制或十六进制的数字。数字中不能包含位宽说明和格式说明,对于$readmemb系统任务,每个数字必须是二进制数字,对于$readmemh系统任务,每个数字必须是十六进制数字。数字中不定值x或X,高阻值z或Z,和下划线(_)的使用方法及代表的意义与一般Verilog HDL程序中的用法及意义是一样的。另外数字必须用空白位置或注释行来分隔开。



在下面的讨论中,地址一词指对存贮器(memory)建模的数组的寻址指针。当数据文件被读取时,每一个被读取的数字都被存放到地址连续的存贮器单元中去。存贮器单元的存放地址范围由系统任务声明语句中的起始地址和结束地址来说明,每个数据的存放地址在数据文件中进行说明。当地址出现在数据文件中,其格式为字符“@”后跟上十六进制数。如:



@hh...h



对于这个十六进制的地址数中,允许大写和小写的数字。在字符“@”和数字之间不允许存在空白位置。可以在数据文件里出现多个地址。当系统任务遇到一个地址说明时,系统任务将该地址后的数据存放到存贮器中相应的地址单元中去。



对于上面六种系统任务格式,需补充说明以下五点:



1)   如果系统任务声明语句中和数据文件里都没有进行地址说明,则缺省的存放起始地址为该存贮器定义语句中的起始地址。数据文件里的数据被连续存放到该存贮器中,直到该存贮器单元存满为止或数据文件里的数据存完。

2)   如果系统任务中说明了存放的起始地址,没有说明存放的结束地址,则数据从起始地址开始存放,存放到该存贮器定义语句中的结束地址为止。

3)   如果在系统任务声明语句中,起始地址和结束地址都进行了说明,则数据文件里的数据按该起始地址开始存放到存贮器单元中,直到该结束地址,而不考虑该存贮器的定义语句中的起始地址和结束地址。

4)   如果地址信息在系统任务和数据文件里都进行了说明,那么数据文件里的地址必须在系统任务中地址参数声明的范围之内。否则将提示错误信息,并且装载数据到存贮器中的操作被中断。

5)   如果数据文件里的数据个数和系统任务中起始地址及结束地址暗示的数据个数不同的话,也要提示错误信息。



下面举例说明:

先定义一个有256个地址的字节存贮器 mem:



reg mem;



下面给出的系统任务以各自不同的方式装载数据到存贮器mem中。



initial$readmemh("mem.data",mem);

initial$readmemh("mem.data",mem,16);

initial$readmemh("mem.data",mem,128,1);



第一条语句在仿真时刻为0时,将装载数据到以地址是1的存贮器单元为起始存放单元的存贮器中去。第二条语句将装载数据到以单元地址是16的存贮器单元为起始存放单元的存贮器中去,一直到地址是256的单元为止。第三条语句将从地址是128的单元开始装载数据,一直到地址为1的单元。在第三种情况中,当装载完毕,系统要检查在数据文件里是否有128个数据,如果没有,系统将提示错误信息。





3.8.7.系统任务 $random



这个系统函数提供了一个产生随机数的手段。当函数被调用时返回一个32bit的随机数。它是一个带符号的整形数。



$random一般的用法是:$ramdom % b ,其中 b>0.它给出了一个范围在(-b+1):(b-1)中的随机数。下面给出一个产生随机数的例子:



reg rand;

rand = $random % 60;



上面的例子给出了一个范围在-59到59之间的随机数,下面的例子通过位并接操作产生一个值在0到59之间的数。



reg rand;

rand = {$random} % 60;



利用这个系统函数可以产生随机脉冲序列或宽度随机的脉冲序列,以用于电路的测试。下面例子中的Verilog HDL模块可以产生宽度随机的随机脉冲序列的测试信号源,在电路模块的设计仿真时非常有用。同学们可以根据测试的需要,模仿下例,灵活使用$random系统函数编制出与实际情况类似的随机脉冲序列。



[例] `timescale 1ns/1ns

module random_pulse( dout );

output dout;

reg dout;

integer delay1,delay2,k;

initial

begin

#10 dout=0;

for (k=0; k< 100; k=k+1)

begin

delay1 = 20 * ( {$random} % 6);

// delay1 在0到100ns间变化

delay2 = 20 * ( 1 + {$random} % 3);

// delay2 在20到60ns间变化

#delay1dout = 1 << ({$random} %10);

//dout的0--9位中随机出现1,并出现的时间在0-100ns间变化

#delay2dout = 0;

//脉冲的宽度在在20到60ns间变化

end

end

endmodule







3.9.编译预处理



Verilog HDL语言和C语言一样也提供了编译预处理的功能。“编译预处理”是Verilog HDL编译系统的一个组成部分。Verilog HDL语言允许在程序中使用几种特殊的命令(它们不是一般的语句)。Verilog HDL编译系统通常先对这些特殊的命令进行“预处理”,然后将预处理的结果和源程序一起在进行通常的编译处理。



在Verilog HDL语言中,为了和一般的语句相区别,这些预处理命令以符号“ `”开头(注意这个符号是不同于单引号“ '”的)。这些预处理命令的有效作用范围为定义命令之后到本文件结束或到其它命令定义替代该命令之处。Verilog HDL提供了以下预编译命令:



`accelerate,`autoexpand_vectornets,`celldefine,`default_nettype,`define,`else,`endcelldefine,`endif,`endprotect,`endprotected,`expand_vectornets,`ifdef,`include,`noaccelerate,`noexpand_vectornets,`noremove_gatenames,`noremove_netnames,`nounconnected_drive,`protect,`protecte,`remove_gatenames,`remove_netnames,`reset,`timescale,`unconnected_drive



在这一小节里只对常用的`define、`include、`timescale进行介绍,其余的请查阅参考书。





3.9.1.宏定义 `define



用一个指定的标识符(即名字)来代表一个字符串,它的一般形式为:



`define 标识符(宏名) 字符串(宏内容)



如:`define signal string



它的作用是指定用标识符signal来代替string这个字符串,在编译预处理时,把程序中在该命令以后所有的signal都替换成string。这种方法使用户能以一个简单的名字代替一个长的字符串,也可以用一个有含义的名字来代替没有含义的数字和符号,因此把这个标识符(名字)称为“宏名”,在编译预处理时将宏名替换成字符串的过程称为“宏展开”。`define是宏定义命令。



[例1]:`defineWORDSIZE 8

module

regdata; //这相当于定义 reg data;



关于宏定义的八点说明:



1)   宏名可以用大写字母表示,也可以用小写字母表示。建议使用大写字母,以与变量名相区别。

2)   `define命令可以出现在模块定义里面,也可以出现在模块定义外面。宏名的有效范围为定义命令之后到原文件结束。通常,`define命令写在模块定义的外面,作为程序的一部分,在此程序内有效。

3)在引用已定义的宏名时,必须在宏名的前面加上符号“`”,表示该名字是一个经过宏定义的名字。

4)   使用宏名代替一个字符串,可以减少程序中重复书写某些字符串的工作量。而且记住一个宏名要比记住一个无规律的字符串容易,这样在读程序时能立即知道它的含义,当需要改变某一个变量时,可以只改变 `define命令行,一改全改。如例1中,先定义WORDSIZE代表常量8,这时寄存器data是一个8位的寄存器。如果需要改变寄存器的大小,只需把该命令行改为:`defineWORDSIZE 16。这样寄存器data则变为一个16位的寄存器。由此可见使用宏定义,可以提高程序的可移植性和可读性。

5)   宏定义是用宏名代替一个字符串,也就是作简单的置换,不作语法检查。预处理时照样代入,不管含义是否正确。只有在编译已被宏展开后的源程序时才报错。

6)   宏定义不是Verilog HDL语句,不必在行末加分号。如果加了分号会连分号一起进行置换。如:

[例2]:moduletest;

rega, b, c, d, e, out;

`defineexpressiona+b+c+d;

assign out = `expression + e;

...

    endmodule



经过宏展开以后,该语句为:

assignout = a+b+c+d;+e;

显然出现语法错误。

7)   在进行宏定义时,可以引用已定义的宏名,可以层层置换。如:

[例3]:module test;

rega, b, c;

wire out;

`define aa a + b

`define cc c + `aa

assign out = `cc;

    endmodule

这样经过宏展开以后,assign语句为

assignout = c + a + b;

8)   宏名和宏内容必须在同一行中进行声明。如果在宏内容中包含有注释行,注释行不会作为被置换的内容。如:



[例4]: module

      `define typ_nand nand #5//define a nand with typical delay

   `typ_nand g121(q21,n10,n11);

      ………

      endmodule



经过宏展开以后,该语句为:



nand #5 g121(q21,n10,n11);



宏内容可以是空格,在这种情况下,宏内容被定义为空的。当引用这个宏名时,不会有内容被置换。



注意:组成宏内容的字符串不能够被以下的语句记号分隔开的。

·         注释行

·         数字

·         字符串

·         确认符

·         关键词

·         双目和三目字符运算符



如下面的宏定义声明和引用是非法的。

`definefirst_half"start of string

$display(`first_half end of string");



注意在使用宏定义时要注意以下情况:



1)   对于某些 EDA软件,在编写源程序时,如使用和预处理命令名相同的宏名会发生冲突,因此建议不要使用和预处理命令名相同的宏名。

2)   宏名可以是普通的标识符(变量名)。例如signal_name 和 'signal_name的意义是不同的。但是这样容易引起混淆,建议不要这样使用。





3.9.2.“文件包含”处理`include



所谓“文件包含”处理是一个源文件可以将另外一个源文件的全部内容包含进来,即将另外的文件包含到本文件之中。Verilog HDL语言提供了`include命令用来实现“文件包含”的操作。其一般形式为:

`include “文件名”



   

图3-9-2表示“文件包含”的含意。图3-9-2(a)为文件File1.v,它有一个`include "File2.v"命令,然后还有其它的内容(以A表示)。图3-9-2(b)为另一个文件File2.v,文件的内容以B表示。在编译预处理时,要对`include命令进行“文件包含”预处理:将File2.v的全部内容复制插入到 `include "File2.v"命令出现的地方,即File2.v 被包含到File1.v中,得到图3-9-2(c)所示的结果。在接着往下进行的编译中,将“包含”以后的File1.v作为一个源文件单位进行编译。



“文件包含”命令是很有用的,它可以节省程序设计人员的重复劳动。可以将一些常用的宏定义命令或任务(task)组成一个文件,然后用`include命令将这些宏定义包含到自己所写的源文件中,相当于工业上的标准元件拿来使用。另外在编写Verilog HDL源文件时,一个源文件可能经常要用到另外几个源文件中的模块,遇到这种情况即可用`include命令将所需模块的源文件包含进来。



[例1]:

(1)文件aaa.v

module aaa(a,b,out);

input a, b;

output out;

wire out;

assignout = a^b;

endmodule



(2)文件 bbb.v

`include"aaa.v"

modulebbb(c,d,e,out);

inputc,d,e;

outputout;

wireout_a;

wireout;

         aaaaaa(.a(c),.b(d),.out(out_a));

assignout=e&out_a;

endmodule



在上面的例子中,文件bbb.v用到了文件aaa.v中的模块aaa的实例器件,通过“文件包含”处理来调用。模块aaa实际上是作为模块bbb的子模块来被调用的。在经过编译预处理后,文件bbb.v实际相当于下面的程序文件bbb.v:



module aaa(a,b,out);

input a, b;

outputout;

wire out;

assignout = a ^ b;

endmodule



module bbb( c, d, e, out);

input c, d, e;

output out;

wire out_a;

wire out;

         aaaaaa(.a(c),.b(d),.out(out_a));

assign out= e & out_a;

endmodule



关于“文件包含”处理的四点说明:



1)   一个`include命令只能指定一个被包含的文件,如果要包含n个文件,要用n个`include命令。注意下面的写法是非法的`include"aaa.v""bbb.v"

2)   `include命令可以出现在Verilog HDL源程序的任何地方,被包含文件名可以是相对路径名,也可以是绝对路径名。例如:'include"parts/count.v"

3)   可以将多个`include命令写在一行,在`include命令行,只可以出空格和注释行。例如下面的写法是合法的。

'include "fileB" 'include "fileC" //including fileB and fileC

4)   如果文件1包含文件2,而文件2要用到文件3的内容,则可以在文件1用两个`include命令分别包含文件2和文件3,而且文件3应出现在文件2之前。例如在下面的例子中,即在file1.v中定义:



`include"file3.v"

`include"file2.v"



module test(a,b,out);

input a, b;

output out;

wire out;

assignout= a+b;

endmodule



file2.v的内容为:

`define size2`size1+1

.

.

.



file3.v的内容为:

`define size14

.

.

.



这样,file1.v和file2.v都可以用到file3.v的内容。在file2.v中不必再用 `include    "file3.v"了。



5)   在一个被包含文件中又可以包含另一个被包含文件,即文件包含是可以嵌套的。例如上面的问题也可以这样处理,见图3-9-3.



它的作用和图3-9-4的作用是相同的。







3.9.3.时间尺度 `timescale



`timescale命令用来说明跟在该命令后的模块的时间单位和时间精度。使用`timescale命令可以在同一个设计里包含采用了不同的时间单位的模块。例如,一个设计中包含了两个模块,其中一个模块的时间延迟单位为ns,另一个模块的时间延迟单位为ps。EDA工具仍然可以对这个设计进行仿真测试。



`timescale 命令的格式如下:



`timescale<时间单位>/<时间精度>



在这条命令中,时间单位参量是用来定义模块中仿真时间和延迟时间的基准单位的。时间精度参量是用来声明该模块的仿真时间的精确程度的,该参量被用来对延迟时间值进行取整操作(仿真前),因此该参量又可以被称为取整精度。如果在同一个程序设计里,存在多个`timescale命令,则用最小的时间精度值来决定仿真的时间单位。另外时间精度至少要和时间单位一样精确,时间精度值不能大于时间单位值。



在`timescale命令中,用于说明时间单位和时间精度参量值的数字必须是整数,其有效数字为1、10、100,单位为秒(s)、毫秒(ms)、微秒(us)、纳秒(ns)、皮秒(ps)、毫皮秒(fs)。这几种单位的意义说明见下表。







时间单位

定义

s

秒(1S)

ms

千分之一秒(10-3S)

us

百万分之一秒(10-6S)

ns

十亿分之一秒(10-9S)

ps

万亿分之一秒(10-12S)

fs

千万亿分之一秒(10-15S)



下面举例说明`timescale命令的用法。



[例1]: `timescale1ns/1ps

在这个命令之后,模块中所有的时间值都表示是1ns的整数倍。这是因为在`timescale命令中,定义了时间单位是1ns。模块中的延迟时间可表达为带三位小数的实型数,因为 `timescale命令定义时间精度为1ps.



[例2]: `timescale 10us/100ns

在这个例子中,`timescale命令定义后,模块中时间值均为10us的整数倍。因为`timesacle 命令定义的时间单位是10us。延迟时间的最小分辨度为十分之一微秒(100ns),即延迟时间可表达为带一位小数的实型数。



例3: `timescale 10ns/1ns

moduletest;

regset;

parameterd=1.55;

initial

begin

#d set=0;

#d set=1;

end

endmodule



在这个例子中,`timescale命令定义了模块test的时间单位为10ns、时间精度为1ns。因此在模块test中,所有的时间值应为10ns的整数倍,且以1ns为时间精度。这样经过取整操作,存在参数d中的延迟时间实际是16ns(即1.6×10ns),这意味着在仿真时刻为16ns时寄存器set被赋值0,在仿真时刻为32ns时寄存器set被赋值1。仿真时刻值是按照以下的步骤来计算的。



1)   根据时间精度,参数d值被从1.55取整为1.6。

2)   因为时间单位是10ns,时间精度是1ns,所以延迟时间#d作为

时间单位的整数倍为16ns。

3)   EDA工具预定在仿真时刻为16ns的时候给寄存器set赋值0

(即语句 #d set=0;执行时刻),在仿真时刻为32ns的时候给

寄存器set赋值1(即语句 #d set=1;执行时刻),



注意:如果在同一个设计里,多个模块中用到的时间单位不同,需要用到以下的时间结构。

1)   用`timescale命令来声明本模块中所用到的时间单位和时间精度。

2)   用系统任务$printtimescale来输出显示一个模块的时间单位和时间精度。

3)   用系统函数$time和$realtime及%t格式声明来输出显示EDA工具记录的时间信息。





3.9.4.条件编译命令`ifdef、`else、`endif



一般情况下,Verilog HDL源程序中所有的行都将参加编译。但是有时希望对其中的一部分内容只有在满足条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足条件时对一组语句进行编译,而当条件不满足是则编译另一部分。



条件编译命令有以下几种形式:



1)   `ifdef 宏名 (标识符)

程序段1

`else

程序段2

`endif

它的作用是当宏名已经被定义过(用`define命令定义),则对程序段1进行编译,程序段2将被忽略;否则编译程序段2,程序段1被忽略。其中`else部分可以没有,即:



2)   `ifdef 宏名 (标识符)

程序段1

`endif

这里的 “宏名” 是一个Verilog HDL的标识符,“程序段”可以是Verilog HDL语句组,也可以是命令行。这些命令可以出现在源程序的任何地方。注意:被忽略掉不进行编译的程序段部分也要符合Verilog HDL程序的语法规则。

通常在Verilog HDL程序中用到`ifdef、`else、`endif编译命令的情况有以下几种:

·         选择一个模块的不同代表部分。

·         选择不同的时序或结构信息。

·         对不同的EDA工具,选择不同的激励。



3.10.小结

Verilog HDL的语法与C语言的语法有许多类似的地方,但也有许多不同的地方。我们学习Verilog HDL语法要善于找到不同点,着重理解如:阻塞〔Blocking〕和非阻塞〔Non-Blocking〕赋值的不同;顺序块和并行块的不同;块与块之间的并行执行的概念;task和function的概念。Verilog HDL还有许多系统函数和任务也是C语言中没有的如:$monitor、$readmemb、$stop等等,而这些系统任务在调试模块的设计中是非常有用的,我们只有通过阅读大量的Verilog调试模块实例,经过长期的实践,经常查阅附录中的Verilog语言参考手册才能逐步掌握,。

zxopenljx 发表于 2025-2-15 17:59:34

Verilog HDL的基本语法
页: [1]
查看完整版本: Verilog HDL的基本语法