实现双时钟域数据的交换,最常见的方法就是采用双时钟的异步fifo。但是对于单根信号线,如果仍然采用异步fifo就显得非常stupid,这时,往往通过两级触发器来实现同步。
那么,两级触发器是如何实现同步的呢?本小节就做一个简单的分析和验证。
下面是我刚刚从ORPSoC的工程里看到的一段代码:
//'ddr2_writeback_done':generate in ddr2_if_clk domain always @(negedge ddr2_if_clk) if (ddr2_rst) ddr2_writeback_done <= 0; else if (ddr2_writeback_done_wb_sync2) ddr2_writeback_done <= 0; else if (ddr2_write_state_shr[6]) ddr2_writeback_done <= 1; //'ddr2_writeback_done' sync to 'wb_writeback_done' in wb_clk domain always @(posedge wb_clk) if (wb_rst) begin wb_writeback_done_sync <= 0; wb_writeback_done_sync2 <= 0; end else begin wb_writeback_done_sync <= ddr2_writeback_done; wb_writeback_done_sync2 <= wb_writeback_done_sync; end assign wb_writeback_done = !wb_writeback_done_sync2 & wb_writeback_done_sync; //use 'wb_writeback_done' in wb_clk domain always @(posedge wb_clk) if (wb_rst) do_writeback <= 0; else if (wb_writeback_done) do_writeback <= 0; else if (start_writeback) do_writeback <= 1;
这段代码就可以实现两个时钟域(wb_clk和ddr2_if_clk)间ddr2_writeback_done信号的同步。
为了更清晰的展示其具体的同步过程,我写了一个简单的test case。
a,可综合的sync.v:
/* * file name :sync.v * author :Rill * date :2014-04-12 */ module sync ( input clk_a, input rst_a, input enable_a, input clk_b, input rst_b, output enable_b ); reg signal_a; reg sync1; reg sync2; wire signal_b; reg enable; assign enable_b = enable; //'signal_a':generated in clk_a domain always @(negedge clk_a) begin if (rst_a) signal_a <= 0; else if (sync2) signal_a <= 0; else if (enable_a) signal_a <= 1; end //'signal_a' sync to 'signal_b' in wb_clk domain always @(posedge clk_b) begin if (rst_b) begin sync1 <= 0; sync2 <= 0; end else begin sync1 <= signal_a; sync2 <= sync1; end end assign signal_b = !sync2 & sync1; //use 'signal_b' in wb_clk domain always @(posedge clk_b) begin if (rst_b) enable <= 1'b0; else if (signal_b) enable <= 1'b1; else enable <= 1'b0; end endmodule /********* EOF *************/
b,不可综合的sync_tb.v:
/* * file name :sync_tb.v * author :Rill * date :2014-04-12 */ `timescale 1ns/1ns module sync_tb; reg clk_a; reg rst_a; reg enable_a; reg clk_b; reg rst_b; wire enable_b; parameter CLK_A_PERIOD = 10; parameter CLK_B_PERIOD = 30; //gen clk_a always #(CLK_A_PERIOD/2) clk_a = ~clk_a; //gen clk_b always #(CLK_B_PERIOD/2) clk_b = ~clk_b; //gen clk_a domain test pattern integer m; initial begin //rst #0 clk_a = 1'b0; repeat (10) @(negedge clk_a); rst_a = 1'b1; repeat (10) @(negedge clk_a); rst_a = 1'b0; //wait clk_b domain rst done repeat (1000) @(negedge clk_a); //gen test signal for(m=0;m<10;m=m+1) begin enable_a =1'b1; @(negedge clk_a); enable_a =1'b0; repeat (100) @(negedge clk_a); end repeat (1000) @(negedge clk_a); $stop; end initial begin //rst #0 clk_b = 1'b0; repeat (10) @(posedge clk_b); rst_b = 1'b1; repeat (10) @(posedge clk_b); rst_b = 1'b0; end sync SYNC0 ( .clk_a (clk_a), .rst_a (rst_a), .enable_a (enable_a), .clk_b (clk_b), .rst_b (rst_b), .enable_b (enable_b) ); endmodule /********* EOF *************/
下面是前仿的波形:
从中可以看出:
a,clk_a是下降沿触发,clk_b是上升沿触发。
b,先看m信号上面的波形:
clk_a时钟域产生了一个enable_a信号,这个信号被clk_b时钟域detect到了(enable_b信号)。
enable_a信号是clk_a时钟域下降沿同步的。enable_b信号是clk_b时钟域上升沿同步的。
那么具体是如何实现同步的呢?我们再看m信号下面的波形。
c,首先clk_a时钟域下降沿产生一个周期enable_a信号。
d,这个信号寄存到和他同一时钟域的signal_a。
e,异步的,clk_b时钟域上升沿采集到signal_a并将之寄存到本时钟域的sync1。
f,在clk_b时钟域,经过一个cycle,将sync1传给sync2。这时,即可采集到signal_b信号,而这个signal_b信号就是在clk_b时钟域的。
g,异步的,在clk_a时钟域,在检测到sync2有效以后的第一个下降沿,将寄存enable_a信号的signal_a清除。
h,整个同步过程用了6个clk_a周期,2个clk_b周期。
从上面的分析,我们可以看出,采用两级触发器确实可以实现双时钟域信号的同步,但是这种方式也不是随便任何时候都能使用的,如果是慢时钟域同步快时钟域的信号,则要求快时钟域的信号产生的不能过快,否则将会丢失部分信号。
还是上面的sync.v,我们修改sync_tb.v中enable_a产生之后等待的时间,就会发现有丢失的情况发生。
修改后的sync_tb.v:
/* * file name :sync_tb.v * author :Rill * date :2014-04-12 */ `timescale 1ns/1ns module sync_tb; reg clk_a; reg rst_a; reg enable_a; reg clk_b; reg rst_b; wire enable_b; parameter CLK_A_PERIOD = 10; parameter CLK_B_PERIOD = 30; //gen clk_a always #(CLK_A_PERIOD/2) clk_a = ~clk_a; //gen clk_b always #(CLK_B_PERIOD/2) clk_b = ~clk_b; //gen clk_a domain test pattern integer m; initial begin //rst #0 clk_a = 1'b0; repeat (10) @(negedge clk_a); rst_a = 1'b1; repeat (10) @(negedge clk_a); rst_a = 1'b0; //wait clk_b domain rst done repeat (1000) @(negedge clk_a); //gen test signal for(m=0;m<10;m=m+1) begin enable_a =1'b1; @(negedge clk_a); enable_a =1'b0; repeat (3) @(negedge clk_a);//modify!!! end repeat (1000) @(negedge clk_a); $stop; end initial begin //rst #0 clk_b = 1'b0; repeat (10) @(posedge clk_b); rst_b = 1'b1; repeat (10) @(posedge clk_b); rst_b = 1'b0; end sync SYNC0 ( .clk_a (clk_a), .rst_a (rst_a), .enable_a (enable_a), .clk_b (clk_b), .rst_b (rst_b), .enable_b (enable_b) ); endmodule /********* EOF *************/
从中可以看出,clk_a时钟域一共产生了10次enab_a,但是clk_b时钟域只detect到了4次。
上面是从快时钟同步到慢时钟,如果快时钟域的信号产生频率太高的话,就会造成丢失。
那么如果是从慢时钟同步到快时钟呢?
将clk_a和clk_b的时钟周期互换,每隔一个clk_a就产生一个enable_a信号,结果如何呢?
修改后的sync_tb.v:
/* * file name :sync_tb.v * author :Rill * date :2014-04-12 */ `timescale 1ns/1ns module sync_tb; reg clk_a; reg rst_a; reg enable_a; reg clk_b; reg rst_b; wire enable_b; parameter CLK_A_PERIOD = 30; parameter CLK_B_PERIOD = 10; //gen clk_a always #(CLK_A_PERIOD/2) clk_a = ~clk_a; //gen clk_b always #(CLK_B_PERIOD/2) clk_b = ~clk_b; //gen clk_a domain test pattern integer m; initial begin //rst #0 clk_a = 1'b0; repeat (10) @(negedge clk_a); rst_a = 1'b1; repeat (10) @(negedge clk_a); rst_a = 1'b0; //wait clk_b domain rst done repeat (1000) @(negedge clk_a); //gen test signal for(m=0;m<10;m=m+1) begin enable_a =1'b1; @(negedge clk_a); enable_a =1'b0; repeat (1) @(negedge clk_a);//modify!!! end repeat (1000) @(negedge clk_a); $stop; end initial begin //rst #0 clk_b = 1'b0; repeat (10) @(posedge clk_b); rst_b = 1'b1; repeat (10) @(posedge clk_b); rst_b = 1'b0; end sync SYNC0 ( .clk_a (clk_a), .rst_a (rst_a), .enable_a (enable_a), .clk_b (clk_b), .rst_b (rst_b), .enable_b (enable_b) ); endmodule /********* EOF *************/
下面是修改后的前仿波形:
从中可以看出,即使慢时钟域每隔一个周期产生一个enable_a信号(一共10次),快时钟域也不会丢失(也检测到10次)。
本小节我们对采用两级触发器实现双时钟域同步的问题进行了简单分析,其实除了快慢时钟的问题,还有亚稳态的问题,由于是前仿,没有延迟信息,所以看不到亚稳态情况。