case语句是一种多分支选择语句,if语句只有两个分支可供选择,而实际问题中常常需要用到多分支选择,Verilog语言提供的case语句直接处理多分支选择。
case语句通常用于微处理器的指令译码,它的一般形式如下:
1) case(表达式)
2) casez(表达式)
3) casex(表达式)
分支项的一般格式如下:
分支表达式: 语句
缺省项(default项): 语句
说明:
a) case括弧内的表达式称为控制表达式,case分支项中的表达式称为分支表达式。控制表达式通常表示为控制信号的某些位,分支表达式则用这些控制信号的具体状态值来表示,因此分支表达式又可以称为常量表达式。
b) 当控制表达式的值与分支表达式的值相等时,就执行分支表达式后面的语句。如果所有的分支表达式的值都没有与控制表达式的值相匹配的,就执行default后面的语句。
c) default项可有可无,一个case语句里只准有一个default项。下面是一个简单的使用case语句的例子。该例子中对寄存器rega译码以确定result的值。
reg [15:0] rega;
reg [9:0] 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语句的区别主要有两点:
case ( select[1:2] )
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[7:0] ir;
casez(ir)
8 'b1???????: instruction1(ir);
8 'b01??????: instruction2(ir);
8 'b00010???: instruction3(ir);
8 'b000001??: instruction4(ir);
endcase
例4:
reg[7:0] r, mask;
mask = 8'bx0x0x0x0;
casex(r^mask)
8 'b001100xx: stat1;
8 'b1100xx00: stat2;
8 'b00xx0011: stat3;
8 'bxx001100: stat4;
endcase
由于使用条件语句不当在设计中生成了原本没想到有的锁存器
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程序的可读性。
在Verilog HDL中存在着四种类型的循环语句,用来控制执行语句的执行次数。
1) forever 连续的执行语句。
2) repeat 连续执行一条语句 n 次。
3) while 执行一条语句直到某个条件不满足。如果一开始条件即不满足(为假), 则语句一次也不能被执行。
4) for通过以下三个步骤来决定语句的循环执行。
a) 先给控制循环次数的变量赋初值。
b) 判定控制循环的表达式的值,如为假则跳出循环语句,如为真则执行指定的语句后,转到第三步。
c) 执行一条赋值语句来修正控制循环变量次数的变量的值,然后返回第二步。下面对各种循环语句详细的进行介绍。
forever语句
forever语句的格式如下:
forever 语句; 或 forever begin 多条语句 end
forever循环语句常用于产生周期性的波形,用来作为仿真测试信号。它与always语句不同处在于不能独立写在程序中,而必须写在initial块中。
repeat语句
repeat语句的格式如下:
repeat(表达式) 语句;或 repeat(表达式) begin 多条语句 end
在repeat语句中,其表达式通常为常量表达式。下面的例子中使用repeat循环语句及加法和移位操作来实现一个乘法器。
parameter size=8,longsize=16;
reg [size:1] opa, opb;
reg [longsize:1] result;
begin: mult
reg [longsize:1] shift_opa, shift_opb;
shift_opa = opa;
shift_opb = opb;
result = 0;
repeat(size)
begin
if(shift_opb[1])
result = result + shift_opa;
shift_opa = shift_opa <<1;
shift_opb = shift_opb >>1;
end
end
while语句
while语句的格式如下:
while(表达式) 语句 或 while(表达式) begin 多条语句 end
下面举一个while语句的例子,该例子用while循环语句对rega这个八位二进制数中值为1的位进行计数。
begin: count1s
reg[7:0] tempreg;
count=0;
tempreg = rega;
while(tempreg)
begin
if(tempreg[0]) count = count + 1;
tempreg = tempreg>>1;
end
end
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[7:0] tempi;
for(tempi=0;tempi
1 )memory[tempi]=0;
end
例2:
parameter size = 8, longsize = 16;
reg[size:1] opa, opb;
reg[longsize:1] result;
begin:mult
integer bindex;
result=0;
for( bindex=1; bindex<=size; bindex=bindex+1 )
if(opb[bindex])
result = result + (opa<<(bindex-1));
end
在for语句中,循环变量增值表达式可以不必是一般的常规加法或减法表达式。下面是对rega这个八位二进制数中值为1的位进行计数的另一种方法。见下例:
begin: count1s
reg[7:0] tempreg;
count=0;
for( tempreg=rega; tempreg; tempreg=tempreg>>1 )
if(tempreg[0])
count=count+1;
end
Verilog语言中的任何过程模块都从属于以下四种结构的说明语句。
1) initial说明语句
2) always说明语句
3) task说明语句
4) function说明语句
initial和always说明语句在仿真的一开始即开始执行。initial语句只执行一次。相反,always语句则是不断地重复执行,直到仿真过程结束。在一个模块中,使用initial和always语句的次数是不受限制的。
task和function语句可以在程序模块中的一处或多处调用。其具体使用方法以后再详细地加以介绍。这里只对initial和always语句加以介绍。
initial语句
initial语句的格式如下:
initial
begin
语句1;
语句2;
......
语句n;
end
[例1]:
initial
begin
areg=0; //初始化寄存器areg
for(index=0;index
index =index+1)memory[index]=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块常用于测试文件和虚拟模块的编写,用来产生仿真测试信号和设置信号记录等仿真环境。
always语句
always语句在仿真过程中是不断重复执行的。其声明格式如下:
always <时序控制> <语句>
always语句由于其不断重复执行的特性,只有和一定的时序控制结合在一起才有用。如果一个always语句没有时序控制,则这个always语句将会发成一个仿真死锁。见下例:
[例1]:always areg = ~areg;
这个always语句将会生成一个0延迟的无限循环跳变过程,这时会发生仿真死锁。
如果加上时序控制,则这个always语句将变为一条非常有用的描述语句。见下例:
[例2]:always #half_period areg = ~areg;
这个例子生成了一个周期为:period(=2*half_period) 的无限延续的信号波形,常用这种方法来描述时钟信号,作为激励信号来测试所设计的电路。
[例3]:
reg[7:0] 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块,它们都是并行运行的。
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);
下面的例子说明怎样定义任务和调用任务:任务定义:
task my_task;
input a, b;
inout c;
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;
reg clock, red, amber, green;
parameter on=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
//定义交通灯开启时间的任务
task light(color,tics);
output color;
input[31:0] tics;
begin
repeat(tics) @(posedge clock);//等待tics个时钟的上升沿
color=off;//关灯
end
endtask
//产生时钟脉冲的always块
always
begin
end
endmodule
这个例子描述了一个简单的交通灯的时序控制,并且该交通灯有它自己的时钟产生器。
三. function说明语句
函数的目的是返回一个用于表达式的值。
1、定义函数的语法:
function <返回值的类型或范围> (函数名);
<端口说明语句>
<变量类型说明语句>
begin
<语句>
........
end
endfunction
请注意<返回值的类型或范围>这一项是可选项,如缺省则返回值为一位寄存器类型数据。下面用例子说明:
function [7:0] getbyte;
input [15:0] address;
begin
<说明语句> //从地址字中提取低字节的程序
getbyte = result_expression; //把结果赋予函数的返回字节
end
endfunction
2、从函数返回的值
函数的定义蕴含声明了与函数同名的、函数内部的寄存器。如在函数的声明语句中<返回值的类型或范围>为缺省,则这个寄存器是一位的,否则是与函数定义中<返回值的类型或范围>一致的寄存器。函数的定义把函数返回值所赋值寄存器的名称初始化为与函数同名的内部变量。下面的例子说明了这个概念:getbyte被赋予的值就是函数的返回值。y 函数的调用
3、函数的调用
是通过将函数作为表达式中的操作数来实现的。
其调用格式如下:<函数名> (<表达式><,<表达式>>*) 其中函数名作为确认符。下面的例子中通过对两次调用函数getbyte的结果值进行位拼接运算来生成一个字。word = control? {getbyte(msbyte),getbyte(lsbyte)} : 0;
4、函数的使用规则 与任务相比较函数的使用有较多的约束,下面给出的是函数的使用规则:
1) 函数的定义不能包含有任何的时间控制语句,即任何用#、@、或wait来标识的语句。
2) 函数不能启动任务。
3) 定义函数时至少要有一个输入参量。
4) 在函数的定义中必须有一条赋值语句给函数中的一个内部变量赋以函数的结果值,该内部变量具有和函数名相同的名字。
5、举例说明 下面的例子中定义了一个可进行阶乘运算的名为factorial的函数,该函数返回一个32位的寄存器类型的值,该函数可后向调用自身,并且打印出部分结果值。
module tryfact;
//函数的定义
function[31:0]factorial;
input[3:0]operand;
reg[3:0]index;
begin
factorial = operand? 1 : 0;
for(index=2;index<=operand;index=index+1)
factorial = index * factorial;
end
endfunction
//函数的测试
reg[31:0]result;
reg[3:0]n;
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设计数字系统的方法和技术。
Verilog HDL语言中共有以下一些系统函数和任务:$bitstoreal, $rtoi, $display, $setup, $finish, $skew, $hold, $setuphold, $itor, $strobe, $period, $time, $printtimescale, $timefoemat, $realtime, $width, $real tobits, $write, $recovery。
在Verilog HDL语言中每个系统函数和任务前面都用一个标识符$来加以确认。这些系统函数和任务提供了非常强大的功能。有兴趣的同学可以自行查阅资料。
下面对一些常用的系统函数和任务逐一加以介绍。
$display和$write任务
格式:
$display(p1,p2,....pn);
$write(p1,p2,....pn);
这两个函数和系统任务的作用是用来输出信息,即将参数p2到pn按参数p1给定的格式输出。参数p1通常称为“格式控制”,参数p2至pn通常称为“输出表列”。这两个任务的作用基本相同。$display自动地在输出后进行换行,$write则不是这样。如果想在一行里输出多个信息,可以使用$write。在$display和$write中,其输出格式控制是用双引号括起来的字符串,它包括两种信息:
格式说明,由"%"和格式字符组成。它的作用是将输出的数据转换成指定的格式输出。格式说明总是由“%”字符开始的。对于不同类型的数据用不同的格式输出。表一中给出了常用的几种输出格式。如下表一:
普通字符,即需要原样输出的字符。其中一些特殊的字符可以通过表二中的转换序列来输出。下面表中的字符形式用于格式字符串参数中,用来显示特殊的字符。如下表二:
module disp;
initial
begin
$display("\\\t%%\n\"\123");
end
endmodule
module disp;
reg[31:0] 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
module printval;
reg[11:0]r1;
initial
begin
r1=10;
$display("Printing with maximum size=%d=%h",r1,r1);
$display("Printing with minimum size=%0d=%0h",r1,r1);
end
enmodule
如果表达式值的所有位均为不定值,则输出结果为小写的x。
如果表达式值的所有位均为高阻值,则输出结果为小写的z。
如果表达式值的部分位为不定值,则输出结果为大写的X。
如果表达式值的部分位为高阻值,则输出结果为大写的Z。
每4位二进制数为一组代表一位十六进制数,每3位二进制数为一组代表一位八进制数。
如果表达式值相对应的某进制数的所有位均为不定值,则该位进制数的输出的结果为小写的x。
如果表达式值相对应的某进制数的所有位均为高阻值,则该位进制数的输出结果为小写的z。
如果表达式值相对应的某进制数的部分位为不定值,则该位进制数输出结果为大写的X。
如果表达式值相对应的某进制数的部分位为高阻值,则该位进制数输出结果为大写的Z。
注意:因为$write在输出时不换行,要注意它的使用。可以在$write中加入换行符\n,以确保明确的输出显示格式。
系统任务$monitor
格式:
$monitor(p1,p2,.....,pn);
$monitor;
$monitoron;
$monitoroff;
`timescale 10ns/1ns
module test;
reg set;
parameter p=1.6;
initial
begin
$monitor($time,,"set=",set);
#p set=0;
#p set=1;
end
endmodule
`timescale10ns/1ns
module test;
reg set;
parameter p=1.55;
initial
begin
$monitor($realtime,,"set=",set);
#p set=0;
#p set=1;
end
endmodule
initial $readmemh("mem.data",mem);
initial $readmemh("mem.data",mem,16);
initial $readmemh("mem.data",mem,128,1);
`timescale 1ns/1ns
module random_pulse( dout );
output [9:0] 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间变化
#delay1 dout = 1 << ({$random} %10);
//dout的0--9位中随机出现1,并出现的时间在0-100ns间变化
#delay2 dout = 0;
//脉冲的宽度在在20到60ns间变化
end
end
endmodule
END
往期精选
FPGA技术江湖广发江湖帖
无广告纯净模式,给技术交流一片净土,从初学小白到行业精英业界大佬等,从军工领域到民用企业等,从通信、图像处理到人工智能等各个方向应有尽有,QQ微信双选,FPGA技术江湖打造最纯净最专业的技术交流学习平台。
FPGA技术江湖微信交流群
加群主微信,备注姓名+学校/公司+专业/岗位进群
FPGA技术江湖QQ交流群
备注姓名+学校/公司+专业/岗位进群