關鍵詞:函數,大小端轉換,數碼管譯碼

在 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 = 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        

函數裏的參數也可以改寫,例如:

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