跨时钟域处理

跨时钟域处理是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来达到同样的目的。
 

猜你喜欢

转载自www.cnblogs.com/firetribe/p/8862308.html