3、时钟域与跨时钟域:单时钟域设计、多时钟域设计、跨时钟域同步(CDC)的基本方法

时钟,是数字电路的“心跳”。

做图像加速,说白了就是在跟时钟赛跑。像素数据哗哗地流进来,你得在下一个时钟沿到来之前算完。但现实很骨感——一个芯片里往往有多个时钟,它们频率不同、相位不同,甚至毫无关系。这时候,时钟域和跨时钟域的问题就来了。

我刚开始做FPGA那会儿,就吃过这个亏。一个简单的双口RAM,两边时钟差了那么几纳秒,结果读出来的数据全是乱的。查了三天,最后发现是跨时钟域没处理好。从那以后,我对CDC(Clock Domain Crossing)就格外上心。

3.1 单时钟域设计

单时钟域,就是整个设计只有一个时钟。所有寄存器、状态机、计数器,都跟着同一个节拍走。

这种设计最简单,也最安全。你不需要考虑同步问题,时序分析也相对容易。说白了,只要满足建立时间和保持时间,电路就能稳定工作。

但图像加速里,单时钟域往往不够用。比如,摄像头传感器输出的是24MHz的像素时钟,而DDR控制器跑在400MHz。你总不能把整个系统都降到24MHz吧?那性能就全废了。

单时钟域设计的核心原则:
  • 所有时序逻辑使用同一个时钟源
  • 时钟树要平衡,减少skew
  • 组合逻辑的延迟必须小于时钟周期

3.2 多时钟域设计

多时钟域,就是设计里有多个独立的时钟。每个时钟驱动自己的逻辑块,块与块之间通过特定机制交换数据。

我习惯把多时钟域设计比作“多个独立的国家”。每个国家有自己的时间(时钟),内部事务自己处理。但国与国之间要通商(交换数据),就得有海关和签证(同步机制)。

在图像加速里,典型的场景是:

  • 图像采集端:24MHz像素时钟
  • 图像处理核心:150MHz处理时钟
  • 显示输出端:60Hz刷新率,对应不同的像素时钟

这三个时钟域之间,必须做跨时钟域同步。

注意:多时钟域设计最大的坑是“亚稳态”。当一个信号从一个时钟域进入另一个时钟域时,如果采样刚好发生在信号变化的窗口期,输出就会进入不确定状态。轻则数据错误,重则系统死机。

3.3 跨时钟域同步的基本方法

3.3.1 双级触发器同步

这是最基础、最常用的CDC方法。说白了,就是打两拍。

为什么是两拍?第一拍可能采到亚稳态,但经过一个时钟周期后,亚稳态大概率会稳定下来。第二拍再采,基本就是干净信号了。

// 双级触发器同步器
module sync_2ff (
    input  wire clk_dst,   // 目标时钟域
    input  wire rst_n,     // 异步复位
    input  wire data_in,   // 来自源时钟域的信号
    output wire data_out   // 同步后的信号
);

reg sync_reg1, sync_reg2;

always @(posedge clk_dst or negedge rst_n) begin
    if (!rst_n) begin
        sync_reg1 <= 1'b0;
        sync_reg2 <= 1'b0;
    end else begin
        sync_reg1 <= data_in;
        sync_reg2 <= sync_reg1;
    end
end

assign data_out = sync_reg2;

endmodule

嗯,这里要注意:双级触发器只适合同步单比特信号。比如控制信号、使能信号。如果是多比特数据总线,就不能这么干。

为什么?因为多比特信号每个bit的路径延迟不同,打两拍后,有的bit先稳定,有的后稳定,结果就是数据错位。我曾经在一个项目中用双级触发器同步16位像素数据,结果图像上出现了“雪花点”,查了好久才发现是这个原因。

我的经验:双级触发器同步后,信号会延迟两个目标时钟周期。如果你的系统对延迟敏感(比如视频同步信号),这个延迟必须算进去。

3.3.2 异步FIFO

异步FIFO是处理多比特数据跨时钟域的标准方案。它用双端口RAM存储数据,写时钟和读时钟完全独立。

核心难点在于:如何判断FIFO空和满?

写指针和读指针分别属于不同的时钟域。直接比较?不行,会亚稳态。所以需要把指针同步到对方时钟域。

我常用的方法是格雷码指针。格雷码相邻两个值只有1bit变化,同步时即使出现亚稳态,最多错一个bit,不会导致指针跳变。

// 异步FIFO的格雷码指针同步
// 写指针同步到读时钟域
reg [ADDR_WIDTH:0] wr_ptr_gray_sync1, wr_ptr_gray_sync2;

always @(posedge rd_clk or negedge rst_n) begin
    if (!rst_n) begin
        wr_ptr_gray_sync1 <= 0;
        wr_ptr_gray_sync2 <= 0;
    end else begin
        wr_ptr_gray_sync1 <= wr_ptr_gray;
        wr_ptr_gray_sync2 <= wr_ptr_gray_sync1;
    end
end

// 空标志判断:读指针追上同步后的写指针
assign empty = (rd_ptr == wr_ptr_gray_sync2);

你想想看,如果直接用二进制指针,同步时多个bit同时变化,亚稳态概率大大增加。格雷码就是专门解决这个问题的。

异步FIFO设计要点:
  • 地址指针用格雷码编码
  • 写指针同步到读时钟域判断空标志
  • 读指针同步到写时钟域判断满标志
  • FIFO深度要大于同步延迟,防止溢出

3.3.3 其他CDC方法

除了双级触发器和异步FIFO,还有一些场景会用到的技巧:

  • 握手协议:源时钟域发请求,目标时钟域回应答。适合低速控制信号。
  • MUX同步:多个控制信号先合并,再同步。减少同步器数量。
  • 数据使能同步:数据总线不变,只同步使能信号。适合数据变化不频繁的场景。

我个人习惯是:能不用CDC就不用。如果必须用,优先选异步FIFO。双级触发器只用于单比特控制信号。握手协议太慢,图像加速里很少用。

避坑指南:我曾经在一个项目中,把异步FIFO的深度设成了2。结果读时钟比写时钟慢,写两笔数据后FIFO就满了,读端还没读完。图像直接卡死。后来我把深度改成16,问题解决。记住:FIFO深度要大于最坏情况下的读写延迟差。

3.4 本章小结

时钟域和跨时钟域,是数字设计里绕不开的坎。单时钟域简单但不够灵活,多时钟域灵活但需要小心处理。

双级触发器适合单比特信号,异步FIFO适合多比特数据。格雷码指针是异步FIFO的核心技巧。

做图像加速,数据量大、时钟频率高,CDC问题尤其突出。我的建议是:设计初期就规划好时钟域划分,把CDC接口标准化。别等到综合完了才发现时序不收敛,那时候改起来就痛苦了。

时钟域与跨时钟域知识体系 单时钟域设计 • 所有逻辑共用同一时钟 • 时序分析最简单 • 无跨时钟域问题 • 适合小规模模块 缺点: • 性能受限于最低频时钟 • 无法处理多速率数据 多时钟域设计 • 多个独立时钟域 • 每个域内部同步 • 域间需要CDC处理 • 适合复杂SoC系统 挑战: • 亚稳态问题 • 时序约束复杂 • 验证难度大 CDC同步方法 双级触发器 • 单比特信号同步 • 打两拍消除亚稳态 • 延迟2个目标时钟 异步FIFO • 多比特数据同步 • 格雷码指针 • 双端口RAM存储 其他方法 • 握手协议 • MUX同步 • 数据使能同步 升级 需要

可可精品资料库