阅读《IEEE Standard for Verilog 2005》时,做一些整理和记录。
任务和函数也属于过程块,多用于仿真文件设计中,使用两者的目的有所区别:
函数(function):对输入的值执行一些处理,返回一个新的值。因此至少有一个input类型的参数,不能有inout或output类型的参数。函数在一个仿真时间单位内执行完毕,因此不能包含任务、不能使用非阻塞赋值。使用上都是把函数作为表达式中的一个操作数。
任务(task):其实作用与module差不多,只是能在过程块中调用,实现的功能比函数更加广泛。任务可以包含时序控制语句,也可以调用其它任务和函数;可以使用任意类型的参数,也可以没有参数。
比如,将一个数据按位取反后得到一个新的数据,将这个功能用任务的形式实现,调用时形式如下:
switch_bytess(old_word, new_word);将这个功能用函数的形式实现,调用时形式如下:
new_word = switch_bytess(old_word);接下来分别介绍任务和函数的一些用法,再给出Verilog支持的系统任务和系统函数。
函数用作表达式中的一个操作数。一个函数的声明架构如下([ ]中的内容表示可选):
function [automatic] [范围或类型] 函数名 (端口列表); ... endfunction // example function [7:0] getbyte(input [15:0] address); begin ... getbyte = ...; end endfunction函数的默认返回值时一个标量,可以使用“[范围或类型]”将返回值设置为real、integer、time、realtime或矢量。
定义函数时,函数内部会隐式地定义一个和函数名相同的数据。函数内部可以直接使用这个数据,这个数据的值便是函数的返回值。
一个任务的声明架构如下([ ]中的内容表示可选):
task [automatic] 任务名 (端口列表); ... endtask // example task my_task (input a, b, inout c, output d, e); begin ... c = ...; d = ...; e = ...; end endtask任务的定义和module差不多,只不过端口列表中的参数可以是任意数据类型。
本系列第10篇中介绍过“命名块+disable”的用法,disable对任务同样适用。使用“disable+任务名”可以提前终止任务的执行。
系统任务和系统函数是Verilog标准的一部分,都以字符"$"为开头。系统任务可划分为六类,下面分别给出一些常用任务的用法。VCD(value change dump)相关的系统任务不在这里罗列。
显示任务又可以分为三类。
这类任务用于显示信息,包括 d i s p l a y 、 display、 display、displayb、 d i s p l a y o 、 displayo、 displayo、displayh和 w r i t e 、 write、 write、writeb、 w r i t e o 、 writeo、 writeo、writeh两组。这两组的唯一区别在于:$display会自动在末尾加上换行符"\n",$write任务不会。
下表给出打印信息中支持的转义字符:
参数功能\n换行符\t制表符(TAB)\\显示 \ 符号\"显示 " 符号\ddd以八进制的方式指定显示的字符%%显示 % 符号\、"、%三个符号再字符串中都有特殊含义,因此需要采用转义字符的方式打印。"\ddd"比较少见,比如P的ASCII码为(80)10,对应(120)8,则字符串中的"\120"会显示"P"。
如果任务的表达式参数没有对应的格式控制,$display / $write的默认格式为十进制、$displayb / $writeb的默认格式为二进制、$displayo / $writeo的默认格式为八进制、$displayh / $writeh的默认格式为十进制。如下面代码:
reg [7:0] cnt = 50; initial begin $display("cnt: ", cnt); $displayh("cnt: ", cnt); $displayo("cnt: ", cnt); $displayb("cnt: ", cnt); end运行仿真时显示的信息为:
cnt: 50 cnt: 32 cnt: 062 cnt: 00110010如果上面代码中都加上格式控制"%d",则四个任务等效,显示的都是十进制的50。下表给出支持的格式控制(参数中的字母不区分大小写),大部分和C语言相同:
参数功能%h16进制格式%d10进制格式%o8进制格式%b2进制格式%cASCII字符格式%s字符串格式%e以指数形式显示实数%f以小数形式显示实数%g以最简短的方式显示实数%t时间格式%l显示库绑定信息%v显示网络信号强度%m显示层次结构名称%u无格式2值数据%z无格式4值数据1.向终端或文件写入值时,系统会自动决定表达式参数值的位置大小。比如一个16bit大小的数,用十六进制需要4个字符宽度(最大FFFF),用十进制需要5个字符宽度(最大65535)。如果在%和radix间加一个0,可以取消这种自动决定的机制,看下面的示例:
reg [15:0] data = 200; initial $display("data: %d.", data); // data: 200. 数据占5个字符 initial $display("data: %0d.", data); // data: 200. 数据占3个字符2.实数可以用%f、%e、%g三种格式显示,下面给出示例:
real num = 12000000; initial begin $display("%e", num); // 1.200000e+07 $display("%f", num); // 12000000.000000 $display("%g", num); // 1.2e+07 end real num = 12.345; initial begin $display("%e", num); // 1.234500e+01 $display("%f", num); // 12.345000 $display("%g", num); // 12.345 end3.%t通常与5.1中的仿真时间函数配合使用,时间显示的格式用4.3中的$timeformat任务设置。
4.%v用于显示标量网络的强度,由于FPGA设计中不会涉及到这方面,前面讲网络数据类型时也没有谈网络强度,这里省略。
5.%m用于显示调用此系统任务的module、任务、函数、命名块的层次结构名称,不需要传入参数。当有多个实例调用同样的 d i s p l a y / display/ display/write任务时,使用%m可以起到区分的作用。示例如下:
`timescale 1ns / 1ps module sim(); initial begin : block1 $display("Hierarchical name is : %m"); end // 打印信息如下 Hierarchical name is : sim.block16.%L用于显示指定模块的库信息,如下:
$display("Library Information is : %l"); // 打印信息如下 Library Information is : xil_defaultlib.sim打印的库信息与Vivado中的设置相符:
7.%u(2值)和%z(4值)更多用在$fwrite任务中,将指定数据的二进制表示形式传给输出流,区别在于%u会将x和z视为0,%z会保留z和x的值。外部程序有的可能支持x和z,有的不支持x和z,因此采用不同的方式来传输数据。
这组系统任务$strobe、$strobeb、$strobeo、$strobeh可以在选择的时间点处显示仿真数据。当前仿真时刻的其它所有语句、事件执行完后,$strobe系统任务才会执行,以确保显示的是正确的数据。示例如下:
reg [15:0] cnt = 0; always @ (posedge clk) cnt = $random; initial begin forever @(posedge clk) $strobe("At time %d, data is %d", $time, cnt); end终端打印的信息如下:
At time 10, data is 13604 At time 30, data is 24193 At time 50, data is 54793 At time 70, data is 22115 ......$strobe使用的参数(包括所有转义字符、格式控制)和$display完全相同。
$monitor使用的参数(包括所有转义字符、格式控制)和$display完全相同。当$monitor任务的参数列表中有一个或多个参数的值发生变化时(不包括 t i m e 、 time、 time、stime和$realtime)打印信息。这个连续地监控参数值的特性,称作连续监控。
$monitoron 和 $monitoroff用于启用或禁用监控过程,相当于打开或关闭监控标志。打开监控标志时,无论参数值是否发生变化,会立即打印一次信息;关闭监控标志后,监控任务处于待机状态,不会执行。默认情况下, 仿真开始的时候会自动打开监控标志。
Verilog文件操作涉及到的函数也放在这里。
打开文件任务 $fopen 和关闭文件任务 $fclose 的使用方法如下:
mcd = $fopen("file_name"); fd = $fopen("file_name", type); fclose(mcd); fclose(fd);$fopen的输入参数都是字符串。如果没有指定type参数,返回值称作多通道描述符(MCD);如果设置了type参数,设定打开文件的方式,返回值称作文件描述符(FD)。MCD和FD都是32bit的数,二者的区别大致如下(示例代码见下一小节):
MCD:32bit,最高位保留不用,剩下的每bit代表一个打开的文件(置1),因此同时最多只能打开31个文件。LSB被“标准输出”文件占用。这种方法的优势是可以将多个MCD用按位或的方式组合在一起,从而同时向多个文件写入内容。这种打开方式相当于是FD的"w"方式,不支持读取文件内容。
FD:32bit,最高位保留不用,恒定为1。三个默认打开的文件会占用三个文件描述符:STDIN、STDOUT、STDERR。这种方法的优势是可以指定打开文件的方式,不过不支持用按位或的方式组合FD,因此不能同时写入多个文件。
$fopen支持的文件打开方式如下表所示,支持的方式与在C语言种的含义相同:
type参数功能r / rb只读,文件指针指向文件头(不删除文件已有内容);文件不存在则返回0r+ / r+b / rb+可读可写,文件指针指向文件头(删除文件已有内容);文件不存在则返回0w / wb只写,文件指针指向文件头(删除文件已有内容);文件不存在则尝试创建w+ / w+b / wb+可读可写,文件指针指向文件头(删除文件已有内容);文件不存在则尝试创建a / ab只写,文件指针指向文件末尾(不删除文件已有内容);文件不存在则尝试创建a+ / a+b/ ab+读写方式打开,文件指针指向文件末尾(不删除文件已有内容);文件不存在则尝试创建参数中的"b"用于操作二进制文件,用以区分文本文件。
$fclose通过MCD或FD关闭了文件后,则不能再对此文件进行读、写操作,对此文件的 f m o n i t o r 和 fmonitor和 fmonitor和fstrobe操作同时也会取消。相应的资源也会释放出来,用于打开其它文件。
4.1节中介绍的“显示任务”,在文件操作中都有相对应的系统任务,列于下表:
$fdisplay$fwrite$fstrobe$fmonitor$fdisplayb$fwriteb$fstrobeb$fmonitorb$fdisplayo$fwriteo$fstrobeo$fmonitoro$fdisplayh$fwriteh$fstrobeh$fmonitorh只不过显示任务的信息输出对象为“标准输出”,在Vivado中也就是Tcl控制台;加了"f"后,文件输出任务的信息输出对象为“文件”。唯一区别在于:文件输出任务的第一个参数是要写入文件的MCD或FD。
此外也没有与 m o n i t o r o n 和 monitoron和 monitoron和monitoroff对应的任务。如果要关闭 f m o n i t o r 或 fmonitor或 fmonitor或fstrobe任务的监控,需要用$fclose关闭文件来实现。
下面给出一个示例代码,展示了MCD和FD的区别,以及如果通过按位或操作同时执行多个MCD的文件输出:
reg [31:0] fd1, fd2, fd3; reg [31:0] mcd1, mcd2, mcd3; reg [31:0] file_output; initial begin // MCD FILE OPEN mcd1 = $fopen("test1.txt"); if (mcd1 == 0) begin $display("mcd open1 failed!"); $finish; end mcd2 = $fopen("test2.txt"); if (mcd2 == 0) begin $display("mcd open2 failed!"); $finish; end mcd3 = $fopen("test3.txt"); if (mcd3 == 0) begin $display("mcd open3 failed!"); $finish; end // 同时写入3个文件 file_output = mcd1 | mcd2 | mcd3 | 1; $fdisplay(file_output,"mcd1: %h, mcd2: %h, mcd3: %h", mcd1, mcd2, mcd3); $fclose(mcd1); $fclose(mcd2); $fclose(mcd3); // FD FILE OPEN fd1 = $fopen("test4.txt", "r+"); fd2 = $fopen("test5.txt", "r+"); fd3 = $fopen("test6.txt", "r+"); $display("fd1: %b, fd2: %b, fd3: %b", fd1, fd2, fd3); $fclose(fd1); $fclose(fd2); $fclose(fd3); end使用按位或运算符将多个MCD合并在一起,1(即32bit中的LSB)表示标准输出,在Vivado中是Tcl控制台。执行$fdisplay会向三个文件和Tcl控制台同时输出信息。
终端打印信息如下,MCD和FD的值与前面所说的它们的特点相吻合:
mcd1: 00000002, mcd2: 00000004, mcd3: 00000008 fd1: ffffb1e0, fd2: ffffb1e1, fd3: ffffb1e2** s w r i t e ∗ ∗ 任 务 ( 包 括 swrite** 任务(包括 swrite∗∗任务(包括swriteb、 s w r i t e o 、 swriteo、 swriteo、swriteh)和 $sformat 任务可以将数据以格式化的形式转换为字符串,存储到reg型变量中。它们的使用方法和 $fwrite 完全一样,只是第一个参数不是要输出的文件FD或MCD,而是要输出的reg型变量的名称。
这两个任务的功能和C语言stdio.h库中sprintf函数的功能类似。这两个的任务的区别在于:
$swrite:和其它任务一样,格式控制可以放在参数列表的不同参数中;$sformat:格式控制只能放在第二个参数中,其余参数全部被视作输出项(和stdio库的sprintf函数用法完全相同)。下面给出一个示例代码,试图将128和351两个数字拼接为字符串存放在reg变量中:
reg [6*8:1] str_reg1, str_reg2; reg [8:0] str1 = 128, str2 = 351; initial begin // 格式控制放在不同的参数中 $swrite(str_reg1,"%0d","%0d",str1,str2);//"%0d",str2); $display("The value of str_reg1 is : %s", str_reg1); // 格式控制只能放在第二个参数中 $sformat(str_reg2,"%0d","%0d",str1,str2);//"%0d",str2); $display("The value of str_reg2 is : %s", str_reg2); end终端打印信息如下:
The value of str_reg1 is : 128351 ERROR: No Format provided for this argument ERROR: No Format provided for this argument The value of str_reg2 is : 437220两个任务都采用将格式控制"%d"放在两个参数内的形式。 s w r i t e 正 确 的 得 到 了 字 符 串 " 128351 " ; swrite正确的得到了字符串"128351"; swrite正确的得到了字符串"128351";sformat得到的结果错误,且提示了两次ERROR信息。这正是因为$format只将第二个参数"%d"视作格式控制,其余三个参数视作输出项,导致错误发生。
“c = $fgetc(fd)” 用于读取FD指向文件的一个字符(相当于1byte),如果发生错误,则返回"EOF(值为-1)"。-1用二进制补码表示各bit全为1,因此存放读取结果的reg型变量c的位宽最好大于8bit,这样才能将EOF和字符"0xFF"区分开。
**"code = u n g e t c ( c , f d ) " ∗ ∗ 用 于 向 文 件 的 b u f f e r 中 插 入 一 个 字 符 c ( 实 际 上 没 有 改 变 文 件 内 容 ) , 下 一 次 调 用 ungetc(c, fd)"** 用于向文件的buffer中插入一个字符c(实际上没有改变文件内容),下一次调用 ungetc(c,fd)"∗∗用于向文件的buffer中插入一个字符c(实际上没有改变文件内容),下一次调用getc任务时,会读取到这个字符。如果插入成功,则返回值code为0;插入失败,返回值code为EOF。
下面是一个测试代码,假设文本文件"test1.txt"中有一行字符串"fpgaesigner":
integer fd; initial fd = $fopen("test1.txt", "r"); reg [15:0] c, ins_flag; initial begin repeat (4) begin c = $fgetc(fd); $write("%c", c); end ins_flag = $ungetc("D", fd); // 中间插入字符,但实际没有写入文件中 repeat (8) begin c = $fgetc(fd); $write("%c", c); end c = $fgetc(fd); $write("\n%h\n", c); // 读取错误的字符,返回EOF $fclose(fd); end // 终端打印信息 fpgaDesigner ffff读取完所有字符后,再用$fgetc读取会发生错误,返回值为"0xFFFF"。
"code = $fgets(str, fd)" 用于读取一组字符,当遇到这三种情况时会停止读取:(1)读到换行符’\n’;(2)读取的字符已经填满了reg类型的变量str;(3)读到EOF,错误发生。 出现错误时,返回值code为0;否则,返回值code为已读取的字符个数。
看下面的示例代码,假设test1.txt中存放了两行数据“fpga”和“Designer”:
integer fd; initial fd = $fopen("test1.txt", "r"); reg [5*8-1:0] str, read_cnt; initial begin repeat(3) begin read_cnt = $fgets(str,fd); $display("read cnt : %d", read_cnt); $display("str : %s", str); end $fclose(fd); end //打印信息 read cnt : 5 str : fpga read cnt : 5 str : Desig read cnt : 3 str : ner换行符’\n’也会占一个字符,因此第一次$fgets对应的打印信息中有一次换行。最后一次读取时已经读到了文件末尾,因此只读取了3个字符,填充在了str的低位上。
"code = $fscanf(fd, format, args);" 和 “code = $sscanf(str, format, args);” 用于按照指定格式读取文件或reg变量中的内容,相当于stdio.h库中的sscanf函数。
读取的数据按照format指定的格式存放在args列表的参数中,返回值code是输入项成功匹配的个数。示例代码如下:
//test1.txt文本文件内容: dog cat 1999 2019 pnp npn 2009 2029 //示例代码: integer fd; initial fd = $fopen("test1.txt", "r"); integer rdcnt; reg [3*8-1:0] str1, str2, data1, data2; reg [17*8-1:0] str = "dog cat 1999 2019"; initial begin // 从文件格式化输入 repeat (2) begin rdcnt = $fscanf(fd,"%s %s %d %d", str1, str2, data1, data2); $display("fscanf read %0d char: %s %s %d %d", rdcnt, str1, str2, data1, data2); end // 从reg输入 rdcnt = $sscanf(str,"%s %s %d %d", str1, str2, data1, data2); $display("sscanf read %0d char: %s %s %d %d", rdcnt, str1, str2, data1, data2); $fclose(fd); end //终端打印信息 fscanf read 4 char: dog cat 1999 2019 fscanf read 4 char: pnp npn 2009 2029 sscanf read 4 char: dog cat 1999 2019$fread可以批量地读取二进制数据(文本文件当然也是二进制文件,只不过是二进制数据中可以显示的ASCII子集),其用法比较多:
code = $fread(reg, fd); code = $fread(mem, fd); code = $fread(mem, fd, start); code = $fread(mem, fd, start, count); code = $fread(mem, fd, , count);读取的数据可以放在reg型变量或者内存mem(也就是reg的二维数组)中,其它参数的含义如下:
start: 读取的第一个数据装载在mem中的地址位置。没有使用该参数,则从mem的最低地址开始填装。
count: 向mem中装载数据数量的最大值(1byte为1个数据)。没有使用该参数,则$fread一直读取数据,直到mem填满或者数据文件已读完。
$fread读取数据是按字节(byte)的方式。返回值code为已读取的字符数,0则表示有错误发生导致读取失败。给出一个示例,存在一个二进制文件"1.bin"内容如下:
如下代码读取文件内容:
integer fd; initial fd = $fopen("../1.bin", "rb"); integer i; reg [15:0] data [15:0]; // mem一个元素存储2byte integer rdcnt; initial begin rdcnt = $fread(data,fd); $display("read %0d data", rdcnt); for (i = 0; i < 16; i=i+1) $write("%h ", data[i]); $display; // 补一个换行符 $fclose(fd); end //终端打印信息 read 32 data 5478 9cec dd7b 7095 759e effb 4f2e 4410 4c04 5418 9380 48d2 22a0 9e9e 118a 8bcc1byte为1个数据,因此上面的mem功能存储32个数据,读到的数据与文件内容一致。
所有文件I/O执行过程中如果发生了错误,会返回一个错误代码。使用" errno = $ferror(fd, str); "可以获取最近一次I/O操作的错误信息,以字符串的形式存储在reg型变量str中(位宽至少640bit),同时还会返回一个常数值errno,与特定的错误信息相对应。
示例代码如下:
integer fd, err, code; reg [639:0] errstr; reg data [3:0]; initial begin fd = $fopen("../test5.txt", "r"); // 打开文件 err = $ferror(fd, errstr); $display("err %0d: %0s",err,errstr); code = $fscanf(fd,"%b",data); // 读取文件内容 err = $ferror(fd, errstr); $display("err %0d: %0s",err,errstr); end //打印信息 err 0: No Error err -1: end of file is seen打开文件时,没有错误发生,errno值为0,str的值为" No Error ";在执行$fscanf时,读完文件中所有数据后,返回一个"end of file is seen"的错误。
" code = $feof(fd); "用于检测当前文件指针是否指向EOF(文件末尾),返回非0值表示指针已经指向文件末尾,否则返回0。示例代码如下:
integer fd, code; initial begin fd = $fopen("../test5.txt", "a+"); // 打开文件 while ($feof(fd) == 0) begin code = $fgetc(fd); $write("%c", code); end $display("\nFIND EOF: %0d",$feof(fd)); end //打印信息 FIND EOF: 1当 f g e t c 读 到 文 件 末 尾 时 , fgetc读到文件末尾时, fgetc读到文件末尾时,feof的返回值为1(非0)。
" pos = $ftell(fd); " 用于获取当前文件指针指向的位置。" code = $fseek(fd, offset, operation); " 和 " code = $rewind(fd); " 用于调整文件指针的位置。
$fseek将文件指针按照operation设置的方式移动offset个字节。operation的含义如下表:
值功能0文件指针移动到offset指定的位置处1文件指针移动到当前位置加上offset的位置处2文件指针移动到EOF加上offset的位置处r e w i n d 相 当 于 rewind相当于 rewind相当于fseek(fd,0,0),把文件指针重新移到文件开始位置(wind有“倒带”的意思)。文件指针移动过程中发生错误则返回-1,否则返回0。下面是一个示例代码:
integer fd; reg [7:0] data [3:0]; initial begin fd = $fopen("../test5.txt", "r"); // 打开文件 $display("after open pos: %0d", $ftell(fd)); $fseek(fd,8,0); // 定位到位置“8” $display("after fseek pos: %0d", $ftell(fd)); $fread(data,fd); // 读取数据 $display("after read pos: %0d", $ftell(fd)); $rewind(fd); // 重新定位到开头 $display("after rewind pos: %0d", $ftell(fd)); $fclose(fd); end //打印信息 after open pos: 0 after fseek pos: 8 after read pos: 12 after rewind pos: 0如果文件以“a”或“a+”的方式打开,所有输出到文件的内容只会添加到文件末尾,改变文件指针的位置不会改变文件写入的位置(事实上,当写入文件时,文件指针会自动重新定位到文件末尾)。
疑问记录: 一直没有搞清楚Verilog中“a+”的文件打开方式。按描述来说应该是“可读可写”。但在Vivado Simulator中测试,以a+方式打开文件,始终无法读出数据。 打开文件后,将文件指针移动到任何位置(有数据),读取时都会提示错误" err:end of file is seen "。有待进一步查证。
r e a d m e m b ∗ ∗ 和 ∗ ∗ readmemb** 和 ** readmemb∗∗和∗∗readmemh 可以批量地把文本文件中的数据读入到内存中,是一种快速的文件读取方法,无需打开文件、关闭文件等操作。读取的文本文件只能包含以下内容:
空白区:空格、换行、TAB、跳页;注释:支持所有类型的注释。使用注释和空白区的目的是为了分割不同的数字;二进制或十六进制数字: r e a d m e m b 用 于 读 取 二 进 制 数 据 , readmemb用于读取二进制数据, readmemb用于读取二进制数据,readmemh用于读取十六进制数据。两个任务的使用方法如下:
$readmemb/h("file_name", memory_name); $readmemb/h("file_name", memory_name, start_addr); $readmemb/h("file_name", memory_name, start_addr, final_addr);调用任务时可以设置数据存放在内存中的起始地址start_addr和结束地址final_addr。内存地址也可以在文本文件中定义,使用" @hhhh ",即@符号+十六进制形式的地址数据。这样牵涉到的情况比较复杂,下面以示例的形式说明:
// test1:正常读取 调用任务:$readmemh("../test5.txt"); 文本内容:A5A5 1234 8BCD 5869 2386 内存内容:a5a5,1234,8bcd,5869,2386, // test2:调用任务时指定内存地址 => 按地址递增顺序存放在指定的地址范围内 调用任务:$readmemh("../test5.txt", data, 2, 4); 文本内容:A5A5 1234 8BCD 5869 2386 内存内容:xxxx,xxxx,a5a5,1234,8bcd, // test3:调用任务时指定内存地址,起始地址小于结束地址 => 按地址递减顺序存放 调用任务:$readmemh("../test5.txt", data, 4, 2); 文本内容:A5A5 1234 8BCD 5869 2386 内存内容:xxxx,xxxx,8bcd,1234,a5a5, // test4:文件内指定内存地址+注释 => 忽略注释的数据 调用任务:$readmemh("../test5.txt"); 文本内容:@2 A5A5 @3 1234 //8BCD 5869 2386 内存内容:xxxx,xxxx,a5a5,1234,xxxx, // test5:文件内指定内存地址,地址随机顺序 => 数据分别存放到指定位置 调用任务:$readmemh("../test5.txt"); 文本内容:@2 A5A5 @1 1234 @4 8BCD //5869 2386 内存内容:xxxx,1234,a5a5,xxxx,8bcd, // test6:调用任务和文件内都指定了地址 => 文件内的地址必须在任务参数地址的范围内 调用任务:$readmemh("../test5.txt"); 文本内容:@2 A5A5 @1 1234 @4 8BCD //5869 2386 内存内容:xxxx,xxxx,a5a5,xxxx,xxxx, 报错:ERROR: Out of bounds address specified in datafile. Read terminated.这类任务用于显示和设置时标信息。$printtimescale用于显示指定模块的时间单位和精度,不加任何参数便是显示调用此任务的模块的时标信息。示例如下:
`timescale 1ns / 1ps module sim(); moda a1(); initial $printtimescale(); initial $printtimescale(a1); initial $printtimescale(a1.b1); endmodule `timescale 10fs / 1fs module moda(); modb b1(); endmodule `timescale 10ns / 1ns module modb(); endmodule查看特定模块的时标信息,则要在任务参数中指定模块的层次结构信息。运行上面代码的打印信息如下:
Timescale of (sim) is 1ns/1ps. Timescale of (sim.a1) is 10fs/1fs. Timescale of (sim.a1.b1) is 10ns/1ns.仿真控制系统任务有两个,在不同仿真工具中的表现会有所区别。$finish结束仿真,有一个输入参数决定结束仿真时显示的消息,如下表所示(以Vivado Simulator为例):
参数值信息示例0不打印信息$finish called at time : 1 us1打印仿真时间和位置$finish called at time : 1 us : File “C:/Users/GodWa/Desktop/test_sim/test_sim.srcs/sim_1/new/sim.v” Line 362打印仿真时间、位置、仿真使用的内存和CPU时间$finish called at time : 1 us : File “…/sim.v” Line 36 Memory in use : 7204 KB (peak memory: 7204 KB) CPU usage : 15 ms如果调用$finish时没有加参数,则使用默认值1。$stop用于暂停仿真。但在Vivado仿真器中,除了打印的信息外,这两个系统任务的实际表现并没有明显差别。
Verilog提供了一组用于对PLA(可编程逻辑阵列)器件进行建模的系统任务,不过在FPGA设计和仿真文件设计中都用不到,这里省略。
这类任务包括 $q_initialize、$q_add、$q_remove、$q_full、$q_exam用于实现随机队列模型,分别用于创建队列、向队列添加一个实体、从队列拿走一个实体、检查队列是否有放入实体的空间、提供关于队列中活动的一些统计学信息。
目前还不会用到这类任务,待以后补充。
系统函数可划分为五类,下面分别给出一些常用系统函数的用法。
Verilog提供了三个系统函数,来获取当前的仿真时间。
$time返回一个time类型的数据(实际上就是64bit的无符号整数),表示多少个时间单位。比如下面的代码:
initial begin #100; $display("%d", $time); #500; $display("%d", $time); end打印值分别是100和600,其真实含义的时间是由`timescale定义的时间单位决定的。$stime的作用和 $time完全相同,只是返回值的位宽只有32bit。
$realtime也是同样的作用,只是返回的是实数。当时间单位不是整数时(比如有" #5.5 "这样的情况),可以使用这个系统函数来获得精确的仿真时间。
$signed 和 $unsigned比较熟悉,再看其它的转换函数。
$rtoi 和 $itor 用显示的方式完成实数和整数之间的转换(real to integer、integer to real) 。$rtoi和直接将实数赋值给整数的效果不同,如下面代码
reg [7:0] dataa = 0, datab = 0; always @ (posedge clk) begin dataa <= 5.7; // 结果为6 datab <= $rtoi(5.7); // 结果为5 end直接赋值采用近似的方式,$rtoi直接会截断小数部分。
$realtobits 和 $bitstoreal 用于完成实数和64-bit向量形式之间的转换,如下面的代码:
real a = 568.96, b = 0; reg [63:0] dataa = 0; always @ (posedge clk) begin dataa = $realtobits(a); // 结果为双精度浮点数 b <= $bitstoreal(dataa); // 结果为568.96 end实数转换后的64-bit向量采用的是IEEE 754规定的双精度浮点数表示方法。
这类函数用于生成服从某种标准概率分布函数的整数。
$random函数用于生成随机数,每次调用时返回一个新的32bit随机数(带符号整数)。该函数有一个可选的参数(reg、integer或time类型),表示随机种子。下面给出两个例子:
// example1: 产生(-b+1)到(b-1)之间的随机数 reg [23:0] rand; rand = $random % b; // example2: 产生0到b-1之间的随机数 reg [23:0] rand; rand = {$random} % b;example2是利用的拼接运算符的性质,拼接运算符的结果是无符号数,因此整个表达式的结果变成了无符号数。
产生服从某种分布的概率分布函数的伪随机数的其它系统函数如下表所示(所有参数都是整数值):
系统函数PDF$dist_uniform(seed, sstart, end)均匀分布$dist_normal(seed, mean, standard_deviation)正态分布$dist_exponential(seed, mean)指数分布$dist_poisson(seed, mean)泊松分布$dist_chi_square(seed, degree_of_freedom)卡方分布$dist_t(seed, degree_of_freedom)T分布$dist_erlang(seed, k_stage, mean)埃尔朗分布设计者为这些函数提供一个初始的种子值seed,然后让系统函数自动更新这个种子,从而得到特定的分布函数。有两点注意事项:
start和end参数:start的值必须小于end的值,限定了$dist_uniform返回值的范围。mean参数表示一系列函数返回值的均值;standard_deviation和degree_of_freedom表示方差和自由度,决定了随机变量概率密度函数的形状。特别注意:所有函数的种子参数seed(包括$random)必须以变量的形式传入,不能直接给一个常数(seed参数是inout型的,系统函数读取这个值后还会返回另一个值,因此必须用变量作为载体),否则系统函数是不能正常工作的。示例代码如下:
integer seed = 5200; initial begin forever @ (posedge clk) cnt = $random(seed); endVerilog 2005标准的17.9.3小节,给出了实现上面所有函数算法的C语言代码,感兴趣可以查阅。
t e s t test testplusargs 和 v a l u e value valueplusargs 两个系统函数和启动仿真时的参数有关。仿真工具中启动仿真时,可以用"+"指定一些额外的传入参数,这两个函数用于搜索参数中的字符串或值。
这个功能和预编译中的"`ifdef"类似,只不过是面向仿真阶段,比如启动仿真时,传入不同的参数以执行不同的仿真代码。目前没有发现Vivado Simulator有相关的接口,不做详细介绍,等以后用到时再补充。
这类函数用于完成一些常见的数学运算,输入参数和返回值可以是整数或实数。这些函数的效果和C语言中的标准数学库math.h相同,罗列于下表。
Verilog函数等效的C语言函数功能$ln(x)log(x)自然对数$long10(x)log10(x)对数(以10为底)$exp(x)exp(x)指数$sqrt(x)sqrt(x)平方根$pow(x,y)pow(x, y)幂函数$floor(x)floor(x)向下取整$ceil(x)ceil(x)向上取整$sin(x)sin(x)正弦$cos(x)cos(x)余弦$tan(x)tan(x)正切$asin(x)asin(x)反正弦$acos(x)acos(x)反余弦$atan(x)atan(x)反正切$atan2(x,y)atan2(x,y)x/y的反正切$hypot(x,y)hypot(x,y)欧氏距离 sqrt(x2+y2)$sinh(x)sinh(x)双曲正弦$cosh(x)cosh(x)双曲余弦$tanh(x)tanh(x)双曲正切$asinh(x)asinh(x)反双曲正弦$acosh(x)acosh(x)反双曲余弦$atanh(x)atanh(x)反双曲正切