verilog task/function 语句

发布时间 2023-07-14 15:41:31作者: luckylan

task模块

任务task在模块中任意位置定义,并在模块内任意位置引用,作用范围也局限于此模块。

模块内子程序出现下面任意一个条件时,则必须使用任务而不能使用函数。

  • 1)子程序中包含时序控制逻辑,例如延迟,事件控制等
  • 2)没有输入变量
  • 3)没有输出或输出端的数量大于 1
//任务task定义:如下代码是用task完成一个4bit全加器的代码
//--------------- SPI task ------------------/
module EXAMPLE (A, B, CIN, S, COUT); 
input [3:0] A, B; 
input CIN; 
output reg [3:0] S; 
output reg COUT; 
reg [1:0] S0, S1, S2, S3; 

task ADD; 
input A, B, CIN;     output reg [1:0] C; 
reg S, COUT; 
  begin
S = A ^ B ^ CIN; 
COUT = (A&B) | (A&CIN) | (B&CIN); 
C = {COUT, S}; 
end 
endtask 

// 调用任务
always @(A or B or CIN) begin 
ADD (A[0], B[0], CIN, S0); 
ADD (A[1], B[1], S0[1], S1); 
ADD (A[2], B[2], S1[1], S2); 
ADD (A[3], B[3], S2[1], S3);     
S = {S3[0], S2[0], S1[0], S0[0]}; 
COUT = S3[1]; 
end 
endmodule

任务就是一段封装在“task-endtask”之间的程序。任务是通过调用来执行的,而且只有在调用时才执行,如果定义了任务,但是在整个过程中都没有调用它,那么这个任务是不会执行的。调用某个任务时可能需要它处理某些数据并返回操作结果,所以任务应当有接收数据的输入端和返回数据的输出端。另外,任务可以彼此调用,而且任务内还可以调用函数。 

1. 任务(task)类似于一般编程语言中的Process(过程),它可以从描述的不同位置执行共同的代码。通常把需要共用的代码段定义为task,然后通过task调用来使用它。在task中还可以调用其他的task和function。

task的定义

task<任务名>;
    端口与类型说明;
    变量声明;
    语句1;
    …
    语句n;
endtask

task  task_id ;
    port_declaration ;
    procedural_statement ;
endtask

其中,关键词 task 和 endtask 将它们之间的内容标志成一个任务定义,task 标志着一个任务定义结构的开始;task关键字后跟的是任务名;

任务中使用关键字 input、output 和 inout 对端口进行声明。input 、inout 型端口将变量从任务外部传递到内部,output、inout 型端口将任务执行完毕时的结果传回到外部。

变量声明语句:变量(包括输入和输入输出)可以作为寄存器声明,如果它们没有被明确声明,变量就作为与对应的变量有相同范围的reg声明;

可以把 input 声明的端口变量看做 wire 型,把 output 声明的端口变量看做 reg 型。但是不需要用 reg 对 output 端口再次说明。

对 output 信号赋值时也不要用关键字 assign。为避免时序错乱,建议 output 信号采用阻塞赋值。

语句1;…语句n:是一段用来完成这个任务操作的过程语句,如果过程语句多于一条,应将其放在begin end语句块内;

endtask 为任务定义结构体结束标志。

task xor_oper_iner;
    input [N-1:0]   numa;
    input [N-1:0]   numb;
    output [N-1:0]  numco ;
    //output reg [N-1:0]  numco ; //无需再注明 reg 类型,虽然注明也可能没错
    #3  numco = numa ^ numb ;
    //assign #3 numco = numa ^ numb ; //不用assign,因为输出默认是reg
endtask

任务在声明时,也可以在任务名后面加一个括号,将端口声明包起来。上述设计可以更改为:

task xor_oper_iner(
    input [N-1:0]   numa,
    input [N-1:0]   numb,
    output [N-1:0]  numco  ) ;
    #3  numco       = numa ^ numb ;
endtask

在定义一个task时,必须注意以下几点:

(1):  任务定义结构不能出现在任何一个过程块内;

(2):  一个task可以没有输入/输出端口,当然也可以有;

(3):  一个task可以没有返回值,也可以通过输出端口或双向端口返回一个或多个值;

(4):  除任务参数外,task还能够引用说明任务的模块中定义的任何变量;

2.3.2 task的调用

任务可单独作为一条语句出现在 initial 或 always 块中,调用格式如下:

task调用语句给出传入任务的参数值和接收结果的变量值,
 <任务名>  (端口1,端口2,... ,端口n);

端口 1、端口 2,…是参数列表。参数列表给出传入任务的数据(进入任务的输入端)和接收返回结果的变量(从任务的输出端接收返回结果)

task_id(input1, input2, …,outpu1, output2, …);

在调用task时,必须注意一下几点:

(1):  task调用是过程性语句,因此只能出现在always过程块和initial过程块中; 

(2):  当被调用输入、输出或双向端口时,任务调用语句必须包含端口名列表,task调用语句中的列表必须与任务定义时的输入、输出和双向端口参数说明的顺序相匹配,端口必须按顺序对应。

(3): 输入端连接的模块内信号可以是 wire 型,也可以是 reg 型。输出端连接的模块内信号要求一定是 reg 型,这点需要注意。

(4):  在调用task时,参数要按值传递,而不能按地址传递(和其他语言的不同);  

(5):  在一个task中,也可直接访问上一级调用模块中的任何寄存器;  

(6):  可以使用循环中断控制语句disable来中断任务执行,但其是不可综合的。在task被中断后,程序流程将返回到调用task调用的地方继续向下执行。   

(7):  在第一行“task”语句中不能列出端口名称;

(8):  任务的输入、输出端口和双向端口数量不受限制,甚至可以没有输入、输出以及双向端口。

(9):  在任务定义的描述语句中,可以使用不可综合的语句(使用最为频繁的就是延迟控制语句),但这样会造成该任务不可综合。

(10):  在任务中可以调用其他的任务或函数,也可以调用自身。

(11):  在任务定义结构内不能出现 initial和 always过程块。

(12):  任务调用语句和一条普通的行为描述语句的处理方法一致;

(13):  可综合任务只能实现组合逻辑,也就是说调用可综合任务的时间为“0” 。而在面向仿真的任务中可以带有时序控制,如时延,因此面向仿真的任务的调用时间不为“0” 。

实例:对上述异或功能的 task 进行一个调用,完成对异或结果的缓存。

module xor_oper
    #(parameter         N = 4)
     (
      input             clk ,
      input             rstn ,
      input [N-1:0]     a ,
      input [N-1:0]     b ,
      output [N-1:0]    co  ); 
    reg [N-1:0]          co_t ;
    always @(*) begin          //任务调用
        xor_oper_iner(a, b, co_t);
    end
    reg [N-1:0]          co_r ;
    always @(posedge clk or negedge rstn) begin
        if (!rstn) begin
            co_r   <= 'b0 ;
        end
        else begin
            co_r   <= co_t ;         //数据缓存
        end
    end
    assign       co = co_r ; 
   /*------------ task -------*/
    task xor_oper_iner;
        input [N-1:0]   numa;
        input [N-1:0]   numb;
        output [N-1:0]  numco ;
        #3  numco       = numa ^ numb ;   //阻塞赋值,易于控制时序
    endtask
 
endmodule

对上述异或功能设计进行简单的仿真,testbench 描述如下。

激励部分我们使用简单的 task 进行描述,激励看起来就更加的清晰简洁。其实,task 最多的应用场景还是应用于 testbench 中进行仿真。task 在一些编译器中也不支持综合。

`timescale 1ns/1ns 
module test ;
    reg          clk, rstn ;
    initial begin
        rstn      = 0 ;
        #8 rstn   = 1 ;
        forever begin
            clk = 0 ; # 5;
            clk = 1 ; # 5;
        end
    end
 
    reg  [3:0]   a, b;
    wire [3:0]   co ;
    initial begin
        a         = 0 ;
        b         = 0 ;
        sig_input(4'b1111, 4'b1001, a, b);
        sig_input(4'b0110, 4'b1001, a, b);
        sig_input(4'b1000, 4'b1001, a, b);
    end
    task sig_input ;
        input [3:0]       a ;
        input [3:0]       b ;
        output [3:0]      ao ;
        output [3:0]      bo ;
        @(posedge clk) ;
        ao = a ;
        bo = b ;
    endtask ; // sig_input
 
    xor_oper         u_xor_oper
    (
      .clk              (clk  ),
      .rstn             (rstn ),
      .a                (a    ),
      .b                (b    ),
      .co               (co   ));
    initial begin
        forever begin
            #100;
            if ($time >= 1000)  $finish ;
        end
    end 
endmodule // test

仿真结果如下。

由图可知,异或输出逻辑结果正确,相对于输入有 3ns 的延迟。

且连接信号 a,b,co_t 与任务内部定义的信号 numa,numb,numco 状态也保持一致。

2.3.3 任务操作全局变量

因为任务可以看做是过程性赋值,所以任务的 output 端信号返回时间是在任务中所有语句执行完毕之后。

任务内部变量也只有在任务中可见,如果想具体观察任务中对变量的操作过程,需要将观察的变量声明在模块之内、任务之外,可谓之"全局变量"。

例如有以下 2 种尝试利用 task 产生时钟的描述方式。

//way1 to decirbe clk generating, not work
task clk_rvs_iner ;
        output    clk_no_rvs ;
        # 5 ;     clk_no_rvs = 0 ;
        # 5 ;     clk_no_rvs = 1 ;
endtask
reg          clk_test1 ;
always clk_rvs_iner(clk_test1);
//way2: use task to operate global varialbes to generating clk
reg          clk_test2 ;
task clk_rvs_global ;
        # 5 ;     clk_test2 = 0 ;
        # 5 ;     clk_test2 = 1 ;
endtask // clk_rvs_iner
always clk_rvs_global;

仿真结果如下。

第一种描述方式,虽然任务内部变量会有赋值 0 和赋值 1 的过程操作,但中间变化过程并不可见,最后输出的结果只能是任务内所有语句执行完毕后输出端信号的最终值。所以信号 clk_test1 值恒为 1,此种方式产生不了时钟。

第二种描述方式,虽然没有端口信号,但是直接对"全局变量"进行过程操作,因为该全局变量对模块是可见的,所以任务内信号翻转的过程会在信号 clk_test2 中体现出来。

automatic 任务

和函数一样,Verilog 中任务调用时的局部变量都是静态的。可以用关键字 automatic 来对任务进行声明,那么任务调用时各存储空间就可以动态分配,每个调用的任务都各自独立的对自己独有的地址空间进行操作,而不影响多个相同任务调用时的并发执行。

如果一任务代码段被 2 处及以上调用,一定要用关键字 automatic 声明。

当没有使用 automatic 声明任务时,任务被 2 次调用,可能出现信号间干扰,例如下面代码描述:

task test_flag ;
        input [3:0]       cnti ;
        input             en ;
        output [3:0]      cnto ;
        if (en) cnto = cnti ;
endtask

reg          en_cnt ;
reg [3:0]    cnt_temp ;
initial begin
        en_cnt    = 1 ;
        cnt_temp  = 0 ;
        #25 ;     en_cnt = 0 ;
end
always #10 cnt_temp = cnt_temp + 1 ;

reg [3:0]             cnt1, cnt2 ;
always @(posedge clk) test_flag(2, en_cnt, cnt1);       //task(1)
always @(posedge clk) test_flag(cnt_temp, !en_cnt, cnt2);//task(2)

仿真结果如下。

en_cnt 为高时,任务 (1) 中信号 en 有效, cnt1 能输出正确的逻辑值;

此时任务 (2) 中信号 en 是不使能的,所以 cnt2 的值被任务 (1) 驱动的共用变量 cnt_temp 覆盖。

en_cnt 为低时,任务 (2) 中信号 en 有效,所以任务 (2) 中的信号 cnt2 能输出正确的逻辑值;而此时信号 cnt1 的值在时钟的驱动下,一次次被任务 (2) 驱动的共用变量 cnt_temp 覆盖。

可见,任务在两次并发调用中,共用存储空间,导致信号相互间产生了影响。

其他描述不变,只在上述 task 声明时加入关键字 automatic,如下所以。

task automatic test_flag ;

此时仿真结果如下。

  • en_cnt 为高时,任务 (1) 中信号 cnt1 能输出正确的逻辑值,任务 (2) 中信号 cnt2 的值为 X;
  • en_cnt 为低时,任务 (2) 中信号 cnt2 能输出正确的逻辑值,任务 (1) 中信号 cnt1 的值为 X;

可见,任务在两次并发调用中,因为存储空间相互独立,信号间并没有产生影响。

function模块

//函数function定义
//调用函数举例:assign multiout = DWF_mult_tc(multin_a,multin_b);

function[A_width+B_width-1 : 0] DWF_mult_tc;
// 执行2的补码乘法 
input [A_width-1:0] A; 
input [B_width-1:0] B; 
reg sign;// 局部变量声明
begin
// synopsys translate_off
  sign = A[A_width-1] ^ B[B_width-1];  
//生成输出的符号位    //将两个乘数A和B(补码表示)转为无符号数
if (A[A_width-1] == 1'b1) A = ~A + 1'b1;    
if (B[B_width-1] == 1'b1) B = ~B + 1'b1; 
DWF_mult_tc = A * B;  //执行无符号乘法
if (sign == 1'b1) //输出转为2的补
DWF_mult_tc = ~DWF_mult_tc + 1'b1;                                       
// synopsys translate_on 
end
endfunction

function的定义

function的定义:
    function<返回值类型和位宽> <函数名>
      <入口参量和类型声明>
      <局部变量声明>
      行为语句;
       endfunction
function [range-1:0]     function_id ;
    input_declaration ;
    other_declaration ;
    procedural_statement ;
endfunction

函数在声明时,会隐式的声明一个宽度为 range、 名字为 function_id 的寄存器变量,函数的返回值通过这个变量进行传递。当该寄存器变量没有指定位宽时,默认位宽为 1。

函数通过指明函数名与输入变量进行调用。函数结束时,返回值被传递到调用处。

函数在声明时,也可以在函数名后面加一个括号,将 input 声明包起来。例如下面声明的函数格式:

function [N-1:0]     data_rvs(
input     [N-1:0] data_in 
    ......
    ) ;

定义function时,要注意以下几点:

(1):至少有一个输入变量input,可以没有任何输出output或输入输出inout;

(2):通过赋值函数名,函数可以返回一个值,就像是寄存器一样;函数不能使能任务,函数不能被禁能;函数可以调用其他函数,但是不能调用任务

(3):函数不包含定时控制,延时事件控制或等待;不含有非阻塞赋值语句

(4):  function定义结构不能出现在任意一个过程块(always块或者initial块)的内部;

(5):  function定义不能包括有任何时间控制语句,即任何用#,@或wait来标识的语句;  

(6):  定义function时至少要有一个输入参量;

(7):  定义function时,在function内部隐式地将函数名声明成一个寄存器变量,在函数体中必须有一条赋值语句对该寄存器变量赋以函数的结果值,以便调用function时能够得到返回的函数值。如果没有指定的返回值的宽度,function将缺省返回1位二进制数。

function的调用

function的调用:

<函数名>  (<输入表达式1>,...,<输入表达式n>) ;

function_id(input1, input2, …);

输入表达式与函数定义结构中的各个输入端口一一对应,这些输入表达式的排列顺序必须与各个输入端口在函数定义结构中的排列顺序一致。

function的调用既可以出现在过程块中又可以出现在assign连续赋值语句之中;另外,function定义中声明的所有局部变量寄存器都是静态的,即function中的局部寄存器在function的多个调用之间保持他们的值。

下面用函数实现一个数据大小端转换的功能。当输入为 4'b0011 时,输出可为 4'b1100。例如:

module endian_rvs
    #(parameter N = 4)
        (
            input             en,     //enable control
            input [N-1:0]     a ,
            output [N-1:0]    b
    );         
        reg [N-1:0]          b_temp ;
        always @(*) begin
        if (en) begin
                b_temp =  data_rvs(a);
            end
            else begin
                b_temp = 0 ;
            end
    end
        assign b = b_temp ;     
    //function entity
        function [N-1:0]     data_rvs ;
            input     [N-1:0] data_in ;
            parameter         MASK = 32'h3 ;
            integer           k ;
            begin
                for(k=0; k<N; k=k+1) begin
                    data_rvs[N-k-1]  = data_in[k] ;  
                end
            end
    endfunction         
endmodule   

常数函数

常数函数是指在仿真开始之前,在编译期间就计算出结果为常数的函数。常数函数不允许访问全局变量或者调用系统函数,但是可以调用另一个常数函数。

这种函数能够用来引用复杂的值,因此可用来代替常量。

例如下面一个常量函数,可以来计算模块中地址总线的宽度:

parameter    MEM_DEPTH = 256 ;
reg  [logb2(MEM_DEPTH)-1: 0] addr ; //可得addr的宽度为8bit
    function integer     logb2;
    input integer     depth ;
        //256为8bit,我们最终数据应该是8,所以需depth=2时提前停止循环
    for(logb2=0; depth>1; logb2=logb2+1) begin
        depth = depth >> 1 ;
    end
endfunction

2.4.4 automatic 函数

在 Verilog 中,一般函数的局部变量是静态的,即函数的每次调用,函数的局部变量都会使用同一个存储空间。若某个函数在两个不同的地方同时并发的调用,那么两个函数调用行为同时对同一块地址进行操作,会导致不确定的函数结果。

Verilog 用关键字 automatic 来对函数进行说明,此类函数在调用时是可以自动分配新的内存空间的,也可以理解为是可递归的。因此,automatic 函数中声明的局部变量不能通过层次命名进行访问,但是 automatic 函数本身可以通过层次名进行调用。

下面用 automatic 函数,实现阶乘计算:

wire [31:0]          results3 = factorial(4);
function automatic   integer         factorial ;
    input integer     data ;
    integer           i ;
    begin
        factorial = (data>=2)? data * factorial(data-1) : 1 ;
    end
endfunction // factorial

下面是加关键字 automatic 和不加关键字 automatic 的仿真结果。

由图可知,信号 results3 得到了我们想要的结果,即 4 的阶乘。

而信号 results_noauto 值为 1,不是可预知的正常结果。

task和function区别  

和函数一样,任务(task)可以用来描述共同的代码段,并在模块内任意位置被调用,让代码更加的直观易读。函数一般用于组合逻辑的各种转换和计算,而任务更像一个过程,不仅能完成函数的功能,还可以包含时序控制逻辑。下面对任务与函数的区别进行概括:

(1):  function只能与主模块共用一个仿真时间单位,而task可以定义自己的仿真时间单位;   

(2):  function不能调用任务,而task可以调用函数;  

(3):  function至少需要一个输入变量,而task可以没有或者有很多个任意类型的变量;

(4):  function返回一个值,而task则不返回值。

比较点函数任务
输入 函数至少有一个输入,端口声明不能包含 inout 型 任务可以没有或者有多个输入,且端口声明可以为 inout 型
输出 函数没有输出 任务可以没有或者有多个输出
返回值 函数至少有一个返回值 任务没有返回值
仿真时刻 函数总在零时刻就开始执行 任务可以在非零时刻执行
时序逻辑 函数不能包含任何时序控制逻辑 任务不能出现 always 语句,但可以包含其他时序控制,如延时语句
调用 函数只能调用函数,不能调用任务 任务可以调用函数和任务
书写规范 函数不能单独作为一条语句出现,只能放在赋值语言的右端 任务可以作为一条单独的语句出现语句块中