Verilog语法之任务Task与函数Function

本文转载自:孤独的单刀的CSDN博客

1、概述

        与C语言中的函数类似,在Verilog代码中,通过把代码分成小的模块或者使用任务(task)和函数(function),可把一项任务分成许多较小的、易于管理的部分,从而提高代码的可读性、可维护性和可重用性。

        任务(task):一般用于编写测试模块,或者行为描述的模块。其中可以包含时间控制(如:# delays, @, wait);也可以包含input, output 、inout 端口定义和参数;也可以调用其他的任务或函数。任务可以包含时间控制,但加入时间控制后则该部分无法综合,所以任务常用于testbench测试模块。

        函数(function):一般用于计算,或者用来代替组合逻辑。不能包含任何延迟;函数在零时间执行。函数只有input变量,虽然没有output变量,但可以通过函数名返回一个值。可以调用其他的函数,但不可以调用任务。函数不可以包含时间控制,所以可综合,多用来模块化组合逻辑,方便复用、调用。

        Tips:

  • 只能调用本模块内的任务和函数。

  • 在任务和函数中不能声明网络连接类型的变量。

  • 所有的输入和输出变量实际上都是本地寄存器 。

  • 只有当任务或函数调用并执行完后,才能有返回值。例如:若任务或函数中包含一个forever循环时,永远无法执行完,就不可能有返回值。

2、任务 task

        如果传给任务的变量值和任务完成后接收结果的变量已定义,就可以用一条语句启动任务。任务完成以后控制就传回启动过程。如任务内部有定时控制,则启动的时间可以与控制返回的时间不同。

        任务可以启动其它任务,其它任务又可以启动别的任务,可以启动的任务数是没有限制的,不管有多少任务启动,只有当所有的启动任务完成以后,控制才能返回。

2.1、任务的定义

        任务的语法为:

task    <任务名>;
          <端口及数据类型声明语句>
          <语句1>
          <语句2>
          ...
          <语句n>
endtask

调用任务并传递输入/输出变量的声明语句的语法如下:

<任务名> (端口1,端口2,...,端口n);

任务定义如下所示:

task    my_task;
    input    a,b;
    input    c;
    output    d,e;
 
    ...
    <语句>    //执行任务工作相应的语句
    ...
    c = foo1;    //赋初始值
    d = foo2;    //对任务的输出变量赋值
    e = foo3;    //
endtask

任务调用为: 

my_task(v,w,x,y,z);

        任务调用变量(v,w,x,y,z)和任务定义的I/O变量(a,b,c,d,e)之间是一一对应的。当任务启动时,由v,w和x传入的变量赋给了a,b和c。当任务完成后的输出又通过c,d和e赋给x,y和z。 

任务的使用有以下需要注意:

  1. 任务中可以有时间控制语句(此时无法被综合)。

  2. 任务可以没有或可以有一个或多个输入、输出和双向端口。

  3. 传递给任务的变量与任务I/O端口变量的声明次序需要相同。

  4. 任务可以没有返回值,也可以通过输出端口或双向端口返回一个或多个返回值。

  5. 任务可以调用其它的任务或函数,也可以调用该任务本身。

  6. 任务定义结构内不允许出现过程块(initial或always过程块)。

  7. 关键字disable可以用来禁止任务的执行。

  8. 不要在程序的不同部分同时调用同一个任务。这是因为任务只有一组本地变量,同一时刻调用两次相同的任务将会导致错误。这种情况常发生在使用定时控制的任务中。

  9. 在任务或函数中,引用父模块中声明的变量时要特别注意(即注意变量的层次命名规则)。若想在其它模块中调用任务或函数,该任务和函数中所使用的变量必须全都包含在输入/输出口列表中。

2.2、一个task例子

        下面的模块描述了交通灯的运作,其中定义和调用了任务。

module    traffic_lights;
 
    reg clock,red,amber,green;
    parameter on =1, off = 0, red_tics = 350,amber_tics = 30,green_tics = 200;
    initial    red = off;
    initial    amber = off;
    initial    green = off;
  
always                              //交通灯初始化
    begin
        red = on;                   //开红灯
        light(red,red_tics);        //调用等待任务
        green = on;        //开绿灯
        light(green,green_tics);    //等待
        amber = on;                 //开黄灯
        light(amber,amber_tics);    //等灯
    end
 
always    begin        //产生时钟脉冲的always块
    #100 clock = 0;
    #100 clock = 1;
end
 
//定义交通灯开启时间的任务
task    light(color,tics);          
    output    color;
    input    [31:0] tics;
    begin
        repeat(tics) @(posedge clock);    //等待tics个时钟的上升沿
        color = off;
    end
endtask
 
endmodule

        任务light的作用就是根据输入tics,来决定在多少个tics的时钟上升沿后将输入color赋值为0,即模仿红绿黄灯的不同持续时间。

        在这里只是一个行为模块,由于其使用了repeat这个时间控制语句,所以该模块是无法综合成实际电路的。当然了,这里的用途本来也就是作为测试手段。

3、函数 function

        函数的目的是通过返回一个用于某表达式的值,来响应输入信号。适于对不同变量采取同一运算的操作。函数在模块内部定义,通常在本模块中调用,也能根据按模块层次分级命名的函数名从其他模块调用。

3.1、函数的定义

        函数的语法为:

function    <返回值的类型或范围> (函数名)
    <端口说明语句>
    <变量类型说明语句> begin
    <语句>
    ...
    end
 
endfunction

        在这里,<返回值的类型或范围>这一项是可选项,如缺省则返回值为一位寄存器类型数据。例如:

function    [7:0] getbyte;
 
    input    [15:0] address;
    begin
        <说明语句>                                 //从地址字节中提取低字节的程序
        getbyte = result_expression;      //把结果赋予函数的返回字节
    end
 
endfunction

        函数的定义蕴含声明了与函数同名的、函数内部的寄存器。如在函数的声明语句中<返回值的类型或范围>为缺省,则这个寄存器是一位;否则是与函数定义中<返回值的类型或范围>一致的寄存器。

        函数的定义把函数返回值赋值寄存器的名称初始化为与函数同名的内部变量。上面的例子说明这个概念:getbyte被赋予的值就是函数的返回值。

        函数的调用是通过将函数作为表达式中的操作数来实现的,其调用格式为:

 <函数名>    (<表达式><,<表达式>>*)

        其中函数名作为确认符。例如通过对两次调用函数getbyte的结果进行位拼接运算来生成一个字。

word = control?{getbyte(msbyte),getbyte(lsbyte)}:0;

函数的使用有以下需要注意: 

  1. 函数的定义不能包含有任何的时间控制语句,也不允许使用disable终止语句。

  2. 在函数的定义中必须有一条赋值语句给函数中的一个内部变量赋以函数的结果值,该内部变量具有和函数名相同的名字

  3. 函数在一个仿真时间单位内执行完毕,因此不能包含任务、不能使用非阻塞赋值

  4. 与任务一样,函数定义结构只能出现在模块中,而不能出现在过程块内。

  5. 函数至少必须有一个输入端口。

  6. 函数不能有任何类型的输出端口(output端口)和双向端口(inout端口)。

  7. 在一个函数内可以对其它函数进行调用,但是函数不能调用其它任务。

  8. 在函数声明的时候,在Verilog HDL的内部隐含地声明了一个名为function_identifier(函数标识符)的寄存器类型变量,函数的输出结果将通过这个寄存器类型变量被传递回来。

3.2、一个function例子

        在Xilinx的许多源码都出现了这个简单的Function,其功能时:以2为底取对数。

//function 实现 
function integer clogb2 (input integer bit_depth);              
begin                                                           
for(clogb2=0; bit_depth>0; clogb2=clogb2+1)                   
bit_depth = bit_depth >> 1;                                 
end                                                           
endfunction                                                     
 
// 使用案例
localparam integer C_TRANSACTIONS_NUM = clogb2(C_M_AXI_BURST_LEN-1); 
reg [C_TRANSACTIONS_NUM : 0] write_index;
reg [C_TRANSACTIONS_NUM : 0] read_index;

        上面的代码就是定义了一个求位宽的function,用其求得某类寄存器的位宽,然后再对寄存器赋值时就直接使用求得的位宽来赋值,这样复用起来就比较方便了。

4、任务与函数的异同

不同点:

  1. 函数只能与主模块共同用一个仿真时间单位;任务可以定义自己的仿真时间单位

  2. 函数不能启动任务;任务能启动其它任务和函数

  3. 函数至少要有一个输入变量,返回一个值;任务可以没有或有多个任何类型的变量,不返回值

  4. 函数通常用于计算,或描述组合逻辑;任务通常用于调试,或对硬件进行行为描述

  5. 函数不能包含任何时间延迟;任务能包含时间延迟

共同点:

  1. 任务和函数必须在module内调用

  2. 在任务和函数中不能声明wire

  3. 所有输入输出都是局部寄存器

  4. 任务函数执行完成后才返回结果

5、总结与参考

  • 任务与函数的均可用于简化大型模块设计,化零为整

  • 任务可以加入时间控制语句,所以多用于测试模块

  • 函数不可以键入时间控制语句,所以多用于RTL的组合逻辑替代

参考资料1:IEEE Standard for Verilog® Hardware Description Language(IEEE Std 1364™-2005)

参考资料2:Verilog数字系统设计教程(夏宇闻、韩彬)


最新文章

最新文章