目录
3.1.1显示和写任务(display and write)
1.任务
任务类似于一段程序,可以使设计者从设计描述的不同位置执行共同的代码段。用任务定义可以将这个共同的代码段编写成任务,从而能够在设计描述的不同位置通过任务名调用该任务。任务可以包含时序控制,即延时,而且任务也可以调用其他任务和函数。
1.1任务的定义
定义任务的格式如下:
tast [automatic] task_id;
[declarations]...
statements...
endtask
任务可以没有参变量或者有一个或多个参变量。通过参变量可以将值传入和传出任务。除输入参变量外(任务接收到的值),任务还能有输出参变量(任务的返回值)和输入/输出(inout)参变量。任务的定义在模块声明部分编写,下面举例说明:
task rotate_left;
inout[15:0] input_array;
input [3:0] start_bit,stop_bit,rotate_by;
reg fill_value;
integer mac1,mac3;
begin
for(mac3=1;mac3<rotate_by;mac3++)
begin
fill_value = input_array[stop_bit];
for(mac1=stop_bit;mac1>start_bit+1;mac1--)
input_array[mac1]=input_array[mac1-1];
input_array[start_bit]=fill_value;
end
end
endtask
fill_value是一个局部变量,只有在任务中才直接可见。除任务参变量外,任务还能够引用任务定义所在模块中声明的任何变量。
任务可以被声明为automatic类型。这样在任务中,任务内部声明的所有局部变量在每次任务调用时都进行动态分配,即在任务调用中的局部变量不会对两个单独或者并发的任务调用产生影响。而在静态(非automatic)任务中,在每次任务调用中的局部变量都使用同一个存储空间。借助关键字automatic就可以把任务指定为automatic类型。
task automatic task_id(
input.....
output....
inout....
);
reg...;
wire...;
...
endtask
举例说明:
task automatic apply_address(
input [3:0] address,
output success
);
reg check_flag;
...;
endtask
1.2任务的调用
任务调用语句是一个过程性语句,可以出现在always或initial语句中,其格式如下:
task_id (expr1,expr2...exprN);
任务调用语句中,参变量列表必须与任务定义中的参变量列表顺序相匹配。此外,参变量是通过值进行传递的,而不是通过标记进行传递的。注意,由于任务能够包含时序控制,所以任务可能要在被调用后再经过一定延时才能返回值。此外,任务调用中的输出和输入/输出参变量必须是变量。如下例所示:
module global_var;
reg [7:0] qram [63:0];
integer index;
reg check_bit;
task get_parity;
input [7:0] address;
output parity_bit;
parity_bit=^qram[address];//qram是模块内部的变量,任务中并没有声明;任务可以引用定义模块内的任何变量
endtask
initial begin
for(index=0;index<63;index++)
begin
get_parity(index,check_bit);
$display("Parity bit of memory word %d is %b.",index,check_bit);
end
end
endmodule
任务可以带有时序控制或者等待某些特定的事件发生。然而,直到任务退出时,赋给输出变量的值才传递给调用的参变量。看下例:
module task_wait;
reg clk_ssp;
task generate_waveform;
output qclock;
begin
qclock=1;
#2 qclock=0;
#2 qclock=1;
#2 qclock=0;
end
endtask
initial begin
generate_waveform(clk_ssp);
end
endmodule
在任务generate_waveform内对qclock的赋值不会出现在clk_ssp上,即没有波形会出现在clk_ssp上;任务返回后只有对qclock的最终赋值0出现在clk_ssp上。为了避免出现这种情况,方法之一是把qclock声明为全局变量,即在任务之外声明它。
下面举例说明automatic类型任务和非automatic类型任务之间的区别:
task automatic check_cnt;
reg [3:0] count;
begin
$display("At beginning of task,count=%b",count);
if(reset)
count = 0;
count = count + 1;
$display("At end of task,count=%b",count);
end
endtask
reg reset;
...
reset=1;
check_cnt();
...
reset=0;
check_cnt();
...
因为check_cnt是一个automatic类型任务,所以对于每次任务调用,都会为reg变量count重新分配单独的存储空间。打印的结果如下:
At beginning of task,count=xxxx
At end of task,count=1
...
At beginning of task,count=xxxx
At end of task,count=xxxx
如果任务不是一个automatic类型任务,打印的结果如下:
At beginning of task,count=xxxx
At end of task,count=1
...
At beginning of task,count=1
At end of task,count=2
由于count是静态的,即不会被破坏,在第2次调用任务check_cnt时会用到前一次调用所产生的count值,因此在第2次调用时,count的初值为1.
2.函数
函数类似于任务,也提供了在模块的不同位置执行共同代码段的能力。函数与任务的不同之处在于,函数只能返回一个值,也不能包含任何延时(必须立即执行),也不能调用其他任务。此外,函数必须至少要有一个输入,不允许有output(输出)和inout(输入/输出)声明语句,可以调用其他函数。
2.1函数的定义
函数的定义可以在模块声明的任何位置出现,定义的格式如下:
function [automatic] [signed]
[range or type] function_id;
input_declaration
other_declaration
statements
endfunction
函数的输入必须用输入声明语句声明。若在函数定义中没有指定函数值得取值范围和类型,则函数默认返回1位二进制数。返回值的类型可以是real,integer,time或者realtime之一。通过关键词signed可以把返回值声明为带符号值。看下面例子:
module function_example;
parameter MAXBITS=8;
function [MAXBITS-1:0] reverse_bits;
input [MAXBITS-1:0] data_in;
integer k;
begin
for(k=0;k<MAXBITS;k++)
reverse_bits[MAXBITS-k]=data_in[k];
end
endfunction
...;
endmodule
函数名为reverse_bits.函数返回一个长度为MAXBITS的向量。该函数有一个输入,k是局部整型变量。函数的定义隐含声明了一个函数内部的reg类型变量,该变量与函数同名,且取值范围相同。函数通过在函数定义中明确对该寄存器赋值来返回函数值。因此,对这一寄存器的赋值必须出现在函数声明中。
通过关键字automatic可以把函数声明为automatic类型函数,例如:
function parity;
..
endfunction
在automatic函数中,每次函数调用都会给局部变量分配新的存储空间。在非automatic类型函数中,局部变量是静态的,即对于每次调用,函数的局部变量都使用同一个存储空间。automatic类型函数支持编写递归函数(因为每次调用都会分配属于这次调用的单独存储空间) 。下面是利用递归函数计算阶乘的示例:
function automatic [31:0] factorial;
input [31:0] fac_of;
factorial=(fac_of == 1)? 1:factorial(fac_of - 1)*fac_of;
endfunction
描述函数的参变量还有另外一种描述方式,如下所示:
function automatic [31:0] factorial(
input [31:0] fac_of);
factorial=(fac_of == 1)? 1:factorial(fac_of - 1)*fac_of;
endfunction
2.2函数的调用
函数的调用时表达式的一部分,其格式如下:
func_id(expr1,expr2...exprN);
下面举个例子说明函数调用:
reg [MAXBITS-1:0] remap_reg,rmp_rev;//变量声明
rmp_rev=reverse_bits(remap_reg);//函数调用在等号右侧的表达式内
函数内的参数值也可以由定义参数defparam语句来修改,如下所示:
module test;
time current_time;
function watch(
input time ctime
);
parameter RESOLUTION=60;//函数内部参数
...
endfunction
defparam watch.RESOLUTION=3600;
assign print_time=watch(current_time);
endmodule
2.3常数函数
常数函数是指仿真开始之前在细化期间计算出结果为常数的函数。常数函数内不允许访问全局变量或者调用系统函数,但是可以调用另一个常数函数。
3.系统函数和系统任务
verilogHDL提供了内建的系统任务和系统函数,可以把系统任务和系统函数分为以下几类:
(1)显示任务
(2)文件输入/输出任务
(3)时间标度任务
(4)仿真控制任务
(5)PLA建模任务
(6)随机建模任务
(7)变换函数
(8)概率分布函数
(9)字符格式化
(10)命令行参数
3.1显示任务
显示任务用于显示和打印信息,这些任务进一步被分为:
(1)显示和写任务;
(2)选通的监控任务;
(3)连续的监控任务;
3.1.1显示和写任务(display and write)
语法格式如下:
task_name(format_specification1,argument_list1,
format_specification2,argument_list2,
...,
format_specificationN,argument_listN);
task_name可以是$display或$write;display是将指定信息以及行结束符打印到标准输出设备;write是将指定信息打印到标准输出设备时不带行结束符; 下列转义序列能够用于格式定义:
%h或%H:十六进制 %d或%D:十进制 %o或%O:八进制 %b或%B:二进制 %c或%C:ASCII字符
%s或%S:字符串 %t或%T:当前时间格式 %v或%V 线网信号强度
用如下转义序列可以打印特殊字符:
\n 换行
\t 制表符
\\ 字符\
\" 字符"
%% 字符%
例如:
$display("simulation time is %t",$time);
$display($time,"r=%b,s=%d,q=%o,qb=%h",r,s,q,qb);
//由于没有指定时间的格式,所以时间按十进制显示
//显示任务语句可以有多个参变量,还可以分为多行
$display("at time %t,",$time,
"the value is %d.",tube_rst);
$write("simulation time is");
$write("%t\n",$time);
//下面显示的是执行上述语句时的结果
simulation time is 10//可以看出display会自动换行,write则不会
10 r=0,s=1,q=7,qb=a
at time 10,the value is 10.
simulation time is 10
3.1.2选通任务 strobe
在指定时刻显示仿真数据,但这种任务的执行是在当前时阶结束时刻才显示仿真数据。“当前时阶结束时”意味着在指定时阶内的所有事件都已经处理完毕的时刻。
选通任务与显示任务的不同之处在于:显示任务在执行到该语句时立刻执行,而选通任务的执行则要推迟到当前时阶结束时执行。看下面的例子:
integer wait_timer;
initial begin
wait_timer=1;
$display("after first assignment,","wait_timer has value %d",wait_timer);
$strobe("when strobe is executed,","wait_timer has value %d",wait_timer);
wait_timer=2;
$display("after second assignment,","wait_timer has value %d",wait_timer);
end
//产生的输出为:
after first assignment,wait_timer has value 1
after second assignment,wait_timer has value 2
when strobe is executed,wait_timer has value 2
第一个display打印出的值为1来自第一次赋值,第二个 display打印出的值为2来自第二次赋值,strobe打印出的值为2,是最终wait_timer的值;
3.1.3 监控任务monitor
连续监控指定的参变量,只要参变量列表中的参变量值发生变化,就在时阶结束时打印整个参变量列表。例如:
initial
$monitor("at %t,d=%d,clk=%d",$time,d,clk,"and q is %b",q);
监视任务的格式定义与显示任务相同,但是在任意时刻只能有一个监视任务处于激活状态。
可以通过下面两个系统任务启动和关闭监控:$monitoroff和$monitoron
3.2文件输入输出任务
3.2.1文件的打开和关闭
系统函数$fopen可用于打开一个文件。如下例:
integer file_pointer=$fopen(file_name,mode);
系统函数$fclose可用于关闭文件,如下例:
$fclose(file_pointer);
模式mode可以是下面的一种:
(1)"r":打开文件并从文件的头开始读。如果文件不存在就会报错。
(2)"w":打开文件并从文件的头开始写。如果文件不存在就会创建文件。
(3)"a":打开文件并从文件的末尾开始写。如果文件不存在就会创建文件。
(4)"r+"、"r+b"、"rb+":打开文件并从文件的头开始读写。如果文件不存在就会报错。
(5)"w+"、"w+b"、"wb+":打开文件并从文件的头开始读写。如果文件不存在就会创建文件。
(6)"a+"、"a+b"、"ab+":打开文件并从文件的末尾开始读写。如果文件不存在就会创建文件。
“b”是指用二进制的方式打开文件。下面举例:
integer tq_file;//定义句柄
initial
begin
tq_file=$fopen("../../div.tq","r+");
...
$fclose(tq_file);
end
3.2.2输出到文件
显示、写入、选通和监控等系统函数都有一个相应副本,该副本可以用于将相应的信息写入文件。这些系统任务如下:
$fdisplay
$fwrite
$fstrobe
$fmonitor
$fflush
所有这些任务的第一个参变量是一个文件的指针。任务的其余参变量是格式定义的列表,格式定义列表后是相应的参变量列表。下面举例:
integer vec_file;
initial
begin
vec_file=$fopen("div.vec","w");
...
$fdisplay(vec_file,"the simulation time is %t",$time);
//第一个参变量vec_file是文件指针
$fclose(vec_file);
end
在上面的任务$fdisplay执行时,下面的语句将出现在文件div.vec中。
the simulation time is 0
3.2.3从文件中读取数据
有两个系统任务能够从文件中读取存储数据,这些任务从文本文件中读取数据并将数据加载到存储器中:
$readmemb
$readmemh
看下面例子:
reg [3:0] rx_mem[63:0];
initial $readmemb("data.txt",rx_mem);
//读取的每个数据被分配到从0到63的存储器单元中
//在系统任务调用中也可以明确指示地址
$readmemb("data.txt",rx_mem,15,30);//将文件中读取的第一个数据存储到地址15中,后面依次类推
3.3时间标度任务
系统任务:$printtimescale
显示指定模块的时间单位和时间精度。若任务没有指定任何参变量,则用于显示该任务调用所在模块的时间单位和时间精度。如果指定了模块的层次路径作为参变量,则此系统任务用于显示指定模块的时间单位和时间精度。
常用格式:
$printtimescale;
//未指定参变量
$printtimescale(hier_path_to_module);
//指定模块的路径
系统任务:$timeformat
指定了%t格式定义如何报告时间信息。该任务的格式如下:
$timeformat(units_number,precision,suffix,numeric_field_width);
其中unit_number为:
0 :1s
1 :100ms
...
-9 :1ns
例子如下:
$timeformat(-12,3,"ps",5);
$display("current simulation time is %t",$time);
如果没有指定$timeformat,则%t按照源代码中所有时间标度的最小精度显示。
3.4仿真控制任务
系统任务:$finish 使仿真器退出仿真环境,并将控制权返回操作系统。
系统任务:$stop 使得仿真器被挂起,但该阶段任可以发送交互命令给仿真器。下面举例说明:
initial
#500 $stop;//500单位时间后,仿真终止
3.5仿真时间函数
下面系统函数返回仿真时间:
(1)$time 按照所在模块的时间单位刻度,返回64位的整型仿真时间;
(2)$stime 返回32位的仿真时间
(3)$realtime 按照所在模块的时间单位刻度,返回实型仿真时间;
initial
$monitor("at %t,d=%d,clk=%d",$time,d,clk,"and q is %b",q);
3.6转换函数
$rtoi(real_value) 通过小数位截断将实型变为整型
$itor(integer_value) 将整型数据变为实数
$signed(value)和$unsigned(value) 有符号数与无符号数的转换
3.7概率分布函数
函数: $random[seed] 根据种子变量(seed)的值返回一个32位有符号的整型随机数。种子变量(必须是reg型、整型或时间类型变量)控制返回的函数值,即不同的种子生成不同的随机数 。若未指定种子变量,则根据默认种子变量的值生成随机数。下面举例:
integer seed,random_num;
wire rclk;
initial
seed=12;
always
@(rclk) random_num=$random(seed);//在每个rclk的沿上,随机函数被调用并返回32位有符号整型数据
//如果想在一定范围内取数,比如在-10~10之间取数,则可以使用求模操作符来生成:
random_num=$random(seed)%11;
{$random} % 11;//生成0~10之间的随机数,拼接操作符将有符号数变为了无符号数
3.8字符串格式化
(1)$swrite
与$fwrite类似,不同之处在于任务$swrite是写入到字符串(字符串是reg型变量),而$fwrite是写入到文件中。看下面例子:
integer index;
reg [1023:0] name_buf;
$swrite(name_buf,"rx[%d]",index);
//如果index=6,那么执行后name_buf中就有字符串“rx[6]”
(2)$sformat
与$swrite类似,但是$swrite的格式定义必须指定为它的第2个参变量。
$sformat(string_buffer,"time=%t,fifo_empty=%d",$time,fifo_empty);
(3)$sscanf
从字符串中读取一行,并把读取的数据存放到指定的参变量中。看下例:
integer status;
status=$sscanf(string_buffer,"%s %d %d",signal_name,lower_bnd,upper_bnd);
//若string_buffer中有字符串“a 1 2”,则后面的三个变量分别存放a 1 2
//若在读取字符串时出错,则$sscanf会返回一个0状态
4.禁止语句
禁止语句是过程性语句(因此它只能出现在always或initial语句块内)。禁止语句能够在任务或者程序块执行完它的所有语句之前终止其执行,能够用于对硬件中断和全局复位的建模。其格式如下:
disable task_id;
disable block_id;
//在执行禁止语句后,继续执行被禁止的任务调用或程序块的下一条语句
建议在任务定义中最好不要使用disable禁止语句,尤其是当任务返回一些输出值时更是如此。这是因为当任务被禁止时,在verilog语言中规定output和inout变量值是不确定的。一种比较稳妥的办法是:如果在任务中有顺序块的话,在任务总终止顺序块的执行。
5.命名事件
分析下面两个alwasys块语句:
reg ready.done;
//开始两个always语句之间的交互
initial
begin
done=0;
#0 done=1;
end
always
@(done) begin
...
//完成处理这个always语句
//触发下一个always语句
//在信号ready创建一个事件
ready=0;
#0 ready=1;
end
always
@(ready) begin
...
//完成处理这个always语句
//创建事件触发前一个的always语句
done=0;
#0 done=1;
end
两个always块中的两条赋值语句用来在ready和done上创建一个事件,这表明ready和done的用途是作为两个语句块之间的握手信号。
verilog提供一种替代机制来实现这一功能,即使用命名事件。命名事件是verilog的另一种数据类型,在使用前必须声明,其格式如下:
event ready,done;
声明了命名事件后,可以使用事件触发语句创建事件,其格式如下:
->ready;
->done;
命名事件上的事件如同变量上的事件一样能够被监控,即使用@机制,例如:
@done <do_something>
一旦done事件触发语句被执行,若在done上发生一个事件,其后面的do_something语句开始执行。下面用例子说明如何用命名事件重新编写上面的语句:
event ready,done;
initial
->done;
always
@(done) begin
...
//触发下一个always语句
//在信号ready创建一个事件
->ready;
end
always
@(ready) begin
...
//创建事件触发前一个的always语句
->done;
end