1. 程式人生 > >Vivado使用技巧(26):HDL編寫技巧

Vivado使用技巧(26):HDL編寫技巧

在Vivado中進行HDL程式碼設計,不僅需要描述數字邏輯電路中的常用功能,還要考慮如何發揮Xilinx器件的架構優勢。目前常用的HDL語言有三種。VHDL語言的優勢有:

  • 語法規則更加嚴格;
  • 在HDL原始碼中初始化RAM元件更容易;
  • 支援package;
  • 自定義型別;
  • 列舉型別;
  • 沒有reg和wire之間的混淆。

Verilog語言的優勢有:

  • 與C語言類似的語法;
  • 程式碼結構更緊湊;
  • 支援塊註釋(老版VHDL不支援);
  • 沒有像VHDL一樣的重元件例項化。

SystemVerilog語言的優勢有:

  • 與Verilog相比程式碼結構更加緊湊;
  • 結構體和列舉型別有更好的擴充套件性;
  • 更高抽象級別的介面;
  • Vivado綜合支援SystemVerilog 2012。

以博主接觸的情況看,目前使用最廣泛的應該是Verilog語言,替代VHDL成為國內大學教學的主流。SystemVerilog其實有更高級別的描述能力,無論是設計還是模擬效能也更強大,目前很多國外大學都使用SystemVerilog作為教學語言。本文以Verilog語言為基礎講述HDL程式碼編寫技巧。RAM部分內容比較多,單獨放在第27篇講述。

1.觸發器、暫存器和鎖存器

Vivado綜合可以識別出帶有如下控制訊號的觸發器(Flip-Flop)和暫存器(register):上升沿或下降沿時鐘、非同步置位或復位訊號、同步置位或復位訊號、時鐘使能訊號。Verilog中對應著always

塊,其敏感列表中應該包含時鐘訊號和所有非同步控制訊號。

使用HDL程式碼設計觸發器、暫存器時注意如下基本規則:

  • 暫存器不要非同步置位/復位,否則在FPGA內找不到對應的資源來實現此功能,會被優化為其它方式實現。
  • 觸發器不要同時置位和復位。Xilinx的觸發器原語不會同時帶有置位和復位訊號,這樣做會對面積和效能產生不利影響。
  • 儘量避免使用置位/復位邏輯。可以採用其它方法以更少的代價達到同樣 的效果,比如利用電路全域性復位來定義初始化內容。
  • 觸發器控制訊號的輸入應總是高電平有效。如果設定為低電平有效,會插入一個反相器,對電路效能會產生不利影響。

Vivado綜合工具根據HDL程式碼會選擇4種暫存器原語:

  • FDCE:帶有時鐘使能和非同步清0的D觸發器;
  • FDPE:帶有時鐘使能和非同步預置(Preset)的D觸發器;
  • FDSE:帶有時鐘使能和同步置位的D觸發器;
  • FDRE:帶有時鐘使能和同步復位的D觸發器;

暫存器的內容會在電路上電時初始化,因此在申明訊號時最好設定一個預設值。綜合後,報告中可以看到暫存器的使用情況。下面給出一個暫存器的程式碼例項:

//上升沿時鐘、高電平有效同步清0、高電平有效時鐘使能8Bits暫存器
module registers_1(
    input [7:0] d_in,
    input ce, clk, clr,
    output [7:0] dout
);

reg [7:0] d_reg;
always @ (posedge clk)
    if(clr) d_reg <= 8'b0;
    else if(ce) d_reg <= d_in;

assign dout = d_reg;
endmodule

Vivado綜合還會報告檢測出的鎖存器(Latches),通常這些鎖存器是由HDL程式碼設計錯誤引起的,比如if或case狀態不完整。綜合會為檢測出的鎖存器報告一個WARNING(Synth 8-327)。下面給出一個鎖存器的程式碼示例:

//帶有Postive Gate和非同步復位的鎖存器
module latches (
    input G,
    input D,
    input CLR,
    output reg Q
);

always @ *
    if(CLR) Q = 0;
    else if(G) Q = D;

endmodule

2.三態緩衝器

三態緩衝器(Tristate buffer)通常由一個訊號或一個if-else結構來建模,緩衝器可以用來驅動內部匯流排,也可以驅動外部板子上的匯流排。如果使用if-else結構,其中一個分支需要給訊號賦值為高阻狀態。

當三態緩衝器通過管腳驅動外部匯流排時,使用OBUFT原語實現;當驅動內部匯流排時,使用BUFT原語,綜合工具會將其轉換為用LUT實現的邏輯電路。下面給出處理三態緩衝器的程式碼示例:

//使用always塊描述三態
module tristates_1 (
    input T, I, 
    output reg O
);

always @(T or I)
    if (~T) O = I;
    else O = 1'bZ;

endmodule

//使用並行賦值描述三態
module tristates_2 (
    input T, I, 
    output O);

assign O = (~T) ? I: 1'bZ;

endmodule

3.移位暫存器

一個移位暫存器(Shift Register)就是一個觸發器鏈,允許資料在一個固定的延遲段之間傳遞,也叫靜態移位暫存器。其通常包括:時鐘訊號、可選的時鐘使能訊號、序列資料輸入和輸出。

Vivado綜合可以用SRL型別的資源實現移位暫存器,如SRL16ESRLC32E。根據移位暫存器的長度,綜合時會選擇採用一個SRL型別原語實現,或採用級聯 的SRLC型別原語實現。下面給出移位暫存器的程式碼示例:

// 32Bits移位暫存器,上升沿時鐘,高電平有效時鐘使能訊號
// 使用連線運算子{}實現
module shift_registers_0 (
    input clk, clken, SI, 
    output SO
);

parameter WIDTH = 32;
reg [WIDTH-1:0] shreg;

always @(posedge clk)
    if (clken) shreg = {shreg[WIDTH-2:0], SI};

assign SO = shreg[WIDTH-1];

endmodule

// 使用for迴圈實現
module shift_registers_0 (
    input clk, clken, SI, 
    output SO
);

parameter WIDTH = 32;
reg [WIDTH-1:0] shreg;

integer i;
always @(posedge clk)
    if (clken) begin
        for (i = 0; i < WIDTH-1; i = i+1)
            shreg[i+1] <= shreg[i];
        shreg[0] <= SI;
    end

assign SO = shreg[WIDTH-1];

endmodule

4.動態移位暫存器

動態移位暫存器(Dynamic Shift register)是指電路操作期間移位暫存器的長度可以改變。可以採用如下兩種結構實現:

  • 一組觸發器鏈,最大長度可以改變;
  • 一個多路選擇器,在給定時鐘週期選擇從傳遞鏈中提取資料的某一段。結構框圖如下圖所示:

    這裡寫圖片描述

Verilog示例程式碼如下所示:

// 32-bit 動態移位暫存器
module dynamic_shift_register_1 (CLK, CE, SEL, SI, DO);

parameter SELWIDTH = 5;
input CLK, CE, SI;
input [SELWIDTH-1:0] SEL;
output DO;

localparam DATAWIDTH = 2**SELWIDTH;
reg [DATAWIDTH-1:0] data;

always @(posedge CLK)
    if (CE == 1'b1) data <= {data[DATAWIDTH-2:0], SI};

assign DO = data[SEL];  

endmodule

5.乘法器

綜合工具從原始碼中的乘法運算子來推測是否使用乘法器。乘法運算結果的位寬為兩個運算元位寬之和。比如16Bits的訊號乘以8Bits的訊號,結果為24Bits。乘法器可以用Slice單元或DSP塊實現,選擇依據有兩點:(1).運算元的大小;(2).是否需要最佳效能。通過第25篇介紹過的USE_DSP屬性可以強制設定乘法器的實現方式,設定為no用slice實現;設定為yes用DSP塊實現。

當使用DSP塊實現乘法器時,Vivado綜合可以發揮DSP塊流水線能力的最大優勢,綜合時會在乘法運算元和乘法器後插入兩級暫存器。當乘法器無法用一個DSP塊實現時,綜合時會拆分乘法運算,採用幾個DSP塊DSP塊加slice的方案實現。下面給出程式碼示例:

// 16*24-bit乘法器,運算元一級延遲,輸出三級延遲
module mult_unsigned (
    input clk, 
    input [15:0]A, 
    input [23:0]B, 
    output [39:0]RES
);

reg [15:0] rA;
reg [23:0] rB;
reg [39:0] M [3:0];

integer i;
always @(posedge clk)
begin
    rA <= A;
    rB <= B;
    M[0] <= rA * rB;
    for (i = 0; i < 3; i = i+1)
        M[i+1] <= M[i];
end

assign RES = M[3];

endmodule

除了乘法器,綜合工具還可以通過乘法器、加法器/減法器、暫存器的使用,推斷出乘加結構(Multiply-Add)、乘減結構(Multiply-Sub)、乘加減結構(Multiply-Add/Sub)和乘累加結構(Multiply-Accumulate)。

5.複數乘法器

下面給出一個複數乘法器的程式碼示例,該設計會使用三個DSP48單元:

// 複數乘法器(pr+i.pi) = (ar+i.ai)*(br+i.bi)
module cmult # (parameter AWIDTH = 16, BWIDTH = 18)
(
    input clk,
    input signed [AWIDTH-1:0] ar, ai,
    input signed [BWIDTH-1:0] br, bi,
    output signed [AWIDTH+BWIDTH:0] pr, pi
);

reg signed [AWIDTH-1:0] ai_d, ai_dd, ai_ddd, ai_dddd ;
reg signed [AWIDTH-1:0] ar_d, ar_dd, ar_ddd, ar_dddd ;
reg signed [BWIDTH-1:0] bi_d, bi_dd, bi_ddd, br_d, br_dd, br_ddd ;
reg signed [AWIDTH:0] addcommon ;
reg signed [BWIDTH:0] addr, addi ;
reg signed [AWIDTH+BWIDTH:0] mult0, multr, multi, pr_int, pi_int ;
reg signed [AWIDTH+BWIDTH:0] common, commonr1, commonr2 ;

always @(posedge clk)
begin
    ar_d <= ar;
    ar_dd <= ar_d;
    ai_d <= ai;
    ai_dd <= ai_d;
    br_d <= br;
    br_dd <= br_d;
    br_ddd <= br_dd;
    bi_d <= bi;
    bi_dd <= bi_d;
    bi_ddd <= bi_dd;
end

final products
//
always @(posedge clk)
begin
    addcommon <= ar_d - ai_d;
    mult0 <= addcommon * bi_dd;
    common <= mult0;
end
// Real product
//
always @(posedge clk)
begin
    ar_ddd <= ar_dd;
    ar_dddd <= ar_ddd;
    addr <= br_ddd - bi_ddd;
    multr <= addr * ar_dddd;
    commonr1 <= common;
    pr_int <= multr + commonr1;
end
// Imaginary product
//
always @(posedge clk)
begin
    ai_ddd <= ai_dd;
    ai_dddd <= ai_ddd;
    addi <= br_ddd + bi_ddd;
    multi <= addi * ai_dddd;
    commonr2 <= common;
    pi_int <= multi + commonr2;
end

assign pr = pr_int;
assign pi = pi_int;

endmodule

6.DSP塊中的預加器

當使用DSP塊時,設計程式碼最好使用帶符號數(signed)運算,這樣需要一個額外的bit位寬來儲存預加器(pre-adder)結果,這一位也可以封裝在DSP塊中。一個動態配置預加器(後面帶一個乘法器和加法器)的Veirlog示例如下:

// 控制訊號動態選擇預加或預減
module dynpreaddmultadd # (parameter SIZEIN = 16)
(
    input clk, ce, rst, subadd,
    input signed [SIZEIN-1:0] a, b, c, d,
    output signed [2*SIZEIN:0] dynpreaddmultadd_out
);

reg signed [SIZEIN-1:0] a_reg, b_reg, c_reg;
reg signed [SIZEIN:0] add_reg;
reg signed [2*SIZEIN:0] d_reg, m_reg, p_reg;

always @(posedge clk)
    if (rst) begin
        a_reg <= 0; b_reg <= 0;
        c_reg <= 0; d_reg <= 0;
        add_reg <= 0;
        m_reg <= 0; p_reg <= 0;
    end

    else if (ce) begin
        a_reg <= a; b_reg <= b;
        c_reg <= c; d_reg <= d;
        if (subadd)
            add_reg <= a - b;
        else
            add_reg <= a + b;
        m_reg <= add_reg * c_reg;
        p_reg <= m_reg + d_reg;
    end

// 輸出累加結果
assign dynpreaddmultadd_out = p_reg;

endmodule

7.UltraScale DSP塊中的平方電路

UltraScale DSP塊的原語DSP48E2支援計算輸入或預加器輸出的平方。下面的示例程式碼計算了差值的平方根,該設計會使用一個DSP塊實現,且有最佳的時序效能:

// DSP48E2支援平方運算,預加器配置為減法器
module squarediffmult # (parameter SIZEIN = 16)
(
    input clk, ce, rst,
    input signed [SIZEIN-1:0] a, b,
    output signed [2*SIZEIN+1:0] square_out
);

reg signed [SIZEIN-1:0] a_reg, b_reg;
reg signed [SIZEIN:0] diff_reg;
reg signed [2*SIZEIN+1:0] m_reg, p_reg;

always @(posedge clk)
    if (rst) begin
        a_reg <= 0;
        b_reg <= 0;
        diff_reg <= 0;
        m_reg <= 0;
        p_reg <= 0;
    end
    else if (ce) begin
        a_reg <= a;
        b_reg <= b;
        diff_reg <= a_reg - b_reg;
        m_reg <= diff_reg * diff_reg;
        p_reg <= m_reg;
    end

assign square_out = p_reg;

endmodule

8.黑盒子

FPGA設計可以包含EDIF網表,這些網表必須通過例項化與其它設計部分連線在一起。在HDL原始碼中使用BLACK_BOX屬性完成例項化,這部分例項化還可以使用一些特定的約束。使用BLACK_BOX屬性後,該例項將被視作黑盒子。下面給出示例程式碼:

//模組定義
(* black_box *) module black_box1
(
    input in1, in2, 
    output dout
);

endmodule

//模組例項化
module black_box_1 
(
    input DI_1, DI_2, 
    output DOUT
);

black_box1 U1 (
    .in1(DI_1),
    .in2(DI_2),
    .dout(DOUT)
);

endmodule

9.FSM狀態機

預設情況下,Vivado綜合可以從RTL設計中提取出有限狀態機(FSM),使用-fsm_extraction off可以關閉該功能。通常需要設計者設定FSM的編碼方式,便於綜合時根據設定調整優化目標。

Vivado綜合支援Moore和Mealy型狀態機。一個狀態機由狀態暫存器、下一個狀態功能、輸出功能三部分組成,可用如下框圖表示:
這裡寫圖片描述
Mealy狀態機需要從輸出到輸入的反饋路徑。儘管狀態暫存器也支援非同步復位,但最好還是使用同步復位方式。設定一個復位或上電狀態,綜合工具即可識別出FSM。預設狀態編碼為auto,綜合時會選擇最佳的編碼方式:

  • 獨熱碼(One-Hot):最多支援32個狀態,有最快的速度和較低的功耗,每個狀態由一個獨立的bit(對應一個觸發器)表示,狀態間轉換時只需要改變兩個bit的狀態。
  • 格雷碼(Gray):兩個連續狀態之間轉換時只需要改變一個Bit的狀態,可以最小化冒險和毛刺現象,有最低的功耗表現。
  • Johnson編碼:適用於包含沒有分支的長路徑的狀態機;
  • 順序編碼:使用連續的基數值表示狀態,跳轉到下一個狀態的狀態方程最簡單。

下面給出一個FSM的Verilog示例:

// 順序編碼的FSM
module fsm_1(
    input clk, reset, flag,
    output reg sm_out
);

parameter s1 = 3'b000;
parameter s2 = 3'b001;
parameter s3 = 3'b010;
parameter s4 = 3'b011;
parameter s5 = 3'b111;

reg [2:0] state;  //狀態暫存器

always@(posedge clk)
    if(reset) begin
        state <= s1;
        sm_out <= 1'b1;
    end
    else begin
        case(state)
        s1: if(flag) begin
                state <= s2;
                sm_out <= 1'b1;
            end
            else begin
                state <= s3;
                sm_out <= 1'b0;
            end
        s2: begin state <= s4; sm_out <= 1'b0; end
        s3: begin state <= s4; sm_out <= 1'b0; end
        s4: begin state <= s5; sm_out <= 1'b1; end
        s5: begin state <= s1; sm_out <= 1'b1; end
        endcase
    end

endmodule

10.ROM設計方法

Read-only memory(ROM)使用HDL模型實現與RAM非常相似。使用ROM_STYLE屬性選擇使用暫存器或塊RAM資源來實現ROM。使用塊RAM資源實現ROM的示例程式碼如下:

//使用塊RAM資源實現ROM
module rams_sp_rom_1 (
    input clk, en, 
    input [5:0] addr, 
    output [19:0] dout
);

(*rom_style = "block" *) reg [19:0] data;

always @(posedge clk)
    if (en)
        case(addr)
        6'b000000: data <= 20'h0200A; 6'b100000: data <= 20'h02222;
        6'b000001: data <= 20'h00300; 6'b100001: data <= 20'h04001;
        6'b000010: data <= 20'h08101; 6'b100010: data <= 20'h00342;
        ......
        6'b011110: data <= 20'h00301; 6'b111110: data <= 20'h08201;
        6'b011111: data <= 20'h00102; 6'b111111: data <= 20'h0400D;
        endcase


assign dout = data;

endmodule