1. 程式人生 > 其它 >【深入淺出玩轉FPGA】學習筆記_設計技巧

【深入淺出玩轉FPGA】學習筆記_設計技巧

一、基本語法

(一)可綜合的Verilog語法子集

硬體設計的精髓是力求用最簡單的語句描述最複雜的硬體。

常用的RTL語法結構:

模組宣告:module……endmodule。

埠宣告:input,output,inout。

訊號型別:wire,reg(最常用);tri,integer(一般用在測試腳本里)。

引數定義:parameter。

運算操作符:大多數邏輯操作符、移位操作符、算術操作符都是可綜合的(===與!==是不可綜合的)。

比較判斷:if……else,case(casex,casez)……default……endcase。

連續賦值:assign,問號表示式(?:)。

always模組:敏感列表可以是電平、沿訊號posedge/negedge。

begin……end。

任務定義:task……endtask。

迴圈語句:for

賦值訊號:=和<=

(二)if……else與case語句分析

if……else和case語句實現的結構到底是怎樣還是要看開發工具,具體問題具體分析,不能片面強調誰好誰壞。

具體邏輯具體分析,如果你要表達的是同一個邏輯問題,那麼if……else和case只不過是形式上的不同。綜合工具的優化能力足夠強的話,就能看穿這個形式上的不同,實現邏輯上的相同。

(三)for語句

for語句可綜合,但是在RTL級的程式碼中基本不用。一方面因為for語句的使用很佔用硬體資源,另一方面是因為在設計中往往採用時序邏輯設計,用到for迴圈的地方不多。

例:在一個時鐘週期內計算13路脈衝訊號為高電平的個數

module test(clk,rst_n,data,num);
input clk;
input rst_n;
input [12:0] data;
output [15:0] num;

reg [3:0] i;
reg [15:0] num;

always @(posedge clk)begin
    if(!rst_n)begin
        num <= 0;
        end
    else begin
        for(i=0;i<13;i=i+1)
            if(data[i]) num <= num + 1
; end end endmodule

發現每個時鐘週期for迴圈只執行一次 num<=num+1。

因為always語句中使用非阻塞賦值時,是在always結束後才把值賦給左邊的暫存器。

修改成阻塞賦值可解決問題。

for語句綜合的效率不高,在對速度要求不高的前提下,還是寧願用多個時鐘週期去實現也不用for語句。同時時序邏輯裡多用非阻塞賦值語句,修改之後的程式碼風格其實是不可取的。

硬體語言不能像C語言一樣片面的追求程式碼的簡捷。

(四)inout用法

需要考慮到inout埠同時作為輸入輸出口的衝突問題。

驅動源 0 1 X Z
0 0 X X 0
1 X 1 X 1
X X X X X
Z 0 1 X Z

當inout埠作為輸入口使用時,一定要把它置為高阻態。

inout io_data;                                              //inout口
reg out_data;                                               //需要輸出的資料
reg io_link;                                                //inout口方向控制
assign io_data = io_link ? out_data:1'bz;                   //關鍵

(五)探討4輸入LUT

大多數FPGA是基於4輸入LUT的結構。

例1:4輸入與

input clk;
input a,b,c,d;
output reg dout;
always @(posedge clk)
    dout <= a & b & c & d;

綜合後:用了FPGA內部一個4輸入的LUT和一個觸發器

例2:5輸入與

input clk;
input a,b,c,d,e;
output reg dout;
always @(posedge clk)
    dout <= a & b & c & d & e;

綜合後:用了FPGA內部兩個4輸入的LUT和一個觸發器

還有兩個閒置的輸入能否利用起來?

例3:

input clk;
input a,b,c,d,e;
input f,g;
output reg dout;
output reg fout;
always @(posedge clk)begin
    dout <= a & b & c & d & e;
    fout <= f | g;
end

綜合後:用了FPGA內部三個4輸入的LUT和兩個觸發器

因此,在一個組合邏輯中沒有用完的LUT是無法被其他邏輯複用的。

二、狀態機設計

(一)狀態機的基本概念

通過不同的狀態遷移來完成一些特定的順序邏輯,即分多個時間完成一個任務。

構成狀態機的基本要素是狀態機的輸入、輸出和狀態。

Moore型狀態機的狀態變化僅和當前狀態有關,而與輸入條件無關;Mealy型狀態機的狀態變化不僅和當前狀態有關,還取決於當前的輸入條件。

(二)三種不同的狀態機寫法

1、兩段式狀態機

reg [3:0] cstate;
reg [3:0] nstate;

always @(posedge clk or negedge rst_n) begin
    if(!rst_n) cstate <= IDLE;
    else cstate <= nstate;
end

always @(cstate or wr_req or rd_req) begin
    case(cstate)
        IDLE:   if(wr_req)begin
                    nstate = WR_S1;
                    cmd = 3'b011;
                    end
                else if(rd_req) begin
                    nstate = RD_S1;
                    cmd = 3'b011;
                    end
                else begin
                    nstate = IDLE;
                    cmd = 3'b111;
                    end
        WR_S1:  begin
                    nstate = WR_S2;
                    cmd = 3'b101;
                end
        WR_S2:  begin 
                    nstate = IDLE;
                    cmd = 3'b111;
                end
        RD_S1:  if(wr_req) begin 
                    nstate = WR_S2;
                    cmd = 3'b101;
                    end
                else begin
                    nstate = RD_S2;
                    cmd = 3'b110;
                    end
        RD_S2:  if(wr_req) begin 
                    nstate = WR_S1;
                    cmd = 3'b011;
                    end
                else begin 
                    nstate = IDLE;
                    cmd = 3'b111;
                    end
        default:nstate = IDLE;
    endcase
end

2、三段式狀態機

reg [3:0] cstate;
reg [3:0] nstate;

always @(posedge clk or negedge rst_n) begin
    if(!rst_n) cstate <= IDLE;
    else cstate <= nstate;
end

always @(cstate or wr_req or rd_req) begin
    case(cstate)
        IDLE:   if(wr_req) nstate = WR_S1;
                else if(rd_req) nstate = RD_S1;
                else nstate = IDLE;
        WR_S1:  nstate = WR_S2;
        WR_S2:  nstate = IDLE;
        RD_S1:  if(wr_req) nstate = WR_S2;
                else nstate = RD_S2;
        RD_S2:  if(wr_req) nstate = WR_S1;
                else nstate = IDLE;
        default:nstate = IDLE;
    endcase
end

always @(posedge clk or negedge rst_n) begin
    case(nstate)
        IDLE:   if(wr_req) cmd <= 3'b011;
                else if(rd_req) cmd <= 3'b011;
                else cmd <= 3'b111;
        WR_S1:  cmd <= 3'b101;
        WR_S2:  cmd <= 3'b111;
        RD_S1:  if(wr_req) cmd <= 3'b101;
                else cmd <= 3'b110;
        RD_S2:  if(wr_req) cmd <= 3'b011;
                else cmd <= 3'b111;
        default:
    endcase
end

二段式把時序邏輯和組合邏輯分開來,時序邏輯裡進行當前狀態和下一狀態的切換,組合邏輯裡實現各個輸入、輸出以及狀態判斷。易維護,但組合邏輯輸出容易產生毛刺。

三段式較為推薦,時序邏輯的輸出解決了兩段式寫法中組合邏輯的毛刺問題,但較之兩段式,三段式資源消耗多一些;另外,三段式從輸入到輸出會比兩段式延時一個時鐘週期。

三、復位設計

(一)非同步復位與同步復位

1、非同步復位

非同步復位,指復位訊號和系統時鐘訊號的觸發可以在任何時刻,二者相互獨立。

always @(posedge clk or negedge rst_n)
    if(!rst_n) b <= 1'b0;
    else b <= a;

綜合後,可知暫存器存在一個非同步的清零端(CLR),在非同步復位的設計中,這個埠一般接低電平有效的復位訊號rst_n,即使設計中是高電平復位,實際綜合後也會把非同步復位訊號反向後接到這個CLR端。

2、同步復位

always @(posedge clk)
    if(!rst_n) b <= 1'b0;
    else b <= a;

綜合後,可知同步復位沒有用到暫存器的CLR埠,綜合出來的實際電路只是把復位訊號rst_n作為輸入邏輯的使能訊號。這必然會額外增加FPGA內部的資源消耗。

總結:兩種方式各有優缺點。FPGA的暫存器有支援非同步復位專用的埠,採用非同步復位無需增加器件的額外資源,但是非同步復位也存在隱患,即非同步時鐘域的亞穩態問題存在於非同步復位訊號和系統時鐘訊號之間。同步復位在時鐘訊號clk的上升沿觸發時進行系統是否復位的判斷,這降低了亞穩態出現的概率(但不能完全避免),缺點就是增加了器件資源,無法利用已有的復位埠CLR。

3、非同步復位存在隱患

always @(posedge clk or negedge rst_n)
    if(!rst_n) b <= 1'b0;
    else b <= a;
always @(posedge clk or negedge rst_n)
    if(!rst_n) c <= 1'b0;
    else c <= b;

綜合出來電路圖:

問題在於:不能確定復位訊號rst_n會在什麼時候結束。

如果復位訊號結束於b_reg0和c_reg0的{latch edge - setup time,latch edge + hold time}時間之外,一切正常。

但如果復位訊號的撤銷(由低電平變為高電平)出現在clk鎖存訊號的建立時間或者保持時間內,此時clk檢測到rst_n的狀態就會是一個亞穩態(不確定是0還是1)。b_reg0和c_reg0如果出現一個復位一個跳出復位,那麼就會造成系統工作不同步的問題。

(二)復位與亞穩態

亞穩態對一個暫存器的影響相對小一些,但是對於諸如匯流排式的暫存器受到的影響就很大了。

(三)非同步復位、同步釋放

該電路由兩個同一時鐘沿觸發的層疊暫存器組成,該時鐘必須和目標暫存器是一個時鐘域。

input clk;                                      //系統時鐘訊號
input rst_n;                                    //輸入復位訊號,低有效
output rst_nr2;                                 //非同步復位、同步釋放輸出
reg rst_nr1,rst_nr2;                            

//兩級層疊復位產生,低電平復位
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) rst_nr1 <= 1'b0;
    else rst_nr1 <= 1'b1;                       //給一個確定的值1'b1,不會出現亞穩態
end

always @(posedge clk or negedge rst_n) begin
    if(!rst_n) rst_nr2 <= 1'b0;
    else rst_nr2 <= rst_nr1;
end

實現的電路圖:

既解決了同步復位的資源消耗問題,又解決了非同步復位的亞穩態問題。

根本思想:非同步訊號同步化

應用到上述例子,可以得到新的電路圖:

這就得到了穩定復位訊號。

(四)PLL配置後的復位設計

在系統復位後、PLL時鐘輸出前,即系統工作時鐘不確定的情況下,應怎麼考慮這個復位的問題呢?

可以這樣解決:先用FPGA的外部輸入時鐘clk將FPGA的輸入復位訊號rst_n做非同步復位、同步釋放處理,然後這個復位訊號輸入PLL,同時clk也輸入PLL。設計初衷是在PLL輸出時鐘有效前,系統的其他部分都保持復位狀態。PLL的輸出locked訊號在PLL有效輸出之前一直是低電平,PLL輸出穩定有效之後才會拉高該訊號,所以這裡就把FPGA外部輸入復位訊號rst_n和這個locked訊號相與作為整個系統的復位訊號。同時,這個復位訊號也需要讓合適的PLL輸出時鐘非同步復位、同步釋放處理一下。總的來說,為了達到可靠穩定的復位訊號,該設計中對復位訊號進行了兩次處理,分別在PLL輸出前和PLL輸出後。

module sys_ctrl(
    clk,rst_n,sys_rst_n,
    clk_25m,clk_100m
);

input clk;                                                  //FPGA輸入時鐘訊號25MHz
input rst_n;                                                //系統復位訊號

output sys_rst_n;                                           //系統復位訊號,低有效

output clk_25m;                                             //PLL輸出25MHz時鐘頻率
output clk_100m;                                            //PLL輸出100MHz時鐘頻率
wire locked;                                                //PLL輸出有效標誌位,高表示PLL輸出有效

//——————————————————————————————————————————————————————————————
//PLL復位訊號產生,高有效
//非同步復位,同步釋放
wire pll_rst;                                               //PLL復位訊號,高有效

reg rst_r1,rst_r2;

always @(posedge clk or negedge rst_n) begin
    if(!rst_n) rst_r1 <= 1'b1;
    else rst_r1 <= 1'b0;                       

always @(posedge clk or negedge rst_n) begin
    if(!rst_n) rst_r2 <= 1'b1;
    else rst_r2 <= rst_r1;
end

assign pll_rst = rst_r2

//——————————————————————————————————————————————————————————————
//系統復位訊號產生,低有效
//非同步復位,同步釋放
wire sys_rst_n;                                             //系統復位訊號,低有效
wire sysrst_nr0;

reg sysrst_nr1,sysrst_nr2;

assign sysrst_nr0 = rst_n & locked;                         //系統復位直到PLL有效輸出

always @(posedge clk_100m or negedge sysrst_nr0) begin
    if(!sysrst_nr0) sysrst_nr1 <= 1'b0;
    else sysrst_nr1 <= 1'b1;                       

always @(posedge clk_100m or negedge sysrst_nr0) begin
    if(!sysrst_nr0) sysrst_nr2 <= 1'b0;
    else sysrst_nr2 <= sysrst_nr1;
end

assign sys_rst_n = sysrst_nr2;

//——————————————————————————————————————————————————————————————
//例化PLL產生模組
PLL_ctrl            uut_PLL_ctrl(                           
                        .areset(pll_rst),                   //PLL復位訊號,高電平復位
                        .inclk(clk),                        //PLL輸入時鐘,25MHz
                        .c0(clk_25m),                       //PLL輸出25MHz時鐘頻率
                        .c1(clk_100m),                      //PLL輸出100MHz時鐘頻率
                        .locked(locked)                     //PLL輸出有效標誌位,高電平表示PLL輸出有效     
                    );

endmodule

四、FPGA重要設計思想及工程應用

(一)速度和麵積互換原則

速度:整個工程穩定執行所能達到的最高時鐘頻率;

面積:通過一個工程執行所消耗的觸發器(FF)、查詢表(LUT)數量或者等效門數量來衡量。

從系統設計的角度闡釋速度和麵積的互換原則:

這很好的利用了FPGA的並行性。

(二)乒乓操作及串/並轉換設計

1、乒乓操作:主要用於資料流處理

資料緩衝模組可以是任何儲存模組,常用的有雙口RAM、SRAM、SDRAM、FIFO等。

第一個緩衝週期:輸入的資料流快取到“資料緩衝1”模組。

第二個緩衝週期:“輸入資料選擇控制”模組將輸入的資料流快取到“資料緩衝2”模組的同時,“輸出資料選擇控制”模組將“資料緩衝1”模組第一個週期快取的資料流送到“後續處理”模組。

第三個緩衝週期:“輸入資料選擇控制”模組切換使輸入的資料流快取到“資料緩衝1”模組,同時,“輸出資料選擇控制”模組切換使“資料緩衝2”模組快取的第二個週期的資料送到“後續處理”模組。

如此不斷迴圈。

乒乓操作可以實現資料的無縫緩衝與處理。

2、串/並轉換:高速資料流處理

根據資料的順序與數量的要求,串並轉換可以選用暫存器、雙口RAM、SRAM、SDRAM、FIFO等實現。對於數量比較小的設計可以採用移位暫存器完成串並轉換。

移位一般需要時鐘做同步的,即n個時鐘取樣到的序列資料需要在n個時鐘週期後以並行的方式輸出,這是最基本的串入並出的設計思想。

例1:串轉並

使用暫存器cnt,每來一個數cnt即加1,同時以cnt作為索引放入對應位置的暫存器中,當加到符合要求的數值後就將暫存器中的數值輸出。

module serial_parallel(
    input clk,
    input rst_n,
    input data_i,
    output reg [7:0] data_o
);

reg [2:0] cnt;

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0) begin
      data_o <= 8'b0;
      cnt <= 3'b0;
    end
    else begin
        data_o[7 - cnt] <= data_i;                          //高位先賦值
        cnt <= cnt + 1'b1;
    end  
end

endmodule

另外一種思路是使用移位暫存器,即每儲存一個數據後即進行移位,將空的部分準備好儲存下一個資料。

module serial_parallel(
    input clk,
    input rst_n,
    input en,
    input data_i,
    output reg [7:0] data_o
);

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0) begin
      data_o <= 8'b0;
    end
    else if(en == 1'b1) begin
        data_o <= {data_o[6:0],data_i}; 
    end  
    else data_o <=data_o;
end

endmodule

例2:並轉串

module parallel_serial(
    input clk,
    input rst_n,
    input en,
    input [7:0] data_i,
    output data_o
);

reg [7:0] data_buf;

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0) begin
        data_o <=1'b0;
        data_buf <= 8'b0;
    end
    else if (en == 1'b1)
        data_buf <= data_i;
    else
        data_buf <= data_buf << 1;
end

assign data_o = data_buf[7];

endmodule

(三)流水線設計

可提高系統頻率,常用於高速訊號處理領域。

若某個設計可以分為若干步驟進行處理,而且整個資料處理過程是單向的,即沒有反饋運算或者迭代運算、前一個步驟的輸出即是下一個步驟的輸入,就可以採用流水線設計方法來提高系統的工作頻率。

典型的流水線設計是將原本一個時鐘週期完成的較大的組合邏輯通過合理的切割後分為由多個時鐘週期完成。因此,該部分邏輯執行的時鐘頻率會有明顯的提升,尤其當它是一條關鍵路徑,採用流水線設計後整個系統的效能都會得到提升。具體流水線實現可參見下圖:

(四)邏輯複製與模組複用

1、邏輯複製

邏輯複製是一種通過增加面積來改善時序條件的優化手段,最主要的應用是調整訊號的扇出。

如果某個訊號需要驅動的後級邏輯訊號很多,即扇出非常大,那麼為了增加這個訊號的驅動能力,就必須插入很多級的Buffer,也就在一定程度上增加了這個訊號的路徑延時。

這種情況下就可以複製生成這個訊號的邏輯,用多路同頻同相的訊號驅動後續電路,使平均到每路的扇出變低,這樣也就不需要插入Buffer,從而節約該訊號的路徑延時。

如:例1:

input a,b,c,d;
input sel;
output dout;
assign dout = sel ? (a+b):(c+d);

綜合出兩個加法器和一個二選一選擇器

例2:

input a,b,c,d;
input sel;
output dout;
wire ab,cd;
assign ab = sel ? a:c;
assign cd = sel ? b:d;
assign dout = ab + cd;

綜合出兩個二選一選擇器和一個加法器

第一種方法佔用資源多,但速度快些;第二種方法則正好相反。

第一種方法就是一種邏輯複製的設計方法,因為這個設計實現本來只需要一個加法器就可以了,但為了加快速度,就需要進行邏輯複製。第一種方法就是以增加面積為代價換來了速度。

當然,現在的很多綜合工具可以根據情況自動進行邏輯複製。

2、模組複用

就是邏輯複製的逆過程,好處在於節省面積,但會以犧牲速度為代價。

(五)模組化設計

模組劃分的基本原則:子模組功能相對獨立,模組內部聯絡儘量緊密,而模組間的連線儘量簡單。

一般情況下,不在頂層模組做任何邏輯設計。

(六)時鐘設計技巧

儘量避免使用FPGA內部邏輯產生的時鐘,因為它容易導致功能或時序出現問題。

1、內部邏輯產生的時鐘

組合邏輯產生的時鐘不可避免地會有毛刺出現,如果此時輸入埠的資料正處於變化過程,那麼它將違反建立和保持時間要求,從而影響後續電路的輸出狀態,甚至導致整個系統執行失敗。

處理辦法:在輸出時鐘或者復位訊號之前,再用系統專用時鐘訊號(通常指外部晶振輸入時鐘或者PLL處理後的時鐘訊號)打一拍,從而避免組合邏輯直接輸出,達到同步處理的效果。對於輸出的時鐘訊號或復位訊號,最好讓它走全域性時鐘網路,從而減小時鐘網路延時,提升系統時序效能。

2、分頻時鐘與使能時鐘

時鐘滿天飛是很不好的設計風格

通常用FPGA內嵌的PLL或者DLL進行時鐘管理,這種時鐘分頻也是最穩定的。

但若對於無法使用PLL或者DLL資源的器件,則使用使能時鐘設計。

在使能時鐘設計中只使用原有的時鐘,讓分頻訊號作為使能訊號來用。

例:需得到一個50MHz輸入時鐘的5分頻訊號即10MHz

input clk;                                          //50MHz時鐘訊號
input rst_n;                                        //寫使能訊號,低有效

reg [2:0] cnt;                                      //分頻計數暫存器
wire en;                                            //使能訊號,高電平有效

//5分頻計數0~4
always @(posedge clk or negedge rst_n)begin
    if(!rst_n) cnt <= 3'd0;
    else if(cnt < 3'd4) cnt <= cnt + 1'b1;
    else cnt <= 3'd0;
end

assign en = (cnt == 3'd4);                          //5個時鐘週期產生1個時鐘週期高脈衝

//使用使能時鐘
always @(posedge clk or negedge rst_n)
    if(!rst_n) ...;
    else if(en) ...;

使能訊號不直接作為時鐘使用,而是作為資料輸入端的選擇訊號,即可避免使用分頻時鐘。

3、門控時鐘

組合邏輯中多用門控時鐘,一般驅動門控時鐘的邏輯都是隻包含一個與門(或門),如果有其他的附加邏輯,容易因競爭產生不希望的毛刺。門控時鐘通過一個使能訊號控制時鐘的開或者關。當系統不工作時可以關閉時鐘,整個系統就處於未啟用狀態,這樣能在某種程度上降低系統功耗。但使用門控時鐘不符合同步設計的思想,可能會影響系統設計的實現和驗證。

這裡推薦一種既可以降低系統功耗,又能夠穩定可靠的替代門控時鐘。

對於上升沿有效的系統時鐘clk,它的下降沿先把門控訊號gating signal打一拍,然後再用這個使能訊號enable和系統時鐘clk相與後作為後續電路的門控時鐘。

五、基於FPGA的跨時鐘域訊號處理

對於一些複雜的應用,FPGA需要和多個時鐘域的訊號進行通訊。非同步時鐘域所設計的兩個時鐘之間不同頻不同相。

如上圖,對於接收域而言,來自發送域的訊號data_a2b有可能在任何時刻變化。如果出現建立時間或者保持時間違規,接收域將會受到處於亞穩態的資料,引起嚴重後果。

跨時鐘域的訊號成熟的基本思想是同步。

(一)同步設計思想

先舉反例:設計功能就是一個頻率計,FPGA除了脈衝計數外,還要響應CPU的讀取控制。

CPU的控制匯流排是指一個片選訊號和一個讀選通訊號,當二者都有效時,FPGA需要對CPU的地址匯流排進行譯碼,然後把取樣脈衝值送到CPU的資料匯流排上。如下圖為CPU的讀時序圖。

如果給出下面的以組合邏輯為主的實現方式,似乎可以。但對於這種時鐘滿天飛的設計,存在諸多亞穩態危害爆發的可能。脈衝訊號和由CPU控制匯流排產生的選通訊號是來自兩個非同步時鐘域的訊號,它們作為內部時鐘訊號時,如果同一時刻出現一個時鐘在寫暫存器counter,另一個時鐘在讀暫存器counter,那麼明視訊記憶體在著發生衝突的可能。(即若暫存器正處於改變狀態(被寫)時有讀取訊號產生了,問題就會隨之而來)。

input clk;
input rst_n;
input pulse;
input cs_n;
input rd_n;
input [3:0] addr_bus;

output reg [15:0] data_bus;

reg [15:0] counter;

always @(posedge pulse or negedge rst_n) begin
    if(!rst_n) counter <= 16'd0;
    else if(pulse) counter <= counter + 1'b1;
end

wire dsp_cs = cs_n & rd_n;

always @(dsp_cs or addr_bus) begin
    if(dsp_cs) data_bus <= 16'hzzzz;
    else begin
        case(addr_bus)
            4'h0:data_bus <= counter;
            4'h1:...;
            ...
            default:;
        endcase
    end
end

脈衝訊號pulse與CPU讀選通訊號cpu_cs是非同步訊號,pulse何時出現上升沿與cpu_cs何時出現下降沿都是不可控的。如果它們一起觸發了,那麼計數器counter[15:0]正在加1,這個自增過程還在進行中,CPU資料匯流排data_bus[15:0]來讀取counter[15:0],那麼到底讀取的值是自增之前的值還是自增之後的值又或者是其他的值呢?

如下圖所示,為一個計數器的近似模型。當計數器自增1的時候,如果最低位為0,那麼自增的結果只會使最低位翻轉;但是當最低位為1,自增的後果除了使最低位翻轉,還有可能使其它任何位翻轉,比如4'b1111自增1的後果會使4個位都翻轉。由於每個位之間從發生翻轉到翻轉完成都需要經過一段邏輯延時和走線延時,對於一個16位的計數器,要想使這16位暫存器的翻轉時間一致,那是不可能的。因此之前的設計出現衝突時被讀取的脈衝值很可能完全是錯誤的。

要想解決衝突的問題,必須運用同步設計的思想,把這兩個非同步時鐘域的訊號同步到一個時鐘域裡進行處理。

解決辦法:如下圖所示。先是使用脈衝檢測法把脈衝訊號與系統時鐘訊號clk同步,然後依然使用脈衝檢測法得到一個系統時鐘寬度的使能脈衝作為資料鎖存訊號,也就將CPU的控制訊號和系統時鐘訊號clk同步了。如此處理後,兩個非同步時鐘域的訊號就不存在任何讀/寫衝突的情況了。

(二)單向控制訊號檢測

(三)專用握手訊號

所謂握手,即通訊雙方使用了專用控制訊號進行狀態指示。這個控制訊號既有傳送域給接收域的,也有接收域給傳送域的。使用握手協議方式處理跨時鐘域資料傳輸時,只需要對雙方的握手訊號(req和ack)分別使用脈衝檢測方法進行同步。

具體實現,假設req,ack,data匯流排在初始化都處於無效狀態,傳送域先把資料放入匯流排,隨後傳送有效的req訊號給接收域;接收域在檢測到有效的req訊號後鎖存資料匯流排,然後回送一個有效的ack訊號表示讀取完成應答;傳送域在檢測到有效ack訊號後撤銷當前的req訊號,接收域在檢測到req撤銷後也相應撤銷ack訊號,此時完成一次正常握手通訊。此後傳送域開始繼續下一次握手通訊,如此迴圈。

該方式能夠使接收到的資料穩定可靠,有效避免了亞穩態的出現,但控制訊號握手檢測會消耗通訊雙方較多的時間。

module handshack(
    clk,rst_n,
    req,datain,ack,dataout);
    
input clk;                              //50MHz系統時鐘頻率
input rst_n;                            //低電平復位訊號
input req;                              //請求訊號,高電平有效
input [7:0] datain;                     //輸入資料
output ack;                             //應答訊號,高電平有效
output [7:0] dataout;                   //輸出資料,主要用於觀察是否和輸入一致

//————————————————————
//req上升沿檢測
reg reqr1,reqr2,reqr3;

always @(posedge clk or negedge rst_n)
    if(!rst_n) begin
        reqr1 <= 1'b1;
        reqr2 <= 1'b1;
        reqr3 <= 1'b1;
      end
    else begin
        reqr1 <= req;
        reqr2 <= reqr1;
        reqr3 <= reqr2;
      end
    end
//pos_req2比pos_req1延後一個時鐘週期,確保資料被穩定鎖存
wire pos_req1 = reqr1 & ~reqr2;
wire pos_req2 = reqr2 & ~reqr3;

//————————————————————
//資料鎖存
reg [7:0] dataoutr;

always @(posedge clk negedge rst_n) begin
    if(!rst_n) dataour <= 8'h00;
    else if(pos_req1) dataoutr <= datain;
end
assign dataout = dataoutr;

//————————————————————
//產生應答訊號ack
reg ackr;

always @(posedge clk or negedge rst_n) begin
    if(!rst_n) ackr <= 1'b0;
    else if(pos_req2) ackr <= 1'b1;
    else if(!req) ack <= 1'b0;
end

assign ack = ackr;

endmodule

(四)搞定亞穩態

所有的數字器件的訊號傳輸都會有一定的時序要求,從而保證每個暫存器將捕獲的輸入訊號正確輸出。為了確保可靠,輸入暫存器的訊號必須在時鐘沿的某段時間(暫存器的建立時間)之前保持穩定,並且持續到時鐘沿之後的某段時間(暫存器的保持時間)之後才能改變,而該暫存器的輸入反映到輸出則需要經過一定的延時。如果資料變化違反了建立時間和保持時間的要求,那麼暫存器的輸出就會處於亞穩態,即輸出會在高電平1和低電平0之間盤旋一段時間,這也意味著暫存器的輸出達到一個穩定的高或者低電平的狀態所需要的時間大於一般情況下的延時。

同步系統一般不會出現亞穩態問題,亞穩態問題多發生在一些跨時鐘域訊號的傳輸上。由於資料訊號可能在任何時間到達非同步時鐘域的目的暫存器,所以設計者無法保證滿足建立時間和保持時間的要求。然而並非所有違反建立時間和保持時間要求的訊號都會出現亞穩態。進入了亞穩態的暫存器恢復穩定的時間取決於器件的製造工藝與工作環境(大多數情況下會很快)。

暫存器在時鐘沿取樣資料訊號好比一個球從小山的一側拋到另一側。如下圖所示,小山的兩側代表資料的穩定狀態——舊的資料值或者新的資料值;山頂代表亞穩態。如果小球被拋到山頂上,但實際上它只要稍微有些動靜就會滾落山底。在一定時間內,球滾得越遠,它達到穩定狀態的時間也就越短。

如果資料訊號的變化發生在時鐘沿的保持時間之後,就好像球跌落倒了小山的左側,輸出訊號仍然保持時鐘變化前的值不變。如果資料訊號的變化發生在時鐘沿的建立時間之前,並且持續到時鐘沿之後的保持時間都不再變化,就好像球跌落到了小山的右側,輸出資料達到穩定狀態的時間就是一般狀態下的延時。然而一個暫存器的輸入資料違反了建立時間和保持時間的要求,就像球被拋到了山頂,如果球在山頂停留得越久,那麼它到達山底的時間也就越長,這就相應地延長了從時鐘變化到輸出資料達到穩定狀態的時間了。如下圖很好地闡述了亞穩態訊號。

當球到達山底的時間超過了扣除暫存器一般延時時間以外的餘量時間,那麼問題就隨之而來。

當訊號變化處於一個不相關的電路或者非同步時鐘域,它在被使用前就需要先被同步到新的時鐘域中。新的時鐘域中的第一個暫存器將扮演同步暫存器的角色。

為了儘可能消除亞穩態的影響,設計者一般在目的時鐘域使用一串連續的暫存器將訊號同步到新的時鐘域中,這些暫存器有額外的時間用於訊號在被使用前從亞穩態達到穩定值。同步暫存器到暫存器路徑的時序餘量,也就是亞穩態訊號到達穩態的最大時間,也被認為時亞穩態持續時間。

同步暫存器鏈被定義為一串達到以下要求的連續暫存器:

1、鏈中的暫存器都由相同的時鐘或者相位相關的時鐘觸發;

2、鏈中的第一個暫存器由不相關時鐘域或者非同步的時鐘來觸發;

3、每個暫存器的扇出值都為1,鏈中的最後一個暫存器可以例外。

設計者無法預測訊號變化的順序或者說訊號兩次變化間經過了幾個鎖存時鐘週期,因此使用雙時鐘FIFO傳輸訊號或使用握手訊號進行控制。

(五)藉助於儲存器

藉助於儲存器來完成跨時鐘域通訊也是很常用的手段。雙口RAM更適合於需要互通訊的設計,只要雙方對地址做好適當的分配,那麼剩下的工作只要控制好儲存器的讀/寫時序。FIFO本身的特性(先進先出)決定了它更適合單向的資料傳輸。

非同步FIFO在跨時鐘域通訊中的使用

FIFI兩側會有相對獨立的兩套控制匯流排。若寫入請求wrreq在寫入時鐘wrclk的上升沿處於有效狀態,那麼FIFO將在該時鐘沿鎖存寫入資料匯流排wrdata。同理,若讀請求rdreq在讀時鐘rdclk的上升沿處於有效狀態,那麼FIFO將把資料放到讀資料匯流排rddata上,外部邏輯一般在下一個有效時鐘沿讀取該資料。