FPGA逻辑设计回顾(4)亚稳态与单比特脉冲信号的CDC处理问题

2021-01-26 15:51:34 浏览数 (1)

  • 前言
  • 时钟域以及跨时钟域的概念
  • 亚稳态的概念
  • 单脉冲信号的跨时钟域处理
  • 参考资料

前言

注:本文首发自易百纳技术社区,原文地址:https://www.ebaina.com/articles/140000005331

公众号文章不能添加外链接,为了更好的阅读体验,请选择阅读原文!

另外,请近期路过的朋友投个csdn年度博客之星的票,博主需要你的鼓励。https://bss.csdn.net/m/topic/blog_star2020/detail?username=reborn_lee

本文是上一篇文章FPGA逻辑设计回顾(3)多比特信号上升沿检测的设计方式与陷阱?的姊妹篇,都是FPGA以及ASIC设计中再重要不过的设计且应用场景十分广泛,我在以前也分享过类似的设计,但本文在大量参考外文文献的基础上,重新立意,重新组织,相信经过时间与设计经验的积累,会有更清晰更规范的表述。既然是具有分享意义的技术教程,本文分享的RTL设计的原则应是以看得懂、能说明问题为宗旨,不追求复杂隐晦似的“高端大气”。

时钟域以及跨时钟域的概念

通俗地讲,时钟域就是时钟的管辖范围,在我的管辖范围之内的逻辑由我来提供时钟,这样一来,跨时钟域就是数据在不同时钟域之间的交互!从一个时钟域到另一个时钟域,环境(时钟快慢、相位)可能不同,这样就可能导致一系列的问题,轻则亚稳态(水土不服生病),重则影子都没(对方检测不到,当成不存在)了。

上面的描述主要让你更通俗地了解时钟域以及跨时钟域的意思,但是上不了“台面”,写到答卷上要斟酌。一般来说, 逻辑设计中将所有同步元件(例如触发器和RAM等)使用相同时钟信号的部分称为时钟域

各大FPGA厂家的FPGA编译工具(这是习惯性叫法)在逻辑综合以及实现之后都会出一个时序报告,里面就有跨时钟域的报告,在里面你可以看到你有哪些信号进行了跨时钟域。如下图为Vivado工具的报告位置:

时序报告与跨时钟域路径

对于这些跨时钟域的情况,一般我们要在逻辑设计的时候就解决,当然之后也要对其进行约束,一般可以设置为false path等,即让综合工具不要机关算尽般去布局布线让时序满足要求(这会拖慢编译时间,当然时序也不会成功),设置为false path或者时钟组之后,工具默认不对其进行时序分析(因为设计中已经解决了跨时钟域的问题,这也就是为什么输跨时钟域问题是设计解决的,而不是约束解决的)。

亚稳态的概念

上面说了如果信号从一个时钟域到达另一个时钟域会出现一系列问题,就像人因水土不服一样,信号也会“生病”,最常见的症状就是亚稳态!什么是亚稳态呢?

对于这个问题,我觉得使用一个简单的示例来解释会更清晰,如下图:

两个不同的时钟域之间的信号交互

可能的场景可以为在时钟域A中生成了一个使能信号En_Out去触发时钟域B内的算法,总之时钟域B内的逻辑需要时钟域A中生成的使能控制信号。

注意:时钟域A中的使能信号En_Out有效状态至少持续一个时钟周期的时间,否则称不上脉冲,只能叫做毛刺!

假设时钟1(clk1)和时钟2(clk2)的波形关系以及使能信号En_Out的波形图如下:

跨时钟域传输的波形时序分析

上图还给出了各个信号之间的时序关系,Tclk-to-Q,DFF1的含义为信号从被clk1采样到输出(En_Out是输出)之间的延迟(器件是有延迟的,触发器也不例外!)Tsetup,DFF2的含义是触发器DFF2的建立时间(setup time)要求,被采样信号不要在时钟的建立时间范围内翻转(变化),才能满足触发器的建立时间要求,否则就可能导致亚稳态。

可见,上述时序图满足关系

即信号En_Out的翻转时刻不在时钟clk2的建立时间范围内,满足建立时间要求,因此可以正常采样,不存在亚稳态问题。

为了更清晰的描述,下图用箭头和标号给出了上述描述的示意,帮助理解。

正常时序关系

但是问题是不一定都像上述设计那样幸运,如果二者的时钟关系如下呢?

异常时序关系

上述的时序参数应该不需要我重复了,可以清楚地看到两个时钟之间的上升沿很近,这会导致时钟clk1下的信号输出变化时刻出现在时钟clk2的建立时间范围内,导致不满足时钟clk2的建立时间,而就是这个不满足建立时间,就会导致亚稳态。

导致这种情况出现的原因可能还不只进行数据交互的时钟采样沿过近这一条,还可能下图的情况:

布线延迟过大导致建立时间不足

也就是说,时钟二者之间的布线延迟过大(上面的分析未考虑),导致信号En_Out的翻转处传输到时钟clk2时,进入了DFF2的建立时间范围。下面我们总结,如果不满足建立时间要求导致的亚稳态会导致输出的结果是怎么样,如下图:

亚稳态

  • 作为第一种情况,假设DFF2的输出值在t=t2的clk2上升沿时进入逻辑高电平。在这种情况下,虽然我们有建立时间违规,但没有错误,并且触发器包含有效数据。数据的转换符合预期,没有错误。
  • 第二种情况:假设在t= t2的clk2上升沿时,DFF2输出转为逻辑低电平。在这种情况下,使能信号在B时钟域没有成功采样。然而,这不会是一个问题,因为En_Out来自A时钟域,它将至少在clk1的一个周期内为高电平, 因此,下一个上升沿clk2在 t = t 3 将正确地采样En_Out值。对于这个时钟边沿,DFF2的时序要求将得到满足,因为En_Out的变化没有超过clk2的一个周期。在这种情况下,我们对En_Out的采样比它实际转换的时间晚了一个时钟周期。然而,这并不是一个问题,因为两个时钟域的时钟被假设为独立的,我们没有对En_Out信号的到达时间做任何假设。如下图:

t3处采样

  • 接下来假设DFF2寄存器进入了亚稳态状态,在这种情况下,寄存器的输出变成了介于逻辑高电压和逻辑低电压之间的电压,但这只是暂时的。触发器最终将退出metastable状态,进入逻辑高或逻辑低状态。退出亚稳态所需的时间被称为resolution Time(Tr)

决断时间

如上图的绿色方框部分就是resolution time,定义为处于不稳定状态的时间或者退出亚稳态的时间,可以翻译为决断时间。其实从字面意思上也挺好理解的,决断即决定输出是高还是低的时间,一旦过了这个时间,输出的电平状态就确定了。

最后,我们对亚稳态进行总结:

在任何设计中使用的每个触发器(FF)都有一个指定的设置和保持时间,或者说在采样时钟边沿之前和之后,数据输入在规则上不允许改变的时间。

由于DFF2的输入数据在建立时间内发生了变化,寄存器的行为将是不可预测的。由于建立时间的违反,寄存器的输出电压可能是代表逻辑高、逻辑低,甚至更糟糕的是介于逻辑高和逻辑低之间的电压。

这三种情况都有可能发生,而输入数据在相应的时钟边沿实际上是逻辑高电平。同样,当违反寄存器保持时间时,即En_Out在寄存器保持时间定义的活动时钟边沿后的时间窗口内发生变化时,寄存器的输出值将无法预测。

亚稳态是指触发器无法在特定时间内达到已知状态。当触发器进入亚稳状态时,您既无法预测元件的输出电压电平,也无法预测输出何时将稳定至正确的电压电平。在此稳定时间内,触发器的输出处于某个中间电压电平,或者可能振荡,并且可以级联无效的输出电平,以使触发器在信号路径的更下方。

既然跨时钟域传输会有可能出现亚稳态,那么如何解决这个问题呢?答案我们接着看下面的小专题,单脉冲信号的跨时钟域处理,就是为了解决这个问题且不只是解决这一个问题而生。

单脉冲信号的跨时钟域处理

所谓的单脉冲信号,就是单比特信号,如上一个小标题中的使能控制信号等!对于这类信号的跨时钟域处理,有两种场景:

  • 一种是上一个小标题 亚稳态的概念 中讲到的从慢时钟域到快时钟域的脉冲信号处理,如图:

慢时钟域到快时钟域之间跨时钟处理

  • 另一种恰好相反,是从快时钟域到慢时钟域之间进行跨时钟域传输的脉冲信号处理。

快时钟域到慢时钟域之间跨时钟处理

针对这两种情况,我们分别讨论这两种情况的跨时钟域的处理方式!

从慢时钟域到快时钟域的场景

为了表述方便,我们约定一个原则吧,将时钟分为源时钟和目的时钟,如下图:

源时钟以及目的时钟

在这个小标题下,源时钟就是慢时钟,目的时钟就是快时钟,将源时钟域(慢)内的单比特信号同步到另一个目的时钟域(快),我们默认单比特脉冲信号在源时钟域内已经被本地时钟控制的寄存器同步(例如DFF1),这是因为经过时钟同步后信号不仅与时钟保持同步,而且有利于时序优化(时序路径为两个时钟元件之间的数据路径,使用触发器同步,无疑将长的数据路径截短,有利于时序通过),这也是我们推荐的一种设计习惯;

信号从慢时钟域同步到快时钟域,在目的时钟域是一定能采样到的,只不过可能会出现亚稳态的结果,针对这种场景下出现的亚稳态,我们的处理方式是两级寄存器同步,也就是通常我们说的使用目的寄存器对源时钟域的脉冲信号”打两拍“!

两级寄存器同步

两级寄存器同步能够将最终输出信号发生亚稳态发生的几率降低到很低的量级,可以大致地认为“消除了”亚稳态。可能还需要注意的是:在一个完整的两级寄存器同步电路中,信号跨时钟域应从原时钟域的原点触发器传递到同步器的第一触发器,而不需要经过原点触发器和同步器的第一触发器之间的任何组合逻辑。如下图:

跨时钟域之间不要存在组合逻辑

针对这张图:

两级寄存器同步

我们给出RTL语言描述(加入了复位信号):

代码语言:javascript复制
module dff2(
    input   wire dat    ,
    input   wire aclk   ,
    input   wire bclk   ,
    input   wire rst    ,
    output  reg  bdat2

    );

    reg     adat    ;
    reg     bdat1   ;

    //源时钟同步输入脉冲信号,改善时序
    always@(posedge aclk or posedge rst) begin
        if(rst) begin
            adat <= 1'b1    ;
        end
        else begin
            adat <= dat     ;
        end
    end

    //两级寄存器同步,“消除”亚稳态
    always@(posedge bclk or posedge rst) begin
        if(rst) begin
            bdat1 <= 1'b1   ;
            bdat2 <= 1'b1   ;
        end
        else begin
            bdat1 <= adat   ;
            bdat2 <= bdat1  ;
        end
    end


endmodule

综合后RTL原理图:

从快时钟域到慢时钟域的场景

上个小标题讲过了从慢时钟域到快时钟域的场景,在那种场景下,目的时钟域的寄存器一定能采样到源时钟域脉冲信号,只是不能保证不会出现亚稳态,因此采样两级寄存器同步的方式降低亚稳态发生的概率。

现在这个小标题下,我们来总结从快时钟域到慢时钟域的跨时钟域场景,如下:

约定规则

同步一个单比特信号到频率较慢的时钟域,比较麻烦。因为这存在目的时钟域检测不到源时钟域脉冲的情况,如下图:

目的时钟检测不到源时钟域脉冲信号

上图可以清晰地看到目的时钟域在时钟上升沿采样不到源时钟域信号,如何才能让目的时钟域能够检测到源时钟域信号呢?

我们首先应该会想到展宽源时钟域信号,这里的展宽需要牺牲一点东西,例如展宽高电平信号就得牺牲低电平持续的时间,同理展宽低电平,就得牺牲一点高电平的时间,不过这都不是事,因为我们既然要展宽,肯定是展宽我们需要使用的电平信号。

如何做到呢?

这通常需要在每个时钟域中设置一个寄存器,从目的时钟域向源时钟域反馈一种形式,表示检测到了信号。

不好理解吗?

下面我们结合时序图来理解我们的处理方式:

信号展宽处理时序图

针对这一处理过程,我们使用RTL硬件描述语言描述:

代码语言:javascript复制
module fast2slow_CDC(
    input   wire    clk1            ,
    input   wire    clk2            ,
    input   wire    rst             ,
    input   wire    pulse_clk1      ,
    output  wire    pulse_syn_clk2  

    );

    reg     pulse_wide_clk2         ;
    reg     reg1_pulse_wide_clk2    ;

    reg     reg1_pulse_wide_clk1    ;
    reg     reg2_pulse_wide_clk1    ;

    //生成脉冲展宽信号
    reg     pulse_wide_clk1         ;
    always@(posedge clk1 or posedge rst) begin
        if(rst) begin
            pulse_wide_clk1 <= 1'b0             ;
        end
        else if(pulse_clk1) begin
            pulse_wide_clk1 <= 1'b1             ;
        end
        else if(reg2_pulse_wide_clk1) begin
            pulse_wide_clk1 <= 1'b0             ;
        end
        else begin
            pulse_wide_clk1 <= pulse_wide_clk1  ;
        end
    end


    //在目的时钟域内采样展宽后的信号

    always@(posedge clk2 or posedge rst) begin
        if(rst) begin
            pulse_wide_clk2 <= 1'b0                 ;
            reg1_pulse_wide_clk2 <= 1'b0            ;
        end
        else begin
            pulse_wide_clk2 <= pulse_wide_clk1      ;
            reg1_pulse_wide_clk2 <= pulse_wide_clk2 ;
        end
    end

    //在源时钟域内同步目的时钟域内的展宽信号,以生成反馈信号

    always@(posedge clk1 or posedge rst) begin
        if(rst) begin
            reg1_pulse_wide_clk1 <= 1'b0                    ;
            reg2_pulse_wide_clk1 <= 1'b0                    ;
        end
        else begin
            reg1_pulse_wide_clk1 <= reg1_pulse_wide_clk2    ;
            reg2_pulse_wide_clk1 <= reg1_pulse_wide_clk1    ;
        end
    end


    assign  pulse_syn_clk2 = reg1_pulse_wide_clk2;



endmodule

给出RTL原理图:

RTL原理图

其实这种方式的效果是不容怀疑的,但是如果你还是不放心,我还是简单写个仿真平台仿真下吧:

仿真结果

可以看到,这种设计很不错,效果很好,但是它也有自身的局限性:就是不能对过近的脉冲进行完美的同步,如下图:

同步异常

可见,两个源时钟域的脉冲距离太近,导致目的寄存器无法完全同步;如果距离足够远,就可以完美同步:

同步正常

尽管如此,这种方式也不失为一种很好的脉冲同步方式,因为大部分场景都是适用的。好了,这篇文章到此也快告一段落了,花了很久来重新构思,目前已经将近凌晨三点,手也冻僵了,好在自配高端台式电脑性能不错,跑Vivado也很快,可以边实验边写文章。

最后附出仿真平台:

代码语言:javascript复制
module cdc_tb(

    );


    reg     clk1            ;
    reg     clk2            ;
    reg     rst             ;
    reg     pulse_clk1      ;
    wire    pulse_syn_clk2  ;

    initial begin
        clk1 = 0;
        forever
        #3 clk1 = ~clk1;
    end

    initial begin
        clk2 = 0;
        forever
        #5.5 clk2 = ~clk2;
    end

    initial begin
        rst = 1;
        pulse_clk1 = 0;
        #15
        rst = 0;
        #15
        @(posedge clk1) begin
           pulse_clk1 = #0.5 1'b1;
        end
        @(posedge clk1) begin
            pulse_clk1 = #0.5 1'b0;
        end
        #3 //距离太近
        #59.5 //距离足够远
        @(posedge clk1) begin
            pulse_clk1 = #0.5 1'b1;
        end
        @(posedge clk1) begin
            pulse_clk1 = #0.5 1'b0;
        end        

    end


    fast2slow_CDC u_fast2slow_CDC(
        .clk1        ( clk1        ),
        .clk2        ( clk2        ),
        .rst         ( rst         ),
        .pulse_clk1  ( pulse_clk1  ),
        .pulse_syn_clk2  ( pulse_syn_clk2  )
    );


endmodule

本系列文章还未结束,下一篇来一起处理多比特信号的跨时钟域问题!

参考资料

Synchronizer techniques for multi-clock domain SoCs & FPGAs

Verifying Clock Domain Crossing

Crossing Clock Domains in an FPGA

Crossing clock domains

Clock-Domain Crossing (CDC)

Get those clock domains in sync

Introduction to Clock Domain Crossing: Double Flopping

Understanding clock domain crossing issues

Clock Domain Crossing (CDC) : Asynchronous communications across boundaries

Clock Domain Crossing

0 人点赞