【Verilog】系统任务和编译指令
系统任务和编译指令
- 一、系统任务
- 1. 输出类任务: display write strobe monitor
- 2. 仿真控制类: stop finish
- 3. 仿真时间: time stime realtime timeformat
- 4. 命令行传参: plusargs
- 5.文件读写: fopen fscanf readmemb readmemh
- 二、编译指令
- 1. 宏定义:`define
- 2. 条件编译:`ifdef
- 3. 文件包含:`include
- 4. `default_nettype
- 5. `resetall
- 6. `celldefine
Verilog中的两个特殊概念:系统任务和编译指令。
一、系统任务
Verilog 为某些常用操作提供了标准的系统任务(也称系统函数), 这些操作包括屏幕显示 、线网值动态监视 、 暂停和结束仿真等。所有的系统任务都具有$<keyword>的形式。
系统任务主要用于仿真调试与控制,而非综合生成硬件电路。
1. 输出类任务: display write strobe monitor
$display的使用方法和 C 语言中的 printf 函数非常类似,可以直接打印字符串,也可以在字符串中指定变量的格式对相关变量进行打印。
$display("This is a test.");// 直接打印字符串$display("This is a test number: %b.",num);// 以二进制格式打印变量 num如果没有指定变量的显示格式,变量值会根据在字符串的位置显示出来,相当于参与了字符串连接。例如:
$display("This is a test number: ",num,"!!!");常用的输出格式:
在输出浮点数的时候,还可以指定精度:
moduletop;initial begin real num=123.456789;$display("Default precision: %f",num);// 默认6位小数$display("2 decimal places: %.2f",num);// 2位小数$display("5 decimal places: %.5f",num);// 5位小数end endmodule此外,还可以使用转义字符显示特殊字符:
$wirte使用方法与$display完全一样,只是前者会在每次显示信息完毕后不会自动换行,后者会自动换行。
当输出后不需要换行时,可以使用显示任务$write。
$strobe的使用方法与$display一致,但打印信息的时间和$display有所差异。
当许多语句与$display任务在同一时间内执行时,这些语句和 $display 的执行顺序是不确定的,一般按照程序的顺序结构执行。
$strobe则是在其他语句执行完毕之后,才执行显示任务。
示例1:
reg[3:0]a;initial begin a=1;#1;a<=a+1;//第一次显示$display("$display excuting result: %d.",a);$strobe("$strobe excuting result: %d.",a);#1;$display();//第二次显示$display("$display excuting result: %d.",a);$strobe("$strobe excuting result: %d.",a);end执行结果如下:
执行第一次显示任务时,非阻塞赋值与$display同时执行,$display显示赋值之前的变量值,而$strobe显示赋值之后的变量值。
示例2:
integer i;initial beginfor(i=0;i<4;i=i+1)begin $display("Run times of $display: %d.",i);$strobe("Run times of $strobe: %d.",i);end end显示结果如下:
$display按照程序结构,执行显示操作 4 次。而此循环语句是在 0 时刻执行的,所以$strobe显示的变量值是循环结束时变量的结果,即 i=4 退出循环后$strobe才会执行。
$monitor为监测任务,用于变量的持续监测。只要变量发生了变化,$monitor就会打印显示出对应的信息。
reg[3:0]cnt;initial begin cnt=3;forever begin #5;if(cnt<7)cnt=cnt+1;end end initial begin $monitor("Counter change to value %d at the time %t.",cnt,$time);end显示的内容如下:
2. 仿真控制类: stop finish
$finish是结束本次仿,$stop是暂停当前的仿真。仿真暂停后通过 Verilog 仿真工具或命令行还可以使仿真继续进行,而结束仿真后仿真无论如何也不能再进行
示例:
initial begin forever begin #100;if($time>=10000)$finish(0);//if ($time >= 10000) $finish(1) ;//if ($time >= 10000) $finish(2) ;end end$finish(0):仿真退出时不打印任何信息。
$finish(1):仿真退出时打印仿真时间(单位是ps)和$finish所在的行信息
$finish(2):仿真退出时不仅打印仿真时间和行信息,还打印一些其他信息。
3. 仿真时间: time stime realtime timeformat
- 仿真时间
$realtime、$time和$stime的时间单位是timescale中指定的时间精度,$realtime会按照当前的时间对仿真时间进行准确读取,而$time和$stime会对当前时间进行四舍五入的读取。
示例:
initial begin #10;$display("$time output1: %t",$time);$display("$stime output1: %t",$stime);$display("$realtime output1: %t",$realtime);#3.2;$display("$time output2: %t",$time);$display("$stime output2: %t",$stime);$display("$realtime output2: %t",$realtime);#5.6;$display("$time output2: %t",$time);$display("$stime output2: %t",$stime);$display("$realtime output2: %t",$realtime);end结果如下:
- 时间显示格式
$timeformat是一个系统任务,用于配置仿真中时间值的打印格式,影响$display、$strobe等系统任务中%t格式符的输出样式。
定义$timeformat之后,它会持续生效,直到执行了另一个$timeformat。
$timeformat的基本语法如下:
$timeformat(units_number,precision_number,suffix_string,minimum_field_width);- units_number:指定时间单位,范围从 0 到 -15,对应不同时间单位(0 为 s,-9 为 ns,-12 为 ps)。
- precision_number:定义时间值小数点后的位数,四舍五入保留。
- suffix_string:在时间值后添加自定义后缀(如单位符号)。
- minimum_field_width:设置输出字符串的最小宽度,不足时左侧补空格
示例:
`timescale10ns/1psmoduletb_timeformat();initial $timeformat(-9,2," ns",10); #1.2345; $display("%t: simulation started.",$realtime);end endmodule///////////////////////////// 12.34 ns: simulation started.///////////////////////////4. 命令行传参: plusargs
仿真时可通过命令行传参的方式进行参数的传递:
使用$test$plusargs( str )时,只需在仿真命令行中加入"+str "即可。
使用$value$plusargs( str,var )时,需要在 str 内部指定传递参数时数值的类型。而在命令行传递参数时,数值不需要添加任何有关进制的说明,只保留相关进制的数值即可。命令行传递参数的格式需要参照$value$plusargs时 str 声明的格式。
示例:
initial beginif($test$plusargs("DISPLAY_CTRL"))begin $display("Display simulation information!!!");end end reg[1:0]display_sel;initial beginif($value$plusargs("INFO_SEL=%b",display_sel))begin $display("Parameter transfer succeeds!!!");endelsebegin display_sel=2'b0;end end initial begin #1;if(display_sel==2'b01)$display("You have selected Runoob!!!");elseif(display_sel==2'b10)$display("You have selected Verilog!!!");elseif(display_sel==2'b11)$display("You have selected Me!!!");else$display("What do you really what???");end在仿真的命令行中添加+DISPLAY_CTRL +INFO_SEL=01即可传递参数:
./simv \+DISPLAY_CTRL+INFO_SEL=01\-l logs/run.log输出结果如下:
因为在定义INFO_SEL的时候,已经指定其类型为二进制(INFO_SEL=%b),所有船体给它的 01 会被仿真工具认为是二进制,而不是其他类型。
5.文件读写: fopen fscanf readmemb readmemh
- $fopen
Verilog 中的$fopen用来打开一个文件,其基本用法如下:
integer handle1;handle1=$fopen("filename",type)使用fopen打开一个文件之后,将其保存在变量 handle1 中(可以将其称之为文件指针),后续针对该文件的操作就可以直接使用 handle1 这个变量。
type 指定文件的打开方式,具体如下选项如下,其中带有 b 的表示的是二进制文件,其他表示的是普通文本文件。
$fopen只负责打开文件,如果要读取文件中的内容,还需要使用其他系统函数,如$fscanf、$readmemb等。
- $fscanf
$fscanf是 SystemVerilog 中的一个文件输入函数,用于从文件中读取数据并将其转换为特定格式,其基本用法如下:
integer fstat;fstat=$fscanf(fd, format, args)其中,fd 是使用$fopen打开文件时指定的变量,args 是一个 reg 类型的变量。
该命令的意思是,从 fd 中去读取数据,将其转换为 format 指定的格式,然后将转换结果保存在 args 中。
$fscanf中常用的数据格式包括:%b(二进制)、%o(八进制)、%d(十进制)、%h(十六进制)、%s(字符串)等
$fscanf停止的条件是读取到了空格、制表符、换行符或换页符等,具体含义如下实例所示。
假设一个名称为 test.txt 的文件中的内容为:
0123456789adcde fffff55555bbbbb eeeee33333使用fscanf读取其中的内容:
integer fd1;//句柄integer code;//返回操作状态initial begin fd1=$fopen("test.txt","r");//可读end integer i;reg[19:0]din;//存放$fscanf读取的数据,位宽与单个数据位宽一致reg[19:0]file_data[0:7];//将文件中的所有数据进行存储initial beginfor(i=0;i<8;i=i+1)begin code=$fscanf(fd1,"%h",din);//将16进制数据写入din中file_data[i]=din;//依次记录所有16进制数据endfor(i=0;i<8;i=i+1)begin $display("file_data[%2d]=%h",i,file_data[i]);//输出数据进行查看end $fclose(fd1);end输入结果如下:
file_data[0]=01234 file_data[1]=56789file_data[2]=adcde file_data[3]=fffff file_data[4]=55555file_data[5]=bbbbb file_data[6]=eeeee file_data[7]=33333也就是说,可以使用fscanf对同一个文件进行多次读取,下一次读取开始的地方是上一次读取停止的地方,每次读取停止的条件是读取到了空格、制表符、换行符或换页符等
- $readmemb & $readmemh
这两个系统函数的功能是将文件中的数据读取到 reg 数组中,readmemb 按照二进制格式读取,readmemh 按照16进制格式读取,其他没有区别。
使用这两个系统任务的时候,被读取的数据文件的内容只能包含:空白位置(空格、换行、制表格)、注释行、二进制或十六进制的数字。数字中不能包含位宽说明和格式说明,并且必须用空白位置或注释行来分隔开。
$readmemb的基本用法如下:
$readmemb("<数据文件名>",<数组名>)$readmemb("<数据文件名>",<数组名>,<起始地址>)$readmemb("<数据文件名>",<存贮器名>,<起始地址>,<结束地址>)注意,这里是直接使用文件名,而不是文件指针。
示例:
假设一个命令为 test.txt 的文件中的内容为:
00 01 02 03 04 05 06 07 08 09使用$readmemb来读取文件中的内容:
`timescale1ns/1nsmoduletb_read_test();integer i;reg[7:0]mem_test[9:0];//mem_test是位宽8bit,个数为10的数组initial $readmemh("test.txt",mem_test);//绝对路径//显示数组的10个值initial beginfor(i=0;i<10;i=i+1)$display("%d: %h",i,mem_test[i]);end endmodule输出结果如下:
0: 001: 012: 023: 034: 045: 056: 067: 078: 089: 09在不指定地址的情况下,$readmemh将从数据文件中读到的第 1 个数据填入数组的第 0 个位置,以此类推,直到数组被填满。
如果数据的个数大于数据文件中数据的个数,则数组无法被填满,未被填满的部分则依然未被赋值。
如果指定开始地址,就是将读到的第一个数据填充到指定的开始地址,以此类推;如果指定结束地址,填充到结束地址之后,不在填充数据。
二、编译指令
就像 C/C++ 程序需要编译成 .exe 或可执行文件才能运行一样,VCS 编译就是将 HDL “源代码” 编译成计算机 CPU 可以直接高效执行的“仿真程序”(simv),在编译的过程中会对源代码进行分析、检查和优化。
编译指令(Compiler Directives) 是一种特殊的预处理命令,以特定符号(Verilog中的是 `)开头。它们在代码编译前被处理,用于指导编译器如何解析、转换或配置源代码,从而影响编译结果和最终生成的仿真模型。
编译指令并不是Verilog语言的一部分,而是编译器的处理指令
Verilog 中常用的一些编译指令如下:
1. 宏定义:`define
在编译阶段,define用于定义宏常量或者宏函数,或者说是用于文本替换。
一旦define指令被编译,其在整个编译过程中都会有效,相当于是定义了一个 “全局变量”。
例如,在一个文件中定义宏常量:
`define DATA_DW32在另一个文件中可以直接使用 DATA_DW:
reg[`DATA_WIDTH-1:0]data使用define定义函数(或者说带参数的宏)的示例如下:
`defineMAX(a,b)((a)>(b)?(a):(b))initial $display("Max: %d",`MAX(5,9));如果使用define定义的内容很多,可以使用换行符:
`defineCOUNTER_MODULE(name,width)\modulename(\ input wire clk,\ input wire reset_n,\ input wire enable,\ output reg[width-1:0]count \);\ always @(posedge clkornegedge reset_n)begin \if(!reset_n)\ count<={width{1'b0}};\elseif(enable)\ count<=count+1'b1;\ end \ endmodule//===================================================`COUNTER_MODULE(counter8,8)// 生成8位计数器`COUNTER_MODULE(counter16,16)// 生成16位计数器undef用于取消之前定义的宏,释放宏名称以便重新定义。
使用define定义宏的时候是不能重复定义的,需通过undef指令取消原有定义之后才能重新定义。
`undef DATA_DW2. 条件编译:`ifdef
条件编译指令允许根据预定义条件选择性地包含或排除代码段,实现一套代码支持多种配置的目标。
条件编译相关的命令包括:indef、ifndef、elsif、else、endif。
`ifdef FPGA_TARGET `ifdef XILINX_FPGA// Xilinx FPGA特定代码parameter VENDOR="XILINX";(*KEEP="TRUE"*)wire keep_signal;`elsif ALTERA_FPGA// Intel/Altera FPGA特定代码parameter VENDOR="INTEL";(*preserve*)wire keep_signal;`else// 其他FPGAparameter VENDOR="GENERIC";`endif `elsif ASIC_TARGET// ASIC特定代码parameter VENDOR="ASIC";// 功耗优化代码`else// 默认仿真代码parameter VENDOR="SIMULATION";`endif3. 文件包含:`include
include指令在编译时将指定文件的内容插入到当前位置,该指令通常用于将全局或公用的头文件包含在设计文件里。
文件路径既可以使用相对路径,也可以使用绝对路径。
`include"../../param.v"`include"cpu_defines.v"为了避免重复定义,include命令中指定的头文件一般包含保护机制:
// 每个.vh文件都应该有保护机制`ifndef PROJECT_DEFINES_VH `define PROJECT_DEFINES_VH// 使用严格的网络类型检查`default_nettype none// 常量定义`define WORD_SIZE32`define CACHE_SIZE1024// 宏函数定义`defineCLOG2(x)$clog2(x)`endif// PROJECT_DEFINES_V4. `default_nettype
该指令用于为隐式的线网变量指定为线网类型,即将没有被声明的连线定义为线网类型。
// 设置默认为wire类型(默认值)`default_nettype wire// 禁用隐式网络声明(推荐)`default_nettype nonemodulestrict_module(input wire clk,input wire reset_n,output wire result);// 所有信号必须显式声明wire internal_signal;// 必须声明reg state_reg;// 必须声明assign internal_signal=clk&reset_n;assign result=internal_signal;endmodule// 恢复默认行为`default_nettype wire5. `resetall
该编译器指令将所有的编译指令重新设置为默认值。
resetall可以使得缺省连线类型为线网类型。
当resetall加到模块最后时,可以将当前的timescale取消,防止其进一步传递,只保证当前的timescale在局部有效,避免timescale的错误继承。
6. `celldefine
celldefine和endcelldefine用于标记标准单元库中的基本单元,他们包含模块的定义。例如一些与、或、非门,一些 PLL 单元,PAD 模型,以及一些 Analog IP 等。
`celldefinemodule(input clk,input rst,output clk_pll,output flag);…… endmodule `endcelldefine