跨時鍾域處理是FPGA設計中經常遇到的問題,而如何處理好跨時鍾域間的數據,可以說是每個FPGA初學者的必修課。如果是還在校的學生,跨時鍾域處理也是
面試中經常被問到的一個問題。
脈沖信號:跟隨時鍾,信號發生轉變。
電平信號:不跟隨時間,信號發生轉變。
1、單bit的異頻傳輸
主要分為兩種情況。
第一種:信號從B到A,(慢到快)

在時鍾域B下的脈沖信號pulse_b在時鍾域A看來,是一個很寬的“電平”信號會,保持多個clk_a的時鍾周期,所以一定能被clk_a采到。經驗設計采集過程必須寄存兩拍。第一拍將輸入信號同步化,同步化后的輸出可能帶來建立/保持時間的沖突,產生亞穩態。需要再寄存一拍,減少亞穩態帶來的影響。一般來說兩級是最基本要求,如果是高頻率設計,則需要增加寄存級數來大幅降低系統的不穩定性。也就是說采用多級觸發器來采樣來自異步時鍾域的信號,級數越多,同步過來的信號越穩定。
特別需要強調的是,此時pulse_b必須是clk_b下的寄存器信號,如果pulse_b是clk_b下的組合邏輯信號,一定要先在clk_b先用D觸發器(DFF)抓一拍,再使用兩級DFF向clk_a傳遞。這是因為clk_b下的組合邏輯信號會有毛刺,在clk_b下使用時會由setup/hold時間保證毛刺不會被clk_b采到,但由於異步相位不確定,組合邏輯的毛刺卻極有可能被clk_a采到。一般代碼設計如下:
always @ (posedge clk_a or negedge rst_n) begin if (rst_n == 1'b0) begin pules_a_r1 <= 1'b0; pules_a_r2 <= 1'b0; pules_a_r3 <= 1'b0; end else begin //打3拍 pules_a_r1 <= pulse_b; pules_a_r2 <= pules_a_r1; pules_a_r3 <= pules_a_r2; end end assign pulse_a_pos = pules_a_r2 & (~pules_a_r3); //上升沿檢測 assign pulse_a_neg = pules_a_r3 & (~pules_a_r2); //下降沿檢測 assign pulse_a = pules_a_r2;
應該很多人都會問,為什么是打兩拍呢,打一拍、打三拍行不行呢?
先簡單說下兩級寄存器的原理:兩級寄存是一級寄存的平方,兩級並不能完全消除亞穩態危害,但是提高了可靠性減少其發生概率。總的來講,就是一級概率很大,三級改善不大。
這樣說可能還是有很多人不夠完全理解,那么請看下面的時序示意圖:

data是時鍾域1的數據,需要傳到時鍾域2(clk)進行處理,寄存器1和寄存器2使用的時鍾都為clk。假設在clk的上升沿正好采到data的跳變沿(從0變1的上升沿,實際上的數據跳變不可能是瞬時的,所以有短暫的跳變時間),那這時作為寄存器1的輸入到底應該是0還是1呢?這是一個不確定的問題。所以Q1的值也不能確定,但至少可以保證,在clk的下一個上升沿,Q1基本可以滿足第二級寄存器的保持時間和建立時間要求,出現亞穩態的概率得到了很大的改善。
如果再加上第三級寄存器,由於第二級寄存器對於亞穩態的處理已經起到了很大的改善作用,第三級寄存器在很大程度上可以說只是對於第二級寄存器的延拍,所以意義是不大的。
第二種:信號從A到B(快到慢)
如果單bit信號從時鍾域A到時鍾域B,那么存在兩種不同的情況,傳輸脈沖信號pulse_a或傳輸電平信號level_a。實際上,在一般情況下只有電平信號level_a的寬度能被clk_b采集到才可以保證系統正常工作。那么對於脈沖信號pulse_a采取怎樣的處理方法呢?可以用一個展寬信號來替代pulse_a實現垮時鍾域的握手。
主要原理就是先把脈沖信號在clk_a下展寬,變成電平信號signal_a,再向clk_b傳遞,當確認clk_b已經“看見”信號同步過去之后,再清掉signal_a。代碼通用框架如下:
module Sync_Pulse ( clk_a, clk_b, rst_n, pulse_a_in, pulse_b_out, b_out ); /****************************************************/ input clk_a; input clk_b; input rst_n; input pulse_a; output pulse_b_out; output b_out; /****************************************************/ reg signal_a; reg signal_b; reg signal_b_r1; reg signal_b_r2; reg signal_b_a1; reg signal_b_a2; /****************************************************/ //在時鍾域clk_a下,生成展寬信號signal_a always @ (posedge clk_a or negedge rst_n) begin if (rst_n == 1'b0) signal_a <= 1'b0; else if (pulse_a_in) //檢測到到輸入信號pulse_a_in被拉高,則拉高signal_a signal_a <= 1'b1; else if (signal_b_a2) //檢測到signal_b1_a2被拉高,則拉低signal_a signal_a <= 1'b0; else; end //在時鍾域clk_b下,采集signal_a,生成signal_b always @ (posedge clk_b or negedge rst_n) begin if (rst_n == 1'b0) signal_b <= 1'b0; else signal_b <= signal_a; end //多級觸發器處理 always @ (posedge clk_b or negedge rst_n) begin if (rst_n == 1'b0) begin signal_b_r1 <= 1'b0; signal_b_r2 <= 1'b0; end else begin signal_b_r1 <= signal_b; //對signal_b打兩拍 signal_b_r2 <= signal_b_r1; end end //在時鍾域clk_a下,采集signal_b_r1,用於反饋來拉低展寬信號signal_a always @ (posedge clk_a or negedge rst_n) begin if (rst_n == 1'b0) begin signal_b_a1 <= 1'b0; signal_b_a2 <= 1'b0; end else begin signal_b_a1 <= signal_b_r1; //對signal_b_r1打兩拍,因為同樣涉及到跨時鍾域 signal_b_a2 <= signal_b_a1; end end assign pulse_b_out = signal_b_r1 & (~signal_b_r2); assign b_out = signal_b_r1; endmodule
總而言之,在設計中可以簡單的牢記以下五條原則:
1. 再全局時鍾的跳變沿最可靠。
2. 來自異步時鍾域的輸入需要寄存一次以同步化,再寄存一次以減少亞穩態帶來的影響。
3. 不需要用到跳變沿的來自同一時鍾域的輸入,沒有必要對信號進行寄存。
4. 需要用到跳變沿的來自同一時鍾域的輸入,寄存一次即可。
5. 需要用到跳變沿的來自不同時鍾域的輸入,需要用到3個觸發器,前兩個用以同步,第3個觸發器的輸出和第2個的輸出經過邏輯門來判斷跳變沿。
2、多bit傳輸(異頻問題)
處理多bit數據的跨時鍾域,一般采用異步雙口RAM。假設我們現在有一個信號采集平台,ADC芯片提供源同步時鍾60MHz,ADC芯片輸出的數據在60MHz的時鍾上升沿變化,而FPGA內部需要使用100MHz的時鍾來處理ADC采集到的數據(多bit)。
在這種類似的場景中,我們便可以使用異步雙口RAM來做跨時鍾域處理。先利用ADC芯片提供的60MHz時鍾將ADC輸出的數據寫入異步雙口RAM,然后使用100MHz的時鍾從RAM中讀出。
對於使用異步雙口RAM來處理多bit數據的跨時鍾域,相信大家還是可以理解的。當然,在能使用異步雙口RAM來處理跨時鍾域的場景中,也可以使用異步FIFO來達到同樣的目的。