当前位置: 首页 > 教程 > Verilog >

6.1 Verilog 函数

精华
小牛编辑
168浏览
2023-03-14

关键词:函数,大小端转换,数码管译码

在 Verilog 中,可以利用任务(关键字为 task)或函数(关键字为 function),将重复性的行为级设计进行提取,并在多个地方调用,来避免重复代码的多次编写,使代码更加的简洁、易懂。

函数

函数只能在模块中定义,位置任意,并在模块的任何地方引用,作用范围也局限于此模块。函数主要有以下几个特点:

  • 1)不含有任何延迟、时序或时序控制逻辑
  • 2)至少有一个输入变量
  • 3)只有一个返回值,且没有输出
  • 4)不含有非阻塞赋值语句
  • 5)函数可以调用其他函数,但是不能调用任务

Verilog 函数声明格式如下:

function [range-1:0]     function_id ;
input_declaration ;
 other_declaration ;
procedural_statement ;
endfunction

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

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

函数调用格式如下:

function_id(input1, input2, …);

下面用函数实现一个数据大小端转换的功能。

当输入为 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 = 3 2'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        

函数里的参数也可以改写,例如:

defparam data_rvs.MASK = 32'd7 ;

但是仿真时发现,此种写法编译可以通过,仿真结果中,函数里的参数 MASK 实际并没有改写成功,仍然为 32'h3。这可能和编译器有关,有兴趣的学者可以用其他 Verilog 编译器进行下实验。

函数在声明时,也可以在函数名后面加一个括号,将 input 声明包起来。

例如上述大小端声明函数可以表示为:

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

常数函数

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

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

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

实例

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

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,不是可预知的正常结果,这里不再做无用分析。

数码管译码

上述中涉及的相关函数知识似乎并没有体现出函数的优越性。下面设计一个 4 位 10 进制的数码管译码器,来说明函数可以简化代码的优点。

下图是一个数码管的实物图,可以用来显示 4 位十进制的数字。在比赛计分、时间计时等方面有着相当广泛的应用。

每位数码显示端有 8 个光亮控制端(如图中 a-g 所示),可以用来控制显示数字 0-9 。

而数码管有 4 个片选(如图中 1-4),用来控制此时哪一位数码显示端应该选通,即应该发光。倘若在很短的时间内,依次对 4 个数码显示端进行片选发光,同时在不同片选下给予不同的光亮控制(各对应 4 位十进制数字),那么在肉眼不能分辨的情况下,就达到了同时显示 4 位十进制数字的效果。

下面,我们用信号 abcdefg 来控制光亮控制端,用信号 csn 来控制片选,4 位 10 进制的数字个十百千位分别用 4 个 4bit 信号 single_digit, ten_digit, hundred_digit, kilo_digit 来表示,则一个数码管的显示设计可以描述如下:

实例

module digital_tube
      (
      input             clk ,
      input             rstn ,
      input             en ,
 
      input [ 3 : 0 ]       single_digit ,
      input [ 3 : 0 ]       ten_digit ,
      input [ 3 : 0 ]       hundred_digit ,
      input [ 3 : 0 ]       kilo_digit ,
 
      output reg [ 3 : 0 ]  csn , //chip select, low-available
      output reg [ 6 : 0 ]  abcdefg         //light control
      ) ;
 
    reg [ 1 : 0 ]            scan_r ;   //scan_ctrl
    always @ ( posedge clk or negedge rstn ) begin
        if ( !rstn ) begin
            csn             <= 4'b1111 ;
            abcdefg         <= 'd0 ;
            scan_r         <= 3'd0 ;
        end
        else if (en ) begin
            case (scan_r )
            2'd0 : begin
                scan_r     <= 3'd1 ;
                csn       <= 4'b0111 ;     //select single digit
                abcdefg   <= dt_translate (single_digit ) ;
            end
            2'd1 : begin
                scan_r     <= 3'd2 ;
                csn       <= 4'b1011 ;     //select ten digit
                abcdefg   <= dt_translate (ten_digit ) ;
            end
            2'd2 : begin
                scan_r     <= 3'd3 ;
                csn       <= 4'b1101 ;     //select hundred digit
                abcdefg   <= dt_translate (hundred_digit ) ;
            end
            2'd3 : begin
                scan_r     <= 3'd0 ;
                csn       <= 4'b1110 ;     //select kilo digit
                abcdefg   <= dt_translate (kilo_digit ) ;
            end
            endcase
        end
    end
 
    /*------------ translate function -------*/
    function [ 6 : 0 ] dt_translate ;
        input [ 3 : 0 ]   data ;
        begin
        case (data )
            4'd0 : dt_translate = 7'b1111110 ;     //number 0 -> 0x7e
            4'd1 : dt_translate = 7'b0110000 ;     //number 1 -> 0x30
            4'd2 : dt_translate = 7'b1101101 ;     //number 2 -> 0x6d
            4'd3 : dt_translate = 7'b1111001 ;     //number 3 -> 0x79
            4'd4 : dt_translate = 7'b0110011 ;     //number 4 -> 0x33
            4'd5 : dt_translate = 7'b1011011 ;     //number 5 -> 0x5b
            4'd6 : dt_translate = 7'b1011111 ;     //number 6 -> 0x5f
            4'd7 : dt_translate = 7'b1110000 ;     //number 7 -> 0x70
            4'd8 : dt_translate = 7'b1111111 ;     //number 8 -> 0x7f
            4'd9 : dt_translate = 7'b1111011 ;     //number 9 -> 0x7b
        endcase
        end
    endfunction
 
endmodule

仿真结果如下。

由图可知,片选、译码等信号,均符合设计。实际中,4 位数字应当在一定的时间内保持不变,而片选信号不停的循环扫描,数码管才能给肉眼呈现一种静态显示的效果。

小结

如果译码器设计没有使用函数 dt_translate,则在每个 case 选项里对信号 abcdefg 进行赋值时,还需要对 single_digit,ten_digit, hundred_digit, kilo_digit 进行判断。这些判断语句又会重复 4 次。虽然最后综合出的实际硬件电路可能是一样的,但显然使用函数后的代码更加的简洁、易读。

源码下载

Download