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深度要大于同步延迟,防止溢出
3.3.3 其他CDC方法
除了双级触发器和异步FIFO,还有一些场景会用到的技巧:
- 握手协议:源时钟域发请求,目标时钟域回应答。适合低速控制信号。
- MUX同步:多个控制信号先合并,再同步。减少同步器数量。
- 数据使能同步:数据总线不变,只同步使能信号。适合数据变化不频繁的场景。
我个人习惯是:能不用CDC就不用。如果必须用,优先选异步FIFO。双级触发器只用于单比特控制信号。握手协议太慢,图像加速里很少用。
3.4 本章小结
时钟域和跨时钟域,是数字设计里绕不开的坎。单时钟域简单但不够灵活,多时钟域灵活但需要小心处理。
双级触发器适合单比特信号,异步FIFO适合多比特数据。格雷码指针是异步FIFO的核心技巧。
做图像加速,数据量大、时钟频率高,CDC问题尤其突出。我的建议是:设计初期就规划好时钟域划分,把CDC接口标准化。别等到综合完了才发现时序不收敛,那时候改起来就痛苦了。