FPGA学习笔记(五)————Verilog HDL任务与函数、编译向导
文章目录
-
利用任务和函数可以把一个大的程序模块分解成许多小的任务和函数,以方便调试,并且能使写出的程序清晰易懂。**注意task和function定义和调用都包含在一个module模块内部,**格式与module模块类似,但也有不同。
它们一般用于行为建模,在编写测试验证程序时用得较多,佷多逻辑综合软件都不能佷好地支持任务和函数。
1.任务task与function的区别
-
task:
通常用于调试,或对硬件进行行为描述
可以包含时序控制(#延迟,@, wait)
可以有 input,output,和inout参数
可以调用其他任务或函数
-
function:
通常用于计算,或描述组合逻辑
不能包含任何延迟;函数仿真时间为0
只含有input参数并由函数名返回一个结果
可以调用其他函数,但不能调用任务 -
共同点:
任务和函数必须在module内调用
在任务和函数中不能声明wire
所有输入输出都是局部寄存器
任务函数执行完成后才返回结果。
2.任务task
-
任务(task)定义与调用的格式分别如下:
task 任务名 ; 端口及数据类型声明语句; 其他语句; endtask //任务调用的格式为: 任务名 (端口1,端口2,……)
-
比如下面是定义一个任务的例子:
//使用任务描述运算单元 module alu(a,b,result); input[1:0] a,b; output[3:0] result; wire[1:0] a,b; reg[3:0] result; always@(a, b) begin //按照任务中定义的端口顺序调用任务; cal(a,b, result); end //定义任务cal task cal; //任务端口列表 input[1:0] a; input[1:0] b; output[3:0] result; //内部定义局部变量(必须reg型) reg[3:0] temp; begin temp = a*a; result = temp-b; end endtask endmodule
-
使用任务时需要注意以下几点:
(1)任务的定义与调用须在一个module模块内。
(2)当任务被调用时,任务被澈活。任务的调用与模块调用一样通过任务名调用实现,调用,需列出端口名列表,端口名的排序和类型必须与任务定义中的排序和类型一致。
(3)一个任务可以调用别的任务和函数,可以调用的任务和函数个数不限。
(4)任务的定义不能出现在过程块的内部,任务内部定义的变量,其作用域在task和endtask之间。
-
在使用任务
含有延迟、时序或事件控制结构;
没有输出或输出变量数目大于1;
没有输入变量;
出现以上情况的,一般用任务而不用函数。
-
任务主要特点:
任务可以有input,output 和 inout参数。
传送到任务的参数和与任务I/O说明顺序相同。尽管传送到任务的参数名称与任务内部I/O说明的名字可以相同,但在实际中这通常不是一个好的方法。参数名的唯一性可以使任务具有好的模块性。
可以在任务内使用时序控制。
要禁止任务,使用关键字disable 。
自动任务
-
任务在本质上是静态的,任务中的所有声明项的地址空间是静态分配的。因此,如果这个任务在模块中的两个地方被同时调用,则这两个任务调用将对同一块地址空间进行操作。操作的结果很有可能是错误的。
为了避免这个问题,Verilog通过在task关键字前面添加关键字
automatic
,使任务成为可重入的,这样声明的任务也称为自动任务,每次调用时,在动态任务中声明的所有模块项的存储空间都是动态分配的,每个调用都对各自独立的地址空间进行操作。这样.每个任务调用只对自己所拥有的独立变量副本进行操作.因此可以得到正确的执行结果。
module auto_task;
reg [4:0] cd_add, ef_add;
reg [4:0] c, d , e, f;
reg clk1,clk2;
parameter delay=1;
initial
begin
clk1=0; clk2=0;
c=3;d=5;e=7;f=4;
#20 c=2;d=4;e=8;f=10;
#20 $stop;
end
initial forever #4 clk1=~clk1;
initial forever #5 clk2=~clk2;
task automatic adder; // 任务定义
output [4: 0] ab_adder;
input [4: 0] a, b;
begin
#delay ab_adder = a + b;
end
endtask
always @(posedge clk1)
adder(ef_add,e,f);
always @ (posedge clk2)
adder(cd_add, c, d);
endmodule
- 如果某一任务有可能在程序代码的两处被同时调用,建议最好使用自动任务。
3.函数function
-
函数的目的是返回一个用于表达式的值。函数的定义格式为:
function 返回值位宽或类型说明 函数名; 端口声明; 局部变量定义; //函数主体 begin 语句1; ..... 语句n; end endfunction
-
在这里,<返回值位宽或类型说明>这一项是可选项,如默认则返回值为一位寄存器类型数据。
function [ 7 : 0 ] getbyte; input[15:0] address; begin ……… getbyte = result_express; end endfunction
-
函数返回的值 : 函数的定义蕴含声明了与函数同名的、函数内部的寄存器。函数的定义把函数返回值所赋值寄存器的名称初始化为与函数同名的内部变量。并且这个与函数同名内部变量和返回值位宽或类型说明相一致。
-
函数的调用:函数的调用是通过将函数作为表达式中的操作数来实现的。其调用格式如下:
`函数名(表达式,,,表达式)``
下面的例子中,两次调用函数getbyte,把两次调用产生的值进行位拼接运算,以生成一个字。
word = control ? {getbyte(msbyte),getbyte(lsbyte)}:0;
-
下面是一个function的例子:
module alu(a,b,product,result); input[1:0] a,b; output[3:0] product,result; wire[1:0] a,b; reg[3:0] product,result; reg [7:0] all_result; always@(a,b) begin //注意调用函数的方法与任务不同; all_result = cal(a,b); product = all_result[7:4]; result = all_result[3:0]; end //定义函数cal function [7:0] cal; input[1:0] a; input[1:0] b; reg[3:0] temp; begin product = a*b; temp = a*a; result = temp-b; cal = {product,result}; end endfunction endmodule
-
下面是一个乘累加器(MAC)代码
module mac(out,a,b,clk,clr); output[15:0] out; input[7:0] a,b; input clk,clr; reg[15:0] out; wire[15:0] sum; assign sum = mult(a,b)+out; always@(posedge clk or posedge clr) begin if(clr) out <= 0; else out <= sum; end //定义函数mult; function[15:0] mult; input[7:0] a,b; reg[15:0] result; integer i; begin result = a[0] ? b:0; for(i=1;i<=7;i=i+1) begin if(a[i] == 1) result=result+(b<<i);//左移相加 end mult = result; end endfunction endmodule // b 1 1 0 // a 1 0 1 1 1 0 //相当于每次左移一位 0 0 0 1 1 0 —————————————— 1 1 1 1 0
-
函数与任务相比,其功能要弱。在函数中,可以嵌套函数,但不可以调用任务;而任务中,即可以调用函数,也可以调用任务。
函数中不允许出现延时和事件控制语句,即任何用#、@、或wait来标识的语句,也就是函数必须马上执行完,而任务可以在执行中挂起。
-
函数至少需要一个参数,且参数必须都为输入参数,不可以包含输出或双向端口。而任务可以有多种类型的变量(包括input、output或inout)。
函数必须有一个返回值,返回值被赋予和函数名同名的变量,这就决定函数只有一个返回值。
-
使用C语言风格进行变量声明的函数定义:
//定义偶校验位计算函数,该函数采用C风格的变量声明; function calc_parity( input[31:0] address) ; begin //适当地设置输出值。使用隐含的内部寄存器calc_parity calc_parity = ^address; end endfunction
递归函数
-
verilog中的函数是不能进行递归调用的;设计模块中若某函数在两个不同的地方被同时并发调用,由于这两个调用同时对同一块地址空问进行操作.那么计算结果将是不确定的。
若在函数声明时使用了关键字automatic,那么该函数将成为自动的或可递归的.
下例说明如何定义自动函数,来完成阶乘运算:
//用函数的递归调用定义阶乘计算 module top; .................. //定义自动(递归)函数 function automatic integer factorial; input [31 : 0] oper; begin if (oper >= 2) factorial = factorial( oper - 1) * oper;//递归调用 else factorial=1: end endfunction integer result; initial begin result = factorial (4); end endmodule
常量函数
-
常量函数是指在仿真开始之前的编译阶段计算出的结果为常数的函数,常量函数只允许操作常量,不允许访问全局变量或者调用系统函数。这种函数能够用来引用复杂的值,因此可用来代替常量。
下例中声明了一个常量函数,它可以用来计算模块中地址总线的宽度。
module ram(…); ………….. parameter ram_depth = 256; input [ clogb2(ram_depth)-1:0] addr_bus; ……………. //常量函数 function integer clogb2(input integer depth); begin for ( clogb2=0;depth>0;clogb2=clogb2+1) depth = depth >>1;//逻辑右移 end endfunction ………………….. endmodule
带符号的函数
-
Verilog-2001标准使使用了关键字 signed,使得寄存器数据类型、网络数据类型、端口以及函数都可以定义成带符号的类型。
下面举儿个例子说明:
reg signed [63:0] data;
wire signed [7:0] vector;
input signed [31:0] a;
function signed [127:0] alu;
-
Verilog1995标准中,没有指定基数(进制)的整型数被认为是有符号数,相反,指定了基数(进制)的整型数被认为是无符号数。Verilog2001标准增加了一个额外的标识符。字母’s’和基数标识符一起指定该数是带符号数。
举例说明如下:
16’hC501 //16位十六进制无符号数
16’shC501 //16位十六进制有符号数
-
除了可以定义有符号数据类型和数值外,Verilog2001还增加了两个新的系统函数,即
$signed
和$unsigned
。这两个系统函数可以将无符号数变换为带符号数,或相反。举例说明如下:
reg[63 : 0] a; //无符号数据类型 always@(a) begin result1 = a/2; //无符号运算 result2 = $signed(a) / 2; //有符号运算 end
-
Verilog-2001标准中,另一个关于带符号数运算的改进是算术移位操作符,右移和左移分别用符号>>>和<<<表示。算术右移操作不改变数值的符号,移位时,用符号位填充空缺位口。
例如,如果8位带符号变量D,
D= 8’b10100011
则D的3位的逻辑右移和算术右移的结果:
D>>3; //逻辑右移的结果是8’b00010100
D>>>3; //算术右移的结果是8’b11110100
小结
(1)任务和函数都用来对设计中多处使用的公共代码进行定义,使用任务和函数可以将模块分割成许多个可独立管理的子单元,增强了模块的可读性和可维护性。与C语言中的子程序起着相同的作用;
(2)任务可以有任意多个输入、输入/输出(inout)和输出变量。在任务中可以使用延迟、事件和时序控制结构,在任务中可以调用其他的任务和函数;
(3)自动任务使用关键字automatic进行定义,它的每一次调用都对不同的地址空间进行操作。因此,在被多次并发调用时仍然可以获得正确的结果;
(4)函数只能有一个返回值,并且至少要有一个输入变量。在函数中不能使用延迟、事件和时序控制结构。在函数可以调用其它函数,但不能调用任务;
(5)当声明函数时,Verilog仿真器都会隐含地声明一个同名的寄存器变量,函数的返回值通过这个寄存器传递回调用处;
(6)递归函数使用关键字automatic进行定义,递归函数每一次调用都拥有不同的地址空间。因此对这种函数的递归调用和并发调用可以得到正确的结果;
(7)任务和函数都包含在设计层次中,可以通过层次名对它们进行调用。
4.编译向导
-
Verilog HDL语言和C语言一样也提供了编译预处理功能。Verilog HDL允许在程序中使用特殊的编译预处理语句。在编译时,通常先对这些特殊语句进行“预处理”,然后再将预处理的结果和源程序一起进行编译。
预处理命令以符号“`”开头(注意,“`”不是单引号,叫反单引号,在键盘左上角数字1的左边),以区别于其他语句。
宏定义`define
-
`define语句用来将一个简单的名字或标志符(或称为宏名)来代表一个复杂的名字或字符中,其一般形式为:
define 标志符(宏名) 字符串
-
在程序中,引用宏的方法是在宏名前面加上符号“`”。
-
宏定义主要可以起到两个作用:
(1)用一个有意义的标识符取代程序中反复出现的含义不明显的字符串。例如:`define WORDSIZE 8 reg[ `WORDSIZE : 1 ] data; //这相当于定义 reg[8:1] data;
(2)用一个较短的标识符代替反复出现的较长的字符串。例如:
`define sum1 ina+inb+inc+ind module calculate( out1,out2,ina,inb,inc,ind,ine); input ina,inb,inc,ind,ine; output[2:0]out1,out2; wire ina,inb,inc,ind,ine; reg[2:0]out1,out2; always@(ina or inb or inc or ine) begin out1=`sum1+ine; out2=`sum1-ine; end endmodule
此外,在使用宏定义说明语句时候,还需注意:
(1)宏定义不是Verilog HDL语句,不必在行末加分号。如果加了分号会连分号一起进行置换。上题中如果把宏定义改为:
`define sum1 ina+inb+inc+ind;经过宏展开以后:
out1 = ina+inb+inc+ind;+ine;
out2 = ina+inb+inc+ind;-ine;
显然出现语法错(2)宏定义语句可以出现在程序中的任意位置。通常,``define`命令写在模块定义的外面,作为程序的一部分,在此程序内有效。如果对同一个宏名做了多次定义则只有最后一次定义生效。例如:
`define a 1 module muti_define; reg[1:0] out1,out2,out3; initial begin out1 = `a; //out1 = 1 `define a 2 out2 = `a; //out2 = 2 `define a 3 out3 = `a; //out3 = 3 end
-
在编译前,所有引用的宏被替换为宏内容,替换过程不做任何语法检查,所以使用宏的时候要小心。
-
在进行宏定义时,可以引用已定义的宏名,实现层层置换。如:
`define aa a + b `define cc c + `aa module test; reg a, b, c,d; wire out; assign out =`cc+d; endmodule //这样经过宏展开以后,assign语句为 //assign out = c + a + b+d;
-
例:带有宏定义的8位加法器
`define DataWidth 8 module addr8(a,b,c,d,sum); input [`DataWidth-1:0] a,b,c,d; output[`DataWidth :0] sum; wire [`DataWidth-1:0] a,b,c,d; reg [`DataWidth :0] sum; always@(a or b or c or d) begin sum = a+b+c+d; end endmodule
文件包含语句 `include
-
使用Verilog HDL设计数字系统时,一个设计可能包含很多模块,而每个模块都单独存为一个文件。
当顶层模块调用子模块时,就需要到相应的文件中寻找,文件包含的作用就是指明这些文件的位置。
也可以将宏定义、任务或者函数等语句写在单独的文件中,通过文件包含供其他模块调用。
-
``include`是文件包含语句,它可将一个文件全部包含到另一个文件中。其一般形式为:
``include “文件名”`
例用`include语句设计的16位加法器:
addr.v文件的代码为: module addr(cout,sum,a,b,cin); parameter size =16; output cout; output[size-1:0] sum; input cin; input[size-1:0] a, b; assign {cout,sum} = a+b+cin; endmodule //调用文件addr.v中模块addr完成16位加法器 `include "addr.v" module addr16(cout,sum,a,b,cin); parameter MySize =16; output cout; output[MySize-1:0] sum; input[MySize-1:0] a, b; input cin; addr #(MySize) myAddr(cout,sum,a,b,cin); endmodule
条件编译指令 `ifdef `else `endif
-
条件编译指令:
ifdef、
else、`endif;
根据环境需要对一部分代码有选择地进行编译。
条件编译有两种表达形式:// 第一种形式: `ifdef 宏名 程序段 `endif //第二种形式 `ifdef 宏名 程序段1 `else 程序段2 `endif
`define sum a+b
module condition_compile(out,a,b,c);
output[2:0] out;
input a,b,c;
`ifdef sum
assign out=sum+c;
`else
assign out=a+c;
`endif
endmodule
//在上面的例子中,因为定义了“`define sum”,
//所以程序执行“assign out=a+b+c;”
-
例条件编译语句的使用:
//share.v `define SHARE module share; …… endmodule //child1.v `ifdef SHARE `else `include “share.v”//如果没定义,就把这个文件包含进来 `endif `define CHILD1 module child1 share share1; ……… endmodule //child2.v `ifdef SHARE `else `include”share.v” `endif `define CHILD2 module child2 share share2; ……… endmodule //father.v //检查文件是否包含 `ifdef CHILD1 `else `include”child1.v” `endif `ifdef CHILD2 `else `include”child2.v” `endif `define FATHER module father child1 ch1; child2 ch2; …… endmodule
时间尺度 `timescale
-
`timescale语句用于定义模块的时间单位和时间精度,其使用格式如下:
`timescale 时间单位/时间精度
用于时间单位和时间精度的数字只能是1、10和100。 其中用来表示时间度量的符号有:
s、ms、μs 、ns、ps和fs 。
-
时间精度是指模块仿真时间和延时的精确程度,比如定义时间精度为10ns,那么程序中所有的延时至多能精确到10ns。
`timescale 100ns/10ns ………… always @(din) fork #3 dout1 = din; //延时300ns #3.1 dout2 = din; //延时310ns #3.14 dout3 = din; //延时310ns join
小结
- 在用`timescale时.需要注意的是,当多个带不同`timescale定义的模块包含在一起时只有最后一个才起作用。所以属于一个项目,但`timescale定义不同的多个模块最好分开编译,不要包含在一起编译,以免把时间单位搞混。
- 宏定义字符串引用时,不要忘记要用“`”引导。这与C语言不不同。
include等编译预处理也必须用“`”引导,而不是与C语言一样用“#”引导或不需要引导符。 - 合理地使用条件编译和条件执行预处理可以使测试程序适应不同的编译环境,也可以把不同的测试过程编写到一个统一的测试程序中去,间以简化测试的过程,对于复杂设计的验证模块的编写很有实用价值。