资讯动态

FPGA时钟切换毛刺根因与无毛刺设计实战指南

发布时间:2026/10/28 1:10:59 来源:尧图企业网站定制
上周帮一个朋友排查一块FPGA板卡的故障现象非常诡异系统上电后只要执行时钟切换主控状态机偶尔就会跳飞通信数据CRC错误率飙升严重时整个逻辑直接卡死。查了大半天最终定位到一段看起来人畜无害的代码assign clk_out sel ? clk_b : clk_a;。这个用组合逻辑MUX直接选时钟的写法恰恰就是FPGA时钟切换里最典型的“隐形杀手”。这篇文章我会从毛刺产生的根因讲起把无毛刺时钟切换的原理、代码实现、器件原语、时序约束以及实际调试中的避坑经验一次说透。适合正在做动态时钟切换、多时钟域设计、低功耗时钟管理的FPGA工程师参考也适合刚入门、想知道“为什么不能直接在代码里用条件表达式切时钟”的初学者。内容全部来自实际工程照抄就能用。1. 从一个线上事故说起时钟切换为什么是“隐形杀手”1.1 事故现场一段“人畜无害”的代码很多人在设计里遇到需要动态切时钟的场景时第一反应就是写一个选择器assign clk_out sel ? clk_b : clk_a;功能仿真里这代码完全正常波形漂亮输出就是二选一。可一旦放到板子上问题就来了。原因在于功能仿真是理想时序而实际芯片中sel信号的变化和clk_a、clk_b的边沿是异步的没有任何保证。当sel在时钟上升沿附近发生跳变时输出时钟脉冲可能被截断出现一个比正常周期“短半截”的高电平或低电平——这就是毛刺。毛刺的真正危害在于它不是固定的它是概率性的。每一次切换时sel到达的相位都可能在变化于是有时候没事有时候输出一个窄脉冲有时候输出看起来像正常时钟但实际上已经丢了一个沿。这种偶发性在调试时最痛苦跑几个小时可能才出现一次而且一旦出现系统已经跑飞了。更麻烦的是FPGA内部的全局时钟网络没有“滤波”能力。普通信号线上你有RC延迟可以滤掉窄毛刺但时钟网络本身就是低延迟高扇出毛刺会被原封不动地送到成千上万个触发器时钟端。只要有一个触发器采到错误数据整个状态机就乱了而且你根本不知道是哪个触发器出了问题。1.2 毛刺影响的扩散路径从工程角度理解毛刺的影响可以分三个层次看。第一层是触发器采样错误。毛刺如果出现在时钟上升沿附近触发器可能建立时间违规采到不稳定的值甚至导致亚稳态。亚稳态一旦发生它还会向后级寄存器链传播造成一连串的随机错误。在通信系统中这通常表现为CRC错误、帧失步在控制系统中表现为状态机跳飞、输出信号乱闪。第二层是时序收敛问题。当时钟路径上插了组合逻辑MUX时序工具需要对sel到输出时钟的路径做约束但实际上这条路径的时序要求极难收敛。因为sel在任意时刻都可能变化工具只能把它当成普通数据路径处理结果就是布局布线后时序报告一片红甚至出现很离谱的时钟偏斜。第三层是物理可靠性。在ASIC设计中组合逻辑直接选时钟是标准的禁忌会造成时钟门控引起的保持时间问题严重的会直接导致芯片失效。FPGA虽然因为内部门控和布线资源有一定容忍度但这些硬件上的脆弱点依然存在。我见过不少板卡长时间反复切换时钟后全局缓冲或者时钟树附近出现不可复现的偶发故障最后只能换芯片解决。2. 无毛刺时钟切换的核心原理与设计思路2.1 从“选时钟”到“选沿”解决毛刺的本质思路要消除毛刺核心思路不是“让切换更快”而是“让切换发生在安全的位置”。什么是安全的位置答案就是时钟的低电平期间。数字时钟的每个周期都由高电平和低电平组成。如果你能在时钟为低的时间段内完成“关闭旧时钟、打开新时钟”的动作那么输出就永远是从一个完整的低电平过渡到另一个完整的低电平不会出现窄脉冲。换句话说你要把一个电平敏感的操作变成一个边沿敏感的操作。这就是为什么说“从选时钟到选沿”。优秀的无毛刺切换电路都是让sel信号的采样发生在clk_a和clk_b的下降沿附近而不是上升沿。下降沿采样保证了sel更新后的值只会影响下一个周期的低电平区间而这个低电平区间就是安全的切换窗口。用生活化的例子理解时钟的高电平像一扇门的开启区间低电平像关闭状态。如果要在两扇门之间切换通道最安全的做法是把两扇门都保持在关闭状态时完成通路切换而不是在门开到一半时去搬开关。2.2 同步切换与异步切换的分野所有时钟切换问题都要先分清楚一个前提clk_a和clk_b是同步时钟还是异步时钟。同步时钟指的是两个时钟频率和相位关系已知通常来自同一个PLL的分频输出或者经过严格约束的相位对齐时钟。这种场景下下降沿采样方案就够了因为两个时钟的低电平位置关系是确定的切换窗口可以精确控制。异步时钟则完全不同。两个时钟可能来自板上不同的振荡器或者两个独立PLL频率和相位没有任何固定关系。这种情况下即使你把sel在下降沿采样了也无法保证旧时钟关闭和新时钟打开的时序一定落在同一个安全窗口里。你还需要一个握手机制先确认旧时钟确实关闭了再允许打开新时钟。实际工程中很多人误以为自己设计里的两个时钟是同步的。比如两个时钟都从同一个PLL的clkout0和clkout1引出但PLL的两个输出本身可能有不同的相位、占空比如果不做严格的时钟约束它们依然可能在某些边界条件下出现窄脉冲竞态。所以我的习惯是只要两个时钟不是被同一个综合工具明确识别为“相关时钟”一律按异步处理。3. 可落地的无毛刺时钟切换实现方案3.1 经典下降沿同步切换电路逐行解析先给一个适用于同步时钟场景的经典实现这也是业界流传很广的一种写法稍加改进就能用在很多设计中。module glitchless_clk_mux_sync ( input clk_a, input clk_b, input select, // 0: 选择 clk_a, 1: 选择 clk_b input rst_n, output clk_out ); reg sel_a_ff1, sel_a_ff2; // select 同步到 clk_a 域 (下降沿) reg sel_b_ff1, sel_b_ff2; // select 同步到 clk_b 域 (下降沿) always (negedge clk_a or negedge rst_n) begin if (!rst_n) begin sel_a_ff1 1b1; // 默认选中 clk_a sel_a_ff2 1b1; end else begin sel_a_ff1 ~select; sel_a_ff2 sel_a_ff1; end end always (negedge clk_b or negedge rst_n) begin if (!rst_n) begin sel_b_ff1 1b0; sel_b_ff2 1b0; end else begin sel_b_ff1 select; sel_b_ff2 sel_b_ff1; end end assign clk_out (sel_a_ff2 clk_a) | (sel_b_ff2 clk_b); endmodule这个电路的关键点有四个。第一select信号分别在clk_a和clk_b的下降沿被采样并且都经过了二级触发器同步。二级同步一方面消除亚稳态风险更重要的是保证select在各自时钟域的稳定时间足够。第二sel_a_ff2和sel_b_ff2是互斥的使能信号。当选择A时sel_a_ff21sel_b_ff20输出等于clk_a选择B时相反。这两个信号永远不会同时为0注意看复位时sel_a_ff21、sel_b_ff20没有问题切换过程中有一个短暂的“中间态”但由于都是在各自下降沿更新中间态只出现在低电平期间不会切出窄脉冲。第三输出用两个AND门加一个OR门实现。sel_a_ff2为0时clk_a被强制拉低sel_b_ff2为0时clk_b被强制拉低。OR门只会把两路中有效的那一路透传到输出。因为在下降沿更新所以每次透传切换都发生在低电平。第四代码里的* keep true *我虽然没有加但在实际工程中建议对中间寄存器和两个AND门加KEEP约束。不然综合器可能把这些寄存器合并掉或者把组合逻辑重新优化成不可控的形式。尤其是当select信号被识别为常数时整个切换电路可能被综合成一个固定连接彻底失去切换能力。3.2 异步时钟切换反馈握手方案异步场景下光靠下降沿同步还不够。原因很简单你无法保证“关闭A”和“打开B”这两个动作发生在同一个低电平窗口。如果两个时钟的低电平没有交集输出就会长时间停在低电平导致下游逻辑少采很多拍。更危险的是如果“打开B”先于“关闭A”发生两个时钟会同时驱动输出出现不可控的高电平叠加。异步无毛刺切换的标准做法是加入反馈握手。基本流程如下。第一步切换请求信号在源时钟域内被打拍先降频降速确保稳定。第二步当前选中时钟域在下降沿采样到切换请求后将本域使能en_a拉低关闭本域时钟输出。第三步本域关闭后产生一个确认信号ack_a这个信号跨时钟域到目标时钟域在目标域内做二级同步。第四步目标时钟域在下降沿采样到确认信号后将本域使能en_b拉高打开目标时钟输出。这个过程中en_a和en_b在任何时刻都最多只有一个为1从结构上杜绝了两个时钟同时驱动输出的可能。同时两个使能的翻转都发生在各自时钟域的下降沿所以每次切换都是干净的。工程上我一般不建议自己完全手写这种握手逻辑因为边界条件太多很容易漏掉某种组合。优先使用FPGA厂商提供的专用原语比如Xilinx的BUFGCTRL内部已经实现了这套互锁逻辑。如果必须手写至少要跑百万次随机仿真的回归把所有切换相位都覆盖到我才敢上板。3.3 用FPGA原语实现BUFGCTRL与BUFGMUXXilinx 7系列及之后器件最稳妥的无毛刺时钟切换手段是例化BUFGCTRL原语。BUFGCTRL #( .INIT_OUT (0), .PRESELECT_I0 (TRUE), .PRESELECT_I1 (FALSE) ) bufg_ctrl_mux ( .O (clk_out), .I0 (clk_a), .I1 (clk_b), .S0 (sel_a), // 1: 选择 I0 .S1 (sel_b), // 1: 选择 I1 .CE0 (1b1), .CE1 (1b1), .IGNORE (1b0) );S0和S1负责选择输入时钟CE0和CE1是使能端。BUFGCTRL内部有专门的反馈逻辑它会保证切换后的第一个时钟沿是完整、干净的不会出现窄脉冲。IGNORE端口可以用来绕过内部的切换保护逻辑只在调试特定问题时使用正常工作一定要接0。这里有个使用要点S0和S1不能同时为1这也是内部无毛刺逻辑的要求。如果你用一路反向信号控制两路选择比如S1~S0要小心组合逻辑产生的毛刺跑到S端所以最好由触发器在下降沿或上升沿寄存后再给到S0/S1。IntelAltera系FPGA里对应的原语是ALTCLKCTRL通过Qusys或者直接实例化都可以。另外很多FPGA的PLL本身就带时钟切换功能比如Xilinx MMCM/PLL的CLKIN1/CLKIN2切换、Intel PLL 的clkswitch引脚。这类PLL切换通常会伴随失锁切换完成后需要等待locked信号重新拉高再开始正常业务。国内主流的几家用FPGA厂商也都有类似的时钟缓冲原语。比如紫光同创PGL系列时钟IP、安路科技CGU里的时钟切换功能使用上思路大同小异。我最近在国产FPGA平台上做项目原则就是能调用厂商原语就绝不自己搭电路省心很多。3.4 两种方案的选型对比很多读者会问既然原语这么好那为什么还要学手写电路因为原语在一些场景下并不适用比如时钟信号还要做门控修改、需要定制切换时序、或者目标器件没有合适的原语。另外理解手写电路的过程能帮你更透彻地读懂原语的行为。对比项手写下降沿同步反馈厂商原语BUFGCTRL等无毛刺保证需要严格设计否则有边界风险硬件内部互锁可靠时序控制灵活性高可定制切换窗口低行为由原语定义资源占用LUTFF额外逻辑专用时钟缓冲资源异步时钟支持手写握手可实现原语内部已处理综合风险可能被优化、需KEEP约束工具自动识别无风险仿真难度需覆盖大量相位随机场景行为模型简单我个人在工程里的做法是默认用原语遇到原语无法覆盖的场景才手写并且手写的电路会放在单独的module里加满约束做充分验证。3.5 时序约束千万别忘了“异步时钟组”代码写对了原语也例化了但如果约束不对工具依然可能在布局布线时把风险藏起来。最常见的问题是没有告诉工具clk_a和clk_b是异步的导致时序引擎把切换路径当成同步路径分析疯狂报violation或反过来因为期望时序太乐观导致实际电路覆盖不住。推荐的约束写法以Vivado为例create_clock -period 10.000 [get_ports clk_a] create_clock -period 15.000 [get_ports clk_b] set_clock_groups -asynchronous \ -group [get_clocks -include_generated_clocks clk_a] \ -group [get_clocks -include_generated_clocks clk_b]set_clock_groups -asynchronous的核心作用是告诉时序工具这两个时钟不会同时作为时序路径的起点和终点不需要进行跨时钟路径的建立/保持时间检查。这样一方面释放掉大量无意义的violation另一方面也避免工具在时钟切换路径上做过度优化。对于手写切换电路中的关键寄存器建议加如下约束或属性(* keep true, shreg_extract no *) reg sel_a_ff1;另外如果切换请求select信号是某个状态机产生的最好给它添加合理的set_max_delay确保它不会在时钟沿附近翻转。这个约束能进一步降低竞态风险。4. 常见问题与排查技巧实录4.1 切换后时钟“漏拍”或周期被拉长现象切换完成后输出时钟的第一个周期明显比正常周期长看起来像丢了一个拍。原因对于异步时钟切换握手机制会引入一个“等待窗口”。关闭旧时钟后要等确认信号同步到目标时钟域目标时钟才会打开。这个过程中输出时钟保持低电平的时间取决于两个时钟的周期和同步级数。如果下游逻辑对时钟连续周期有严格要求比如高速串行收发器这种“漏拍”就是大问题。处理方式评估下游逻辑对时钟间隙的容忍度如果无法容忍必须换用锁定相位的同步切换方案在握手路径上减少同步级数但要注意亚稳态风险如果是PLL输出切换干脆用PLL自带的切换功能它的切换动作是原语级保证的行为更可预期。4.2 上电后输出无时钟或时钟乱跳现象复位释放后clk_out没有输出或者输出频率不对。原因手写切换电路里的使能寄存器在上电瞬间没有确定初值。如果仿真里寄存器默认是X态输出自然是未知实际芯片上电时寄存器初值取决于FPGA的配置可能为0也可能为1导致默认选中了错误时钟甚至两个使能同时为0整个时钟被关死。处理方式给所有使能寄存器加复位复位态明确默认选择某个时钟使用厂商原语时通过INIT_OUT、PRESELECT_I0/I1属性设定初始状态在测试平台上故意不拉复位跑一次看输出行为是否合理这个仿真能暴露初值问题。4.3 偶发毛刺还能在示波器上看到现象上板后用示波器观察clk_out大部分时间是正常的但偶尔能看到一个窄脉冲。原因如果电路已经用了下降沿同步或原语大概率不是切换逻辑本身的问题而是切换请求select信号本身的建立时间不足。select在很慢的时钟域产生传到切换模块时已经接近稳定但仍有亚稳态风险。在个别工艺角、电压、温度条件下亚稳态增加了毛刺就出来了。处理方式增加切换请求同步级数从2级加到3级甚至4级给select信号加约束避免在切换模块的采样沿附近变化用逻辑分析仪长时抓拍把偶发问题变成可统计问题再分析。4.4 综合工具报组合环路告警现象手写异步握手电路后Vivado或Quartus报combinational loop。原因握手逻辑中“反馈确认”路径如果写成组合逻辑直接反馈给同域使能就会形成组合环路。比如assign en_a ack_b req_a;如果ack_b又受en_a影响就变成环。处理方式确保所有反馈信号都经过至少一个触发器再参与使能逻辑用set_false_path告诉工具这条路径不需要时序检查但前提是你确认逻辑功能正确如果实在无法消除把相关寄存器加DONT_TOUCH属性防止工具在布线时破坏结构。4.5 实测技巧怎么判断切换是否真的无毛刺仿真阶段我的习惯是做针对性的时钟切换验证而不是只跑功能仿真。要点是第一构建一个testbench让select在时钟上升沿附近以随机相位翻转覆盖最恶劣的情况。可以设一个0到5ns的随机延时反复跑几千次。第二在输出时钟上做最小脉宽断言。比如要求每一段高电平和低电平都不能小于2ns一旦出现窄脉冲仿真立刻报错。property no_glitch_high; (posedge clk_out) (clk_out 1b0) [-1] |- (clk_out 1b0); endproperty第三上板后用示波器在时钟引脚上长时采集触发方式设为“下降沿斜率异常”观察是否有窄的高脉冲。时钟频率较高时要使用带宽足够的示波器探头通常至少要1GHz以上带宽。4.6 避坑清单一页纸检查项常见错误正确做法时钟MUX写法assign clk_out sel ? clk_b : clk_a;原语或下降沿同步使能门控同步级数单级触发器同步切换请求至少二级关键场景三级采样沿上升沿采样选择信号必须下降沿采样反馈握手异步切换无反馈直接切增加关闭确认握手或直接用原语初值处理无复位显式复位或原语INIT属性时序约束未设置异步时钟组set_clock_groups -asynchronous综合保护寄存器被合并优化加KEEP/DONT_TOUCH属性PLL关联切换后PLL失锁不处理等待LOCKED必要时复位PLL我自己在实际项目里是吃过亏的最开始也因为省事直接在代码里用MUX切时钟结果板子跑起来随机崩溃查了整整两天才定位到时钟路径。后来痛定思痛把设计里所有时钟切换点都梳理了一遍统一换成原语方案并给所有切换路径加了异步时钟组约束。从那以后再没有因为时钟切换出过功能问题。最后再分享一个实用的扩展技巧如果你需要实现多路时钟三个以上之间的无毛刺切换不要自己搭多级MUX树那会让时序复杂度和验证难度同时爆炸。更合理的做法是把各路时钟先通过无毛刺切换两两组成“虚拟时钟”再用原语级联或者干脆用厂商提供的时钟切换IP通过配置接口动态切换。多路切换时务必在切换前先停掉相关业务逻辑切完后再恢复从系统层面规避风险。时钟是FPGA设计里唯一不能“差不多”的资源。希望这篇文章能帮你避开我在那个深夜调试时踩过的坑让你的系统切换时钟时真正做到无声无息、干脆利落。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑