FPGA跨時鐘域處理方法
跨時鐘域的訊號分為兩類,一類是單位元的訊號,一類是多位元的訊號。這兩類訊號無論是快時鐘域到慢時鐘域還是慢時鐘域到快時鐘域,無論是流資料還是控制訊號,都可以使用非同步FIFO進行同步。因此下文分類的不同情景,每一種情景都可以使用非同步FIFO進行同步,後文就不作介紹。但需要注意的是,快時鐘域到慢時鐘域的同步,在使用非同步FIFO時,快時鐘域平均流量是不能大於慢時鐘域的處理速度的,否則資料會丟失,這其實與是否使用FIFO來進行同步無關。因為FIFO的作用本就是在某一段持續時間內,傳送方傳送的資料大於接收方的處理能力時,暫時作為快取用的。若傳送方的平均流量大於接受方的處理能力,那麼除非FIFO無限大,否則隨著時間的增加,就會丟失資料。
1、單位元訊號
大部分文章介紹單位元訊號的跨時鐘域處理時,都是預設該單位元訊號為控制訊號或者說變化相對較慢的單位元脈衝訊號。但實際上單位元訊號也存在是流資料的可能,可能實際工程上單位元的流資料訊號較少,但是起碼理論上存在這種可能,如果不加以區分討論,初學者往往容易混淆。
1.1 單位元脈衝訊號
1.1.1 慢時鐘域到快時鐘域
慢時鐘域的單位元脈衝訊號同步至快時鐘域,可以採用多級暫存器的方法,也就是將單位元脈衝訊號在快時鐘域打多拍。一般情況下只需要採用兩級暫存器即可,因為更多級的暫存器對效能提升並不明顯。前提條件要求從慢時鐘域到快時鐘域,原因是隻有慢時鐘域到快時鐘域,才能保證慢時鐘域的脈衝訊號能被快時鐘域取樣到。
需要注意的是,多級暫存器的主要作用是避免亞穩態的傳播(不能完全消除亞穩態,但可以使亞穩態出現的概率大大降低),並不能保證資料穩定後,是正確的值,而是隨機的0或1。但是由於慢時鐘域的脈衝訊號持續時間大於快時鐘域的一個週期,因此在快時鐘域的下一個上升沿到來時,慢時鐘域的脈衝訊號仍然持續,此時快時鐘域可以採到正確的值。也就是說,出現亞穩態時,快時鐘域實際上需要已經對慢時鐘域的訊號進行第二次取樣了。顯然地,第二次取樣時,需要滿足建立時間與保持時間,否則可能會再次出現亞穩態。因此可以看出快時鐘域與慢時鐘域的關係並不是任意的,兩者並不能接近到無法滿足第二次取樣的建立時間和保持時間。快時鐘域域慢時鐘域需要滿足下面的條件:Tslow>Tfast+Thold+Tsetup,其中Tslow為慢時鐘域的時鐘週期,Tfast為快時鐘域的時鐘週期,Thold與Tsetup分別為快時鐘域暫存器的保持時間和建立時間。通過上面的討論我們可以發現,當使用多級暫存器時,如果出現了亞穩態,快時鐘域 能夠 採到慢時鐘域訊號所需的時間比沒有出現亞穩態時可能會多一個週期。如果有彼此關聯的兩個多位元訊號,比如說地址訊號,它們從慢時鐘域同步至快時鐘域時,可能到達快時鐘域的時間時不一樣的,那麼得到的地址就是錯誤的,這就是多位元訊號即使是從慢時鐘域到快時鐘域,也不能夠使用多級暫存器同步的原因。但如果多位元訊號彼此無關,從慢時鐘域到快時鐘域時,是可以使用多級暫存器同步的。
程式碼如下:
- module syn(
- input rsta_n,
- input clka,
- input dataa,
- input rstb_n,
- input clkb,
- output datab
- );
- reg syn1;
- reg syn2;
- always@(posedge clkb or negedge rstb_n) begin
- if(!rstb_n) begin
- syn1<=1'b0;
- syn2<=1'b0;
- end
- else begin
- syn1<=level;
- syn2<=syn1;
- end
- end
- assign datab=syn2;
- endmodule
2、快時鐘域到慢時鐘域
(1)使用握手訊號
快時鐘但慢時鐘域的單位元訊號同步可以使用握手訊號。握手訊號的使用相對來說耗時較長,如果快時鐘域的訊號變化較快,是無法使用握手訊號來進行同步的,否則慢時鐘域可能會漏採快時鐘域的訊號。(下圖輸出的不是脈衝訊號而是電平訊號,與下文的程式碼有點區別)
verilog程式碼如下:
- module syn(
- input clka,
- input rsta_n,
- input bit_in,
- input clkb,
- input rstb_n,
- output bit_out
- );
- reg req;
- reg req_f;
- reg req_ff;
- reg req_fff;
- wire ack;
- reg ack_f;
- reg ack_ff;
- //使用req訊號對a時鐘域資料進行保持
- always@(posedge clka or negedge rsta_n) begin
- if(!rsta_n)
- req<=1'b0;
- else if(ack_ff)//ack訊號為高時,不接收新的資料
- req<= 1'b0;
- else if(bit_in)
- req<=1'b1;
- else
- req<=req;
- end
- //將a時鐘域的req訊號同步至b時鐘域
- always@(posedge clkb or negedge rstb_n) begin
- if(!rstb_n) begin
- req_f <=1'b0;
- req_ff<=1'b0;
- end
- else begin
- req_f <=req;
- req_ff<=req_f;
- end
- end
- //在b時鐘域產生單個數據脈衝
- always@(posedge clkb or negedge rstb_n) begin
- if(!rstb_n)
- req_fff<=1'b0;
- else
- req_fff<=req_ff;
- end
- assign bit_out=~req_fff&req_ff;
- //將b時鐘域的ack訊號同步至a時鐘域
- assign ack=req_ff;
- always@(posedge clka or negedge rsta_n) begin
- if(!rsta_n) begin
- ack_f <= 1'b0;
- ack_ff <= 1'b0;
- end
- else begin
- ack_f <= ack;
- ack_ff <= ack_f;
- end
- end
- endmodule
testbench:
- `timescale 1ns / 1ps
- module tb(
- );
- reg clka,clkb;
- reg bit_in;
- reg rsta_n,rstb_n;
- wire bit_out;
- syn test(
- .clka(clka),
- .rsta_n(rsta_n),
- .bit_in(bit_in),
- .clkb(clkb),
- .rstb_n(rstb_n),
- .bit_out(bit_out)
- );
- initial begin
- clka=1'b0;
- clkb=1'b1;
- rsta_n=1'b0;
- rstb_n=1'b0;
- bit_in=1'b0;
- #28
- rsta_n=1'b1;
- rstb_n=1'b1;
- end
- always #2 clka=~clka;
- always #7 clkb=~clkb;
- initial begin
- #98
- bit_in =1'b1;
- #4
- bit_in =1'b0;
- #60
- bit_in =1'b1;
- #4
- bit_in =1'b0;
- #8
- bit_in =1'b1;
- #4
- bit_in =1'b0;
- #300
- bit_in =1'b1;
- end
- endmodule
模擬時序圖:
從模擬圖可以看出,對於快時鐘域的兩個脈衝離得比較近的話,慢時鐘域是會漏採的,使用握手訊號時,對此需要注意。
(2)T觸發器 + 多級觸發器
對於單位元的脈衝訊號,我們也可以使用T觸發器 + 多級觸發器的方法來進行同步,這種方法相較於使用握手訊號所需時間較短,但沒有ack訊號,無法判斷接受方是否接受到了脈衝訊號。因此使用時一定要保證 滿足使用條件。
T觸發器的真值表達式為 Qn+1 =T⊕Qn。總結來說的話,就是每來一個週期的高電平,輸出就翻轉一次。我們利用這個特性,可以將單位元的訊號展寬。就是說在兩個脈衝之間的訊號是保持不變的,不管保持的是0還是1並不重要,我們只要知道脈衝到來之時,T觸發器的輸出會翻轉就足夠了。只要訊號發生了變化,我們在進行同步的時鐘域多打一拍,並與前一拍的訊號進行異或就可以得到一個週期的脈衝,雖然b時鐘域採到的並不是脈衝,但是異或之後得到的就是一個脈衝。
這種方法的本質實際上是將訊號展寬,只不過展寬的訊號可能是0也可能是1。但很顯然,a時鐘域的兩個脈衝間隔要足夠大,因為兩個脈衝的訊號的間隔就是a時鐘域的訊號持續的時間。如果這個時間太短,在b時鐘域是無法採到的。兩個脈衝之間的間隔要大於Tb+Thold+Tsetup,其中Tb為b時鐘域的時鐘週期,Thold與Tsetup分別為b時鐘域暫存器的保持時間和建立時間。
verilog程式碼如下:
- module syn(
- input rsta_n,
- input clka,
- input plusea,
- input rstb_n,
- input clkb,
- output pluseb
- );
- reg level;
- reg syn1;
- reg syn2;
- reg syn2_f;
- //將a時鐘域的脈衝訊號轉為電平訊號
- always@(posedge clka or negedge rsta_n) begin
- if(!rsta_n)
- level<=1'b0;
- else if(plusea)
- level<=~level;
- else
- level<=level;
- end
- //用兩級暫存器同步電平訊號
- always@(posedge clkb or negedge rstb_n) begin
- if(!rstb_n) begin
- syn1<=1'b0;
- syn2<=1'b0;
- end
- else begin
- syn1<=level;
- syn2<=syn1;
- end
- end
- //在b時鐘域將同步過來的電平訊號轉為脈衝訊號
- always@(posedge clkb or negedge rstb_n) begin
- if(!rstb_n)
- syn2_f<=1'b0;
- else
- syn2_f<=syn2;
- end
- assign pluseb=syn2^syn2_f;
- endmodule
1.2單位元流資料
對於單位元流資料而言,無論是快時鐘域到慢時鐘域,還是慢時鐘域到快時鐘域,如果不使用RAM或者FIFO這類儲存空間,想直接將資料通過流的方式進行同步,是無法做到的。這是因為兩個時鐘域的時鐘週期長度不一樣,隨著時間的積累,一定會發生資料的錯位。因此若想同步跨時鐘域的流資料,必須要藉助儲存器空間,否則是無法同步流資料的。需要注意的是,快時鐘域到慢時鐘域的流資料,是不能一直持續的,否則就需要無限大的儲存空間,這在文章開頭已經提到了。
2、多位元訊號
2.1 多位元單
(1)方法一:DUUX實現CDC
控制訊號tx_sel經兩級暫存器同步後作為多路選擇器的sel訊號,cdc_d為傳送時鐘域多位元資料。tx_sel訊號與cdc_d訊號都需要持續一定的時間以保證能被接收時鐘域採到。
2.2 多位元流資料
分析方法同但位元的流資料。
---------------------------------------------------------------------------------------------------------------------------------
最後需要說明的一點是,除了非同步FIFO,當一個時鐘域的訊號送入另一個時鐘域時,都需要另一個時鐘域使用兩級暫存器進行打拍,這是為了避免出現亞穩態的傳播。但需要注意的是,一個時鐘域的訊號送入另一個時鐘域時,這個訊號必須時暫存器輸出的。這是因為若不是暫存器輸出,輸入另一個時鐘域時,就可能產生毛刺,會加大出現亞穩態的概率。雖然說即使出現亞穩態,多級暫存器同步後大概率資料也是會穩定下來的,但是發生故障的概率會隨著亞穩態出現次數的增加而增加,系統的穩定性會受到影響。更為重要的是,毛刺產生的亞穩態會導致出現一個不想要出現的0或1。這我們之前討論的亞穩態有所不同,之前討論的出現了亞穩態,資料穩定下來後,最多也就是資料推遲一個週期來到,但這個資料我們還是需要的。但是因毛刺產生的亞穩態穩定之後,產生的0或1是我們不需要的,這個不需要的0或1如果出現在後續的電路中,且後續的電路有較強的因果關係時,整個系統都會出現錯誤,且難以排查。
參考連結:
1.你真的懂2-flop synchronizer嗎-- CDC的那些事(2)