FPGA語法篇


復雜的電路設計通常使用自頂向下的設計方法,設計過程中的不同階段需要不同的設計規格。比如架構設計階段,需要模塊框圖或算法狀態機(ASM)圖表這方面的設計說明。一個框圖或算法的實現與寄存器(reg)和連線(wire)息息相關。Verilog便具有將ASM圖表和電路框圖用計算機語言表達的能力,本文將講述Vivado綜合支持的Verilog硬件描述語言。

Verilog提供了行為化結構化兩方面的語言結構,描述設計對象時可以選擇高層次低層次的抽象等級。使用Verilog設計硬件時,可以將其視作並行處理面向對象編程。Vivado綜合支持IEEE 1364標准。Vivado綜合對Verilog的支持可以用最有效的方式描述整體電路和各個模塊。綜合會為每個模塊選擇最佳的綜合流程,將高層次的行為級或低層次的結構級轉換為門級網表

本文將介紹Vivado綜合支持的所有Verilog語法。


1.可變部分選擇

除了用兩個明確的值限定選擇邊界外(如assign out = data[8:2]),還可以使用變量從向量中選擇一組bit。設置一個起始點和截取的寬度,起始點可以動態變化。示例如下:

reg [3:0] data;
reg [3:0] select;
wire [7:0] byte = data[select +: 8]; //+、-表示從起始點開始增加或減少

2.結構化Verilog

Verilog可以進行多個代碼塊設計,並按一定的設計層次組合起來。下面給出於此相關的重要概念:

  • 組件(Component):結構化設計中的一個基本塊;
    • 申明(Declaration):組件與外部交流的信息;
    • 主體(Body):組件內部的行為或結構;
  • 端口(Port):組件的I/O;
  • 信號(Signal):組件與組件之間的連線;

一個組件用常見的模塊(module)來表示。組件之間的連接由實例化(instantiation)聲明實現。實例化聲明規定一個組件在另外一個組件或電路中的實例,賦予標識符,並用關系列表設定信號與端口之間的聯系。

除了自己設計的組件外,結構化Verilog還支持實例化預定義的原語:邏輯門、寄存器、Xilinx特定的原語(如CLKDLL、BUFG)。這些原語都定義在Xilinx Verilog庫文件unisim_comp.v中。邏輯門原語包括AND、OR、XOR、NAND、NOR、NOT。實例化這些邏輯門來搭建更大的邏輯電路,示例如下:

//實現2輸入或非邏輯功能 
module build_xor 
(
    input a, b, 
    output c
);

wire a_not, b_not;
//每個實例必須有不同的實例化名稱
not a_inv (a_not, a); 
not b_inv (b_not, b);
and a1 (x, a_not, b);
and a2 (y, b_not, a);
or out (c, x, y);

endmodule

3.Verilog參數

參數化代碼提高了可讀性和代碼緊湊型、容易維護和再使用。一個Verilog參數(parameter)就是一個常數(不支持字符串),且實例化參數化模塊時可以改寫參數值。下面給出示例:

//Verilog參數控制實例化塊寄存器的寬度
module myreg #(parameter SIZE = 1)
(
    input clk, clken, 
    input [SIZE-1:0]d, 
    output reg [SIZE-1:0]q
);

always @(posedge clk)
    if (clken) q <= d;

endmodule

//頂層模塊
module test #(parameter SIZE = 8)
(   
    input clk, clken, 
    input [SIZE-1:0] di,
    output [SIZE-1:0] do
);

myreg #SIZE inst_reg (clk, clken, di, do);

endmodule

4.Verilog使用限制

在Vivado綜合中用到的Verilog語法有如下3點限制:

  • 大小寫敏感:Verilog是一種大小寫敏感的語言,但在Vivado中,只有實例和信號名稱會區分大小寫。如果兩個module名稱只有大小寫不同,綜合時會報錯。盡管如此,也不推薦僅用大小寫區分兩個不同的對象,在混合語言工程中可能會引起意料之外的問題。

  • 阻塞和非阻塞賦值:不要混合使用阻塞和非阻塞賦值。盡管綜合時可能不會報錯,但在仿真時會出現錯誤。下面給出兩個錯誤例子:

//同一信號不要混用阻塞和非阻塞賦值 
always @(in1)
    if (in2) out1 = in1;
    else out1 <= in2;

//同一信號的不同bit不要混用    
if (in2) begin
    out1[0] = 1'b0;
    out1[1] <= in1;
end 
else begin
    out1[0] = in2;
    out1[1] <= 1'b1;
end 
  • 整數處理:某些情況下,Vivado綜合器處理整數時與其它綜合工具方法不同,因此必須使用特定的代碼編寫方式。在Case語句或拼接語句中,使用未定義大小的整數都會導致無法預料的結果。下面給出例子:
//case語句
reg [2:0] condition1; 
always @(condition1) begin
    case(condition1)
    4 : data_out = 2;    //生成錯誤結果
    3'd4 : data_out = 2; //正常工作 
    endcase
end

//拼接語句 
reg [31:0] temp;
assign temp = 4'b1111 % 2;  //未確定位寬的運算用臨時信號存儲 
assign dout = {12/3,temp,din};  //12/3運算位寬不確定,結果錯誤

5.Verilog構造和系統任務

Vivado綜合支持的Verilog構造與系統任務包括:

整數、實數、assign(有限制)、deassign(有限制)、repeat語法(重復值必須是常數)、for語法(范圍必須是靜態的)、disable(不能用於for循環和repeat循環)、module定義、defparam、實例數組、`default_nettype、`define、`ifdef、`ifndef、`elsif、`include、`file、`line、$fclose、$fgets、$fopen、$fscanf、$readmemb、$readmemh、$signed、$unsigned、$floor(僅用於參數)、$ceil(僅用於參數)。

Vivado綜合不支持和會忽視的的Verilog構造和系統任務包括:

字符串、網絡類型(tri0、tri1、trireg)、驅動強度、實數和實時寄存器、命名事件、事件(@)、延遲(#)、force、release、forever語法、wait、並行塊、設定塊、macromodule定義、層次結構名稱、`celldefine、`endcelldefine、`resetall、`timescale、`unconnected_drive、`nounconnected_drive、`uselib、$display、$fdisplay、$finish、$fwrite、$monitor、$random、$stop、$strobe、$time、$write、$clog2(僅SystemVerilog支持)、$rtoi、$itor、all others。

介紹其中幾個非常常用的系統任務:

  • $signed和$unsigned可以強制規定輸入數據為帶符號數或無符號數,並作為返回值,不用管之前的符號。
  • $readmemb和$readmemh可以用於初始化塊存儲器,兩者分別用2進制和16進制表示。如“$readmemb(“ram.data”, ram, 0, 7)”;。

6.Verilog原語

Vivado支持上文列出的Verilog門級原語,但不支持上拉下拉、驅動強度和延遲、原語矩陣這些類型的門級原語。也不支持如下轉換級原語:cmos、nmos、pmos、rcmos、rnmos、rpmos、rtran、rtranif0、rtranif1、tran、tranif0、tranif1。

實例化門級原語的示例如下:

gate_type instance_name (output, inputs); //語法模板 
and U1 (out, in1, in2);
bufif1 U2 (triout, data, trienable);

7.行為級Verilog

行為級Verilog中的變量都申明為整數,數據類型可以是reg(程序塊中賦值)、wire(連續賦值)和integer(會被轉換為寄存器類型)。所有變量的默認位寬為1bit,稱作標量(scalar);定義的N bits位寬變量稱作向量(Vector)。reg和wire可以定義為帶符號數signed或無符號數unsigned。變量的每個bit可以是如下值:1(邏輯1)、0(邏輯0)、x(未知邏輯值)、z(高阻)。

reg [3:0] arb_priority;
wire [31:0] arb_request;
wire signed [8:0] arb_signed;

寄存器在定義時可以初始化,初始值是一個常數或參數,不能是函數或任務的調用。在全局復位或上電時,Vivado綜合會將初始化值作為寄存器的輸出(作為寄存器的INIT屬性值)。而且,該初始值與本地復位是相互獨立的。

//定義時初始化寄存器
reg arb_onebit = 1'b0;
reg [3:0] arb_priority = 4'b1011;

//本地置位/復位
always @(posedge clk)
    if (rst) arb_onebit <= 1'b0;

Verilog支持定義wire和reg的數組,支持一位數組和二維數組,但每次從數組中選擇的元素不能超過一個,數組也不能作為任務或函數的傳遞參數。數組的定義示例如下:

//有32個元素的數組,每個元素4bits位寬
reg [3:0] mem_array [31:0]; 
//包含64個8bits位寬元素的數組
wire [7:0] mem_array [63:0];
//包含256*16個8bits位寬wire元素的二維數組 
wire [63:0] array2 [0:255][0:15];
 //包含256*8個64bits位寬reg元素的二維數組 
reg [63:0] array2 [255:0][7:0];

Vivado支持的所有表達式列在下表中:

符號 表達式
{} 拼接運算符
{{}} 復制運算符
+, -, , /, %, * 加、減、乘、除、求余、求冪
>, <, >=, <= 關系運算
! 邏輯取反
&& 邏輯與
|| 邏輯或
==, != 邏輯相等,邏輯不等
=== 條件相等
!== 條件不等
~ 按位取反
& 按位與
| 按位或
^ 按位異或
~^, ^~ 按位等價(異或非)
~& 與非運算
~| 或非運算
<<, >> 左移,右移
<<<,>>> 帶符號左移,帶符號右移
?: 條件表達式
or, ‘,’ 事件或(如用於敏感列表)

其中“===”和“!==”在綜合時與“==”和“!=”功能相同,沒有任何差別。但在仿真中,可以用來判斷變量是否與’x’和’z’是否相等。下表給出常用操作符的運算結果,以供查閱。
這里寫圖片描述
initialalways是兩個程序塊,每個塊內部組織了一些語法聲明,用beginend表示范圍。塊內部的語法聲明按順序執行。綜合時只會處理always塊,會忽略initial塊。


8.模塊module

Verilog中描述組件(component)的方法便是模塊(module),模塊必須申明與實例化。模塊申明包括模塊名稱、電路I/O端口列表、定義功能的主體,並以endmodule結束。

每個電路I/O端口要有名稱、端口模式(input、output、inout),如果端口是數組類型還要有范圍信息。下面給出兩種模塊申明方法的示例:

//方法1,老版本Verilog
module example (A, B, O);

input A, B;
output O;

assign O = A & B;

endmodule

//方法2,推薦用法
module example 
( 
    input A, B, 
    output O
);

assign O = A & B;

endmodule

實例化模塊時,要定義一個實例化名稱和一個端口關系表。列表要規定實例與頂層模塊之間如何連接,列表中的每一個元素將模塊申明中的一個形式端口(port)和頂層模塊中的實際網絡(net)連接在一起。下面給出一個實例化上述模塊的例子:

module top 
(
    input A, B, C, 
    output O
); 

wire tmp;

example inst_example (.A(A), .B(B), .O(tmp));
assign O = tmp | C;

endmodule

Vivado綜合支持兩種連續賦值方式(只適用於wire和三態數據類型),用簡潔的方式完成組合邏輯賦值,但是綜合時會忽略連續賦值中的延遲和強度定義。顯式連續賦值用assign關鍵詞開頭,緊跟一個已經申明過的網絡:“wire mysignal; assign mysignal = select ? b : a;”。隱式連續賦值在申明時便完成賦值:“wire misignal = a | b;”。


9.過程賦值

如上所述,wire和三態類型要用連續賦值,reg類型變量則需要用過程賦值,借助always塊、任務(task)、函數(function)實現。學習Verilog難免會遇到阻塞賦值和非阻塞賦值的概念,但其實在設計中只需要明白阻塞賦值(=)用於仿真;非阻塞賦值(<=)用於設計中的過程賦值即可。

always塊中的組合邏輯由Verilog時間控制語句有效地建模。其中,延遲時間控制語句[#]僅用於仿真,綜合時會忽略;組合邏輯建模主要由事件控制時間控制語句[@]實現。

每個always塊都有一個敏感列表,列在“always @”后面的括號中。如果敏感列表中一個信號的相關事件發生(值變化或邊沿到來),就會激活該always塊。在always塊中,如果信號沒有在if或case的所有分支中明確地賦值,綜合會產生一個鎖存器保持之前的值。一個程序塊中可以使用如下語句:

[1].if-else語句:

使用true和false條件來執行語句,執行多條語句要使用begin…end關鍵詞。

[2].case語句:

比較表達式和分支的值,比較順序按照編寫分支的順序進行,執行第一個匹配的分支。如果沒有匹配項則執行default分支。case語句中不要使用未指定位寬大小的整數,否則可能會產生錯誤結果。

casez將分支的任意bit位上的z值視作不關心;casex將分支的任意bit位上的x值視作不關心。casez和casex中不關心的bit用‘?’代替。下面給出一個使用case的示例代碼:

module mux4 
(
    input [1:0] sel, 
    input [1:0] a, b, c, d, 
    output reg [1:0] outmux
);

always @ *
    case(sel)
        2'b00 : outmux = a;
        2'b01 : outmux = b;
        2'b10 : outmux = c;
        2'b11 : outmux = d;
    endcase

endmodule

上述代碼在評估輸入值時,按照一定的優先級順序進行。如果希望能並行地處理這個過程,使用paralled_case屬性,將case語句替換為“(* paralled_case *)” case(sel)”。

[3].For語句與Repeat語句:

使用循環可以完成一些重復性工作。For循環的邊界必須是常數,停止循環條件需要使用>、<、>=、<=四種運算符。使用“var = var +或- step”來控制執行下一輪運算,var為循環變量,step是一個常數值。

使用repeat語句,重復次數也必須是常數值。

[4].While循環:

While的測試表達式可以是任意合法的Verilog表達式。為了避免造成無限循環,可以使用-loop_iteration_limit選項。該語法很少使用,下面給出一個示例代碼:

parameter P = 4; 
always @(ID_complete) 
begin : UNIDENTIFIED
    integer i; 
    reg found; 
    unidentified = 0; 
    i = 0;
    found = 0;
    while (!found && (i < P))
    begin
            found = !ID_complete[i];
            unidentified[i] = !ID_complete[i];
            i = i + 1;
    end
end

[5].順序always塊:

always塊可以描述帶有順序性的電路,敏感列表中需要包含如下邊沿觸發事件(上升沿posedge或下降沿negedge):必須有一個時鍾事件、可選的置位/復位事件。如果不需要異步信號,always塊模板如下:

always @(posedge CLK)
begin
    //同步部分
end

如果需要異步控制信號,always塊模板如下:

always @(posedge CLK or posedge ACTRL1 or à )
begin
    if (ACTRL1)
        //異步部分
    else
        //同步部分 
end

下面給出四個不同觸發方式的順序always塊示例代碼:

//上升沿觸發時鍾控制的8bits寄存器
module seq1 
(
    input [7:0]DI, 
    input CLK, 
    output reg [7:0] DO
);

always @(posedge CLK) 
    DO <= DI ;
endmodule

//添加一個高電平有效異步復位信號
module seq1 
(
    input [7:0]DI, 
    input CLK, ARST, 
    output reg [7:0] DO
);

always @(posedge CLK or posedge ARST) 
    if (ARST == 1'b1) DO <= 8'h00;
    else DO <= DI ;
endmodule

//再添加一個低電平有效異步置位信號
module seq1 
(
    input [7:0]DI, 
    input CLK, ARST, ASET 
    output reg [7:0] DO
);

always @(posedge CLK or posedge ARST or negedge ASET) 
    if (ARST == 1'b1) DO <= 8'h00;
    else if (ASET == 1'b1) DO <= 8'hFF;
    else DO <= DI ;
endmodule

//不使用異步控制邏輯,使用同步復位
module seq1 
(
    input [7:0]DI, 
    input CLK, SRST, 
    output reg [7:0] DO
);

always @(posedge CLK) 
    if (SRST == 1'b1) DO <= 8'h00;
    else DO <= DI ;
endmodule

最后再補充一些與賦值有關的內容。如果表達式左邊位寬大於右邊的位寬,賦值時需要在高位填充:

  • 如果表達式右邊為無符號數,則高位補0;
  • 如果表達式右邊為帶符號數,則高位補符號位;
  • 如果表達式右邊的最高位為x或z,則無論該數為無符號數還是帶符號數,高位都補充為x或z。

10.任務與函數

如果設計中要多次使用重復的代碼,可以使用任務task和函數function來減少代碼量,提升可維護性。任務和函數必須在模塊中申明和使用,函數頭只包含輸入參數,任務頭包含輸入、輸出和雙向參數。函數的返回值可以申明為無符號數或帶符號數,函數內容與always塊類似。下面分別給出一個函數和任務的示例代碼:

//函數function使用示例
module test 
(
    input [3:0] A, B, 
    input CIN, 
    output [3:0] S, 
    output COUT
);

wire [1:0] S0, S1, S2, S3;

function signed [1:0] ADD;
    input A, B, CIN;
    reg S, COUT;
    begin
        S = A ^ B ^ CIN;
        COUT = (A&B) | (A&CIN) | (B&CIN);
        ADD = {COUT, S};
    end
endfunction

assign S0 = ADD (A[0], B[0], CIN),
       S1 = ADD (A[1], B[1], S0[1]),
       S2 = ADD (A[2], B[2], S1[1]),
       S3 = ADD (A[3], B[3], S2[1]),
       S = {S3[0], S2[0], S1[0], S0[0]},
       COUT = S3[1];

endmodule

//任務task使用示例
module test 
(
    input [3:0] A, B, 
    input CIN, 
    output [3:0] S, 
    output COUT
);

reg [1:0] S0, S1, S2, S3;

task ADD;
    input A, B, CIN;
    output [1:0] C;
    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

Verilog還支持遞歸任務和遞歸函數,要使用automatic關鍵詞申明。遞歸次數由-recursion_iteration_limit選項設置,默認為64,以避免無限遞歸。下面給出一個計算階乘的遞歸函數的例子。

function automatic [31:0] fac;
    input [15:0] n;
    if (n == 1) fac = 1;
    else fac = n * fac(n-1); 
endfunction

Vivado綜合支持函數調用來計算常數值,將其稱之為常數函數。下面給出一個使用常數函數的例子:

module test #(parameter ADDRWIDTH = 8, DATAWIDTH = 4)
(
    input clk, we, 
    input [ADDRWIDTH-1:0] a,
    input [DATAWIDTH-1:0] di,
    output [DATAWIDTH-1:0] do
);

function integer getSize;
    input addrwidth;
    begin
        getSize = 2**addrwidth;
    end
endfunction

reg [DATAWIDTH-1:0] ram [getSize(ADDRWIDTH)-1:0];
always @(posedge clk) 
    if (we) ram[a] <= di;

assign do = ram[a];

endmodule

Verilog中的常數可以用2進制、8進制、10進制和16進制表示,沒有明確表示時默認為10進制。如下面4’b1010、4’o12、4’d10、4’ha表示同一個數。


11.Verilog宏

Verilog可以像這樣定義宏“`define TESTEQ1 4’b1101”。定義的宏可以用在后面的代碼中,如“if (request == `TESTEQ1)”。使用`ifdef`endif可以檢測是否定義了某個宏,相當於條件編譯。如果`ifedf調用的宏被定義過,則內部的代碼將會編譯;如果宏沒有定義,則會編譯`else中的代碼。`else不是必須的,但必須有`endif。

使用宏可以在不修改源代碼的情況下修改設計,在IP核生成和流程測試中很有用。下面給出兩個使用宏的例子:

//示例1
'define myzero 0
assign mysig = 'myzero;

//示例2,條件編譯
'ifdef MYVAR
module if_MYVAR_is_declared;
...
endmodule
'else
module if_MYVAR_is_not_declared;
...
endmodule
'endif

12.Include文件

Verilog可以將源代碼分散在多個文件中,當需要引用另一個文件中的代碼時,可以使用如下語句:“`include <path/file-to-be-included>”。該代碼可以將指定文件的內容全部插入到當前文件的`include行中。Vivado首先會在指定路徑中查找,如果沒有找到則會在-include_dirs選項設置的目錄中查找。可以同時使用多個`include語句。


13.Generate語法

Verilog的注釋和C++語言相同,支持單行注釋和多行注釋,這里不再舉例。最后再說說常用的Generate語法。使用generate可以簡化代碼編寫工作,generate…endgenerate中的內容再RTL分析階段會被轉換為對應的電路。

使用generate語法可以創建原語或模塊實例、initial或always程序塊、連續賦值、網絡和變量申明、參數重定義、任務或函數定義。Vivado支持全部三種generate語法:generate循環(generate-for)、generate條件(generate-if-else)和generate情況(generate-case)。

[1]. generate-for

使用generate-for主要用來創建多個實例化,與for循環用法基本相同,但必須使用genvar變量,且begin語句必須有一個單獨的命名。下面給出一個示例代碼:

generate genvar i;
for (i=0; i<=7; i=i+1)
begin : for_name
    adder add (a[8*i+7 : 8*i], b[8*i+7 : 8*i], ci[i], sum_for[8*i+7 : 8*i],
    c0_or[i+1]);
end
endgenerate

[2]. generate-if-else

主要用來控制生成哪一個對象,每一個分支用begin…end限定,begin語句必須有一個單獨的命名。下面給出一個示例代碼:

//根據數據位寬選擇不同的乘法器實現方式
generate
    if (IF_WIDTH < 10)
    begin : if_name
        multiplier_imp1 # (IF_WIDTH) u1 (a, b, sum_if);
    end
    else
    begin : else_name
        multiplier_imp2 # (IF_WIDTH) u2 (a, b, sum_if);
    end
endgenerate

[3]. generate-case

主要用來控制在哪種條件下生成哪個對象。case的每一個分支用begin…end限定,begin語句必須有一個單獨的命名。下面給出一個示例代碼:

//根據數據位寬選擇不同的加法器實現方式
generate
case (WIDTH)
1:
    begin : case1_name
    adder #(WIDTH*8) x1 (a, b, ci, sum_case, c0_case);
    end
2:
    begin : case2_name
    adder #(WIDTH*4) x2 (a, b, ci, sum_case, c0_case);
    end 
default:
    begin : d_case_name
    adder x3 (a, b, ci, sum_case, c0_case);
    end
endcase
endgenerate


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM