ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA跨时钟域设计实战:从亚稳态到异步FIFO的CDC方案

FPGA跨时钟域设计实战:从亚稳态到异步FIFO的CDC方案 1. 从一个反直觉的波形说起为什么单bit信号跨时钟域不能直接打两拍就完事很多刚接触FPGA设计的工程师都有一个朴素认知跨时钟域嘛不就是把信号用目标时钟打两拍同步一下就好了这个说法对了一半但坑就埋在另一半里。我见过太多项目在实验室跑得好好的一到高低温测试或者长时间运行就偶发数据错误最后定位下来全是CDC处理不当导致的亚稳态传播。先把最核心的概念说清楚。亚稳态是指触发器在时钟沿采样时输入信号恰好处于建立时间和保持时间窗口内导致输出既不是稳定的高电平也不是稳定的低电平而是在某个中间电压附近振荡最终随机稳定到高或低。这个随机稳定的过程需要额外的恢复时间如果在这段时间内下游逻辑又去采样这个信号就会得到不确定的值。那为什么“打两拍”能解决大部分问题因为第一级触发器的输出即使进入亚稳态经过一个时钟周期的恢复时间它稳定到正确值的概率已经非常高通常用MTBF平均无故障时间来衡量。第二级触发器再采样时基本不会碰到亚稳态窗口。但这里有个关键前提被同步的信号必须是单bit且电平持续足够长的信号。如果是多bit总线、脉冲信号或者快速变化的信号打两拍根本不够用。我实际项目中遇到过这样一个案例一个SPI从机的片选信号从50MHz域传到100MHz域工程师直接打了两拍。仿真没问题但上板后偶尔出现从机误触发。原因是什么片选信号的有效宽度在50MHz域只有1个周期到了100MHz域虽然理论上能被采到但由于两个时钟的相位关系不确定第一级触发器可能刚好采到片选信号的跳变沿导致亚稳态传播到第二级最终输出一个毛刺。这种毛刺被从机当成了一次有效的片选。所以CDC设计的第一个原则就是先分类再选方案。信号类型不同同步策略完全不同。下面这张表是我自己总结的分类决策依据基本覆盖了90%以上的场景信号类型典型场景推荐方案风险点单bit电平信号使能、配置位两级触发器同步信号宽度需大于目标时钟周期单bit脉冲信号中断、触发脉冲展宽同步边沿检测脉冲丢失或重复多bit总线数据、地址异步FIFO或握手协议数据一致性无法保证多bit计数器状态机状态格雷码编码同步编码转换错误复位信号全局复位复位同步器复位释放时的亚稳态这张表看起来简单但每一行背后都有大量细节。比如“单bit电平信号”这一行很多人以为只要打两拍就行却忽略了信号宽度必须大于目标时钟周期这个硬性条件。如果源时钟是100MHz目标时钟是200MHz源域的一个周期在目标域只有0.5个周期打两拍根本采不到。这时候要么展宽信号要么改用脉冲同步方案。2. 两级触发器同步器的MTBF计算与参数选择既然两级触发器同步器是最基础的CDC方案那就把它彻底讲透。很多人知道要打两拍但不知道为什么是两拍而不是三拍也不知道触发器之间的间距怎么约束。这一章就从MTBF的计算公式出发把参数选择的逻辑讲清楚。2.1 MTBF公式里每个参数的实际含义MTBF的经典公式是MTBF e^(t_r / τ) / (T_0 × f_clk × f_data)其中t_r留给亚稳态恢复的时间通常等于一个目标时钟周期减去触发器的建立时间τ触发器的亚稳态时间常数由工艺决定通常在几十皮秒量级T_0触发器的亚稳态窗口宽度也在几十皮秒量级f_clk目标时钟频率f_data异步数据的变化频率这个公式看起来复杂但实际使用时可以简化理解t_r越大MTBF指数级增长。而t_r等于目标时钟周期减去建立时间所以目标时钟频率越低MTBF越高。这就是为什么低速时钟域的同步器更可靠。我拿一个实际数字算一下。假设τ50psT_0100psf_clk200MHz周期5nsf_data10MHz建立时间0.2ns那么t_r4.8ns。代入公式MTBF e^(4.8ns/50ps) / (100ps × 200MHz × 10MHz) e^96 / (100e-12 × 2e8 × 1e7) 5e41 / (2e5) 2.5e36秒这个数字大到宇宙寿命的好几倍所以两级同步器在大多数场景下足够。但如果f_clk提高到500MHzt_r变成1.8nsMTBF会降到e^36/5e5≈4.3e10秒虽然还是很大但已经下降了26个数量级。如果工艺更差、τ更大MTBF还会进一步下降。2.2 为什么有时候需要三级触发器三级触发器同步器通常用在两个场景一是时钟频率极高比如超过500MHz两级同步的MTBF不够二是安全性要求极高的场合比如汽车电子或医疗设备需要额外的裕量。但三级触发器有个副作用延迟增加一个周期。对于脉冲信号同步这个延迟可能导致脉冲被错过。所以不是所有场景都适合加第三级。我的经验是先算MTBF如果两级的结果已经远超系统寿命要求比如100年就没必要加第三级。盲目加级只会增加延迟和面积。2.3 同步器链上的综合约束怎么写这是很多人忽略的一点两级触发器之间不能有任何组合逻辑而且综合工具需要知道这两个触发器是同步器不能对它们做优化或重定时。在Synopsys DC或Vivado中通常用ASYNC_REG属性来标记(* ASYNC_REG TRUE *) reg sync_ff1, sync_ff2; always (posedge clk_dst or negedge rst_n) begin if (!rst_n) begin sync_ff1 1b0; sync_ff2 1b0; end else begin sync_ff1 async_signal; sync_ff2 sync_ff1; end endASYNC_REG告诉工具这两个触发器之间的路径是异步的不要做时序优化也不要把它们合并或重定时。在Vivado中这个属性还会让工具自动把触发器放置得尽可能近减少布线延迟。注意有些工程师喜欢在同步器后面再加一级组合逻辑做边沿检测这本身没问题但边沿检测的输出必须再用目标时钟打一拍才能使用否则会产生新的毛刺。3. 脉冲信号跨时钟域展宽、同步、边沿检测的三步走单bit电平信号好办打两拍就行。但脉冲信号就麻烦了如果脉冲宽度小于目标时钟周期打两拍可能完全采不到如果刚好等于一个周期又可能采到亚稳态。我见过最典型的场景是中断信号跨时钟域源域产生一个单周期脉冲目标域需要捕获这个中断并处理。3.1 脉冲展宽的正确做法最稳妥的方案是“三步走”源域展宽、目标域同步、目标域边沿检测。具体来说第一步在源时钟域把单周期脉冲展宽成足够宽的信号。展宽的方法是用一个计数器或者简单的状态机让信号保持有效至少N个目标时钟周期。N怎么定我的经验是至少3个目标时钟周期这样即使相位关系最差也能保证被采到。// 源时钟域脉冲展宽 reg [3:0] stretch_cnt; reg stretched_pulse; always (posedge clk_src or negedge rst_n) begin if (!rst_n) begin stretch_cnt 4d0; stretched_pulse 1b0; end else if (src_pulse) begin stretch_cnt 4d8; // 展宽8个源时钟周期 stretched_pulse 1b1; end else if (stretch_cnt 0) begin stretch_cnt stretch_cnt - 1; stretched_pulse 1b1; end else begin stretched_pulse 1b0; end end这里展宽8个源时钟周期如果源时钟是100MHz目标时钟是200MHz那么展宽后的信号在目标域持续4个周期足够被采到。第二步在目标时钟域用两级触发器同步这个展宽后的信号。第三步在目标域做上升沿检测。方法是用一个寄存器延迟同步后的信号然后检测“当前为高、上一拍为低”// 目标时钟域边沿检测 reg sync_ff1, sync_ff2, sync_ff2_dly; always (posedge clk_dst or negedge rst_n) begin if (!rst_n) begin sync_ff1 1b0; sync_ff2 1b0; sync_ff2_dly 1b0; end else begin sync_ff1 stretched_pulse; sync_ff2 sync_ff1; sync_ff2_dly sync_ff2; end end assign dst_pulse sync_ff2 ~sync_ff2_dly;这样dst_pulse就是目标域的一个单周期脉冲宽度等于目标时钟周期。3.2 脉冲同步的常见坑第一个坑是展宽不够。如果展宽后的信号在目标域只持续1个周期而两级同步器的第一级刚好采到跳变沿第二级可能采到亚稳态。所以展宽至少3个目标周期是底线。第二个坑是边沿检测后的信号又被组合逻辑使用。dst_pulse是组合逻辑输出如果直接拿去驱动其他逻辑可能产生毛刺。正确做法是把dst_pulse再用目标时钟打一拍或者确保它只用于同步逻辑的输入。第三个坑是源域脉冲间隔太短。如果两个脉冲间隔小于展宽时间第二个脉冲会被第一个的展宽信号吞掉。这时候需要加一个脉冲计数器或者用FIFO来缓冲。4. 多bit数据跨时钟域异步FIFO的设计细节与格雷码的妙用多bit数据跨时钟域是最容易出问题的场景。很多人第一反应是用两级触发器同步整个总线但这会导致数据一致性错误总线的不同bit可能在不同周期被采到导致目标域看到的是一个从未存在过的中间值。比如源域数据从0x0F变成0xF0目标域可能采到0xFF或者0x00。4.1 异步FIFO的核心架构异步FIFO是解决多bit数据跨时钟域的标准方案。它的核心思想是用双端口RAM存储数据读写指针分别在各自的时钟域递增通过格雷码同步指针来判断空满状态。为什么用格雷码因为格雷码相邻两个值之间只有1bit变化。这样在跨时钟域同步指针时即使采到亚稳态也只会影响1bit不会导致指针值剧烈跳变。如果用二进制码指针从0111变成1000时4bit同时变化同步后可能得到任意值导致空满判断完全错误。异步FIFO的架构可以用下面这个表来概括模块时钟域功能写指针写时钟二进制递增转格雷码后同步到读域读指针读时钟二进制递增转格雷码后同步到写域双端口RAM双时钟写端口用写时钟读端口用读时钟空判断读时钟比较读指针和同步后的写指针满判断写时钟比较写指针和同步后的读指针4.2 格雷码转换的Verilog实现二进制转格雷码的公式是gray bin ^ (bin 1)。这个操作在Verilog里很简单function [ADDR_WIDTH:0] bin2gray; input [ADDR_WIDTH:0] bin; begin bin2gray bin ^ (bin 1); end endfunction但要注意指针的位宽要比地址位宽多1位。比如RAM深度是16地址是4bit指针需要5bit。多出来的最高位用来区分“绕了一圈”的情况。比如写指针从00000到11111再回到00000时最高位翻转表示已经写了一轮。4.3 空满判断的保守策略异步FIFO的空满判断有个关键点同步过来的指针有延迟。写指针同步到读域需要两个读时钟周期所以读域看到的写指针是“旧”的。这意味着读域判断“空”时实际上FIFO里可能已经有数据了但读域不知道。这是保守的不会导致读空。反过来读指针同步到写域也有延迟写域判断“满”时实际上FIFO可能还没满。这也是保守的不会导致写溢出。这种保守策略的代价是FIFO的实际可用深度比理论深度小。比如深度16的FIFO实际可能只能存14个数据。如果对深度有严格要求需要把FIFO深度设计得比实际需求大一些。4.4 异步FIFO的验证要点验证异步FIFO时除了常规的功能测试还要重点检查几个边界场景同时读写读写时钟频率比接近1:1时空满标志是否频繁翻转写满后继续写写域是否在满标志有效时停止写读空后继续读读域是否在空标志有效时停止读复位释放读写域复位释放时间不同步时指针是否从0开始格雷码同步同步后的指针是否只发生1bit变化我通常会在testbench里加一个断言检查同步后的格雷码指针每次只变化1bit// 断言格雷码指针每次只变化1bit always (posedge clk_rd) begin if (rst_n) begin $assert((gray_rd_ptr ^ gray_rd_ptr_dly) inside {1b1 0, 1b1 1, ...}); end end5. 握手协议比FIFO更轻量的多bit传输方案异步FIFO虽然通用但面积和功耗都不小。如果数据量不大、传输不频繁握手协议是更轻量的选择。握手协议的核心思想是用请求和应答信号来协调两个时钟域的数据传输确保数据在被采样时已经稳定。5.1 四相位握手与两相位握手的区别握手协议有两种基本形式四相位握手和两相位握手。四相位握手的流程是源域把数据放到总线上拉高req目标域同步req后采样数据拉高ack源域同步ack后拉低req目标域同步req变低后拉低ack两相位握手简化了流程源域把数据放到总线上翻转req目标域同步req翻转后采样数据翻转ack源域同步ack翻转后可以发下一个数据四相位握手更可靠因为每个阶段都有明确的电平状态两相位握手更快但需要更仔细地处理边沿检测。我通常推荐四相位握手因为它的时序更容易约束调试也更直观。5.2 握手协议的数据保持时间握手协议的关键是数据必须在req有效期间保持稳定。具体来说从源域拉高req开始到目标域拉高ack并同步回源域为止数据不能变化。这个时间至少是T_hold 2 × T_dst 2 × T_src T_comb其中T_dst是目标时钟周期T_src是源时钟周期T_comb是组合逻辑延迟。为什么是2倍因为req同步到目标域需要2个目标周期ack同步回源域需要2个源周期。如果数据变化太快目标域可能采到旧数据或中间值。所以握手协议适合低速、小数据量的场景比如配置寄存器写入、状态上报等。5.3 握手协议的Verilog实现下面是一个四相位握手发送端的简化实现// 发送端源时钟域 reg req; reg [7:0] data_reg; always (posedge clk_src or negedge rst_n) begin if (!rst_n) begin req 1b0; data_reg 8d0; end else if (send_start !req) begin data_reg data_in; req 1b1; end else if (ack_sync req) begin req 1b0; end end接收端在检测到req同步后的上升沿时采样数据然后拉高ack。ack同步回源域后源域拉低req接收端检测到req变低后拉低ack完成一次握手。6. 复位信号的跨时钟域处理被低估的亚稳态源头复位信号的CDC处理是很多工程师容易忽略的地方。大家通常关注数据路径的CDC却忘了复位信号本身也需要同步。如果复位信号在不同时钟域之间不同步释放可能导致某些触发器先退出复位某些后退出状态机进入非法状态。6.1 异步复位同步释放的标准电路标准的复位同步器是“异步复位、同步释放”reg rst_sync1, rst_sync2; always (posedge clk or negedge rst_async_n) begin if (!rst_async_n) begin rst_sync1 1b0; rst_sync2 1b0; end else begin rst_sync1 1b1; rst_sync2 rst_sync1; end end assign rst_sync_n rst_sync2;这个电路的特点是复位拉低时立即生效异步复位释放时经过两级触发器同步同步。这样每个时钟域的复位释放时间都与该时钟域同步避免了不同触发器退出复位时间不一致的问题。6.2 多时钟域复位的级联处理如果系统有多个时钟域每个时钟域都需要自己的复位同步器。但要注意复位同步器的输入必须是同一个异步复位源。如果每个时钟域用不同的复位源同步释放就失去了意义。我见过一个项目三个时钟域各自用了一个复位同步器但复位源来自不同的GPIO。结果上电时三个域的复位释放时间差了几百纳秒状态机直接跑飞。后来改成统一的复位源再分别同步释放问题解决。6.3 复位同步器的MTBF复位同步器的MTBF计算和普通同步器类似但有一个特殊点复位释放只发生一次所以f_data非常低接近0。这意味着复位同步器的MTBF通常非常高两级触发器足够。但如果复位信号频繁抖动比如按键复位没有消抖f_data会变大MTBF下降。所以复位信号一定要先消抖再进入同步器。7. 静态检查工具在CDC验证中的实际使用前面讲的都是设计层面的东西但CDC问题很难通过仿真完全覆盖因为亚稳态是概率事件仿真器通常不会模拟亚稳态。这时候就需要静态检查工具比如Spyglass CDC。7.1 Spyglass CDC的检查流程Spyglass CDC的典型流程是读入RTL和约束文件设置时钟域和复位域运行CDC检查分析报告修复违规约束文件里需要定义时钟、复位、以及哪些信号是异步的。比如clock -name clk_src -period 10 clock -name clk_dst -period 5 reset -name rst_n -async7.2 常见CDC违规类型与修复Spyglass CDC常见的违规类型包括违规类型含义修复方法Ac_unsync信号未同步就跨时钟域加同步器Ac_cdc01单bit信号跨时钟域两级触发器Ac_datahold多bit数据跨时钟域无握手异步FIFO或握手Ac_glitch组合逻辑跨时钟域改为寄存器输出Ac_reset复位信号未同步复位同步器我实际使用Spyglass的经验是不要追求零违规。有些违规是误报比如同步器内部的路径会被标记为跨时钟域但实际上是安全的。关键是要理解每个违规的含义判断是否真的需要修复。7.3 静态检查与仿真的配合静态检查工具能发现结构性的CDC问题但无法验证功能正确性。仿真能验证功能但无法覆盖亚稳态。两者需要配合使用静态检查确保所有跨时钟域路径都有正确的同步结构仿真验证同步器的功能逻辑比如FIFO的空满判断、握手协议的时序形式验证验证同步器的MTBF是否满足要求有些工具支持我的做法是先用Spyglass跑一遍把所有Ac_unsync和Ac_datahold违规修掉然后用仿真验证功能最后用形式验证检查边界条件。8. 几个真实项目中的CDC踩坑记录理论讲完了最后分享几个我在实际项目中踩过的CDC坑。这些坑在教科书里不会写但每一个都让我调试了好几天。8.1 被综合工具优化掉的同步器有一次我用Vivado综合一个设计综合后的网表里两级同步器只剩下一级。原因是综合工具认为第二级触发器的输出没有被使用实际上被使用了但工具的分析有误把它优化掉了。后来加了ASYNC_REG属性问题解决。这个坑的教训是同步器必须加综合属性否则工具可能做意外优化。在Vivado中用ASYNC_REG在Quartus中用ALTERA_ASYNC_REG在DC中用set_dont_touch。8.2 异步FIFO深度不够导致的溢出一个项目里异步FIFO的深度是16但实际数据突发长度是20。工程师认为FIFO满的时候写域会停止写所以不会溢出。但他忘了写域判断“满”用的是同步过来的读指针有延迟。当FIFO实际快满时写域可能还认为有空位继续写导致溢出。修复方法是把FIFO深度增加到32留出足够的裕量。一般来说FIFO深度要比最大突发长度大20%以上。8.3 复位释放顺序导致的死锁一个多时钟域系统里时钟域A的状态机等待时钟域B的应答时钟域B的状态机等待时钟域A的请求。复位释放时A先释放发出请求B后释放还没准备好接收。结果A一直等B的应答B一直等A的请求死锁。修复方法是复位释放时先释放接收方再释放发送方。或者加一个全局的复位完成信号所有时钟域都等到这个信号有效后才开始工作。8.4 格雷码指针的位宽错误一个工程师写异步FIFO时指针位宽和地址位宽一样都是4bit。结果FIFO深度16但实际只能存15个数据而且空满标志偶尔出错。原因是4bit指针从1111回到0000时格雷码从1000变成0000有2bit变化违反了格雷码相邻只变1bit的规则。修复方法是指针位宽改为5bit最高位用来区分绕圈。这个坑很隐蔽因为仿真时如果读写时钟频率比固定可能不会触发。9. 写在最后CDC设计的几条铁律做了这么多年FPGA设计CDC问题始终是调试时间最长的类别之一。我总结了几条铁律每次做新项目都会对照检查第一任何跨时钟域的信号都必须有明确的同步策略。不能有“这个信号应该没问题”的侥幸心理。Spyglass跑一遍把所有跨时钟域路径列出来逐条确认。第二同步器必须加综合属性。ASYNC_REG、set_dont_touch、ALTERA_ASYNC_REG根据工具选择。不加属性的同步器等于没做同步。第三多bit数据永远不要用两级触发器同步。要么异步FIFO要么握手协议没有第三条路。第四复位信号也要同步释放。异步复位同步释放是标准做法不要图省事直接异步复位所有触发器。第五FIFO深度留裕量。理论深度加20%以上考虑同步延迟和突发长度。第六仿真通过不等于CDC没问题。亚稳态是概率事件仿真器不模拟。静态检查加形式验证才能覆盖CDC问题。第七调试CDC问题时先看波形。抓取跨时钟域信号在源域和目标域的波形看同步器的输出是否有毛刺或延迟异常。很多时候问题就藏在波形细节里。CDC设计没有捷径每一个信号都要认真对待。但一旦掌握了这套方法论你会发现它其实是有章可循的。希望这些经验能帮你少踩几个坑。
返回列表