FPGA基础 -- 无毛刺时钟切换(glitch-free clock switching)
“工程可落地”的无毛刺时钟切换(glitch-free clock switching)讲解与可用代码,分别覆盖ASIC通用做法与FPGA厂商原语做法,并配上时序/CDC约束与验证要点。
一、问题本质与设计目标
把 clk_out
从 clk0
切到 clk1
(或反向)时,如果简单用 assign clk_out = sel ? clk1 : clk0;
,一旦 sel
在任意时刻翻转、恰逢两路时钟处于不同电平/相位,就会产生:
- runt pulse(短脉冲):违反下游触发器的最小高/低脉宽约束;
- 毛刺与双沿:造成“多计数/少计数”、状态机跳变等隐患;
- CDC风险:
sel
通常与两路时钟都异步,若直接驱动组合 MUX,会让毛刺概率放大。
设计目标:切换时实现“break-before-make”(先关后开),只在低电平窗口改变门控,使 clk_out
要么连续、要么短暂停(保持低电平)再平滑接入新时钟,但绝不产生短脉冲。
二、ASIC/通用RTL方案(And-Or 门控 + 低电平更新 + 互斥握手)
核心思路:
- 将两路时钟分别门控成
gclk0 = clk0 & en0
、gclk1 = clk1 & en1
; - 输出
clk_out = gclk0 | gclk1
; en0
/en1
互斥,并且只在相应时钟的低电平边界更新(常用negedge clk
或 ICG 低电平锁存),从而保证开/关都发生在“安全窗口”。
典型可综合参考实现(ASIC/标准单元库/不走FPGA LUT 时钟网络的前提):
// ===============================================================
// Glitch-free 2:1 Clock Switch (ASIC/通用RTL参考)
// - 先关后开(break-before-make)
// - 在各自时钟的 negedge 更新 en,保证改变发生在低电平窗口
// - 对 sel 进行双级同步(各自域)
// ===============================================================
module clk_switch_gf (input wire rst_n, // 异步复位,低有效input wire clk0, // 时钟0(可与clk1异步/不同频)input wire clk1, // 时钟1input wire sel_async, // 1->选择clk1,0->选择clk0(异步控制)output wire clk_out
);// 1) 将 sel 分别同步到两个时钟域,降低亚稳态风险reg sel0_d1, sel0_d2;reg sel1_d1, sel1_d2;always @(posedge clk0 or negedge rst_n) beginif (!rst_n) {sel0_d1, sel0_d2} <= 2'b00;else {sel0_d1, sel0_d2} <= {sel_async, sel0_d1};endalways @(posedge clk1 or negedge rst_n) beginif (!rst_n) {sel1_d1, sel1_d2} <= 2'b00;else {sel1_d1, sel1_d2} <= {sel_async, sel1_d1};endwire sel0 = sel0_d2; // 在clk0域观察到的选择(0=>clk0)wire sel1 = sel1_d2; // 在clk1域观察到的选择(1=>clk1)// 2) 互斥门控en,在各自时钟的negedge更新(低电平窗口)// break-before-make:新使能只有在另一边彻底关闭后才能拉起reg en0, en1;// 默认上电选择clk0(可按需调整)always @(negedge clk0 or negedge rst_n) beginif (!rst_n) en0 <= 1'b1; // 上电默认clk0else en0 <= (~sel0) & (~en1); // 只有当选择clk0 且 en1已关 才能开endalways @(negedge clk1 or negedge rst_n) beginif (!rst_n) en1 <= 1'b0; // 上电默认clk1关闭else en1 <= ( sel1) & (~en0); // 只有当选择clk1 且 en0已关 才能开end// 3) 时钟门控与汇合(ASIC请用ICG/CLKGATE原语替换 & 与 |)wire gclk0 = clk0 & en0;wire gclk1 = clk1 & en1;assign clk_out = gclk0 | gclk1;endmodule
关键点说明
- 互斥保证:
en0 <= ~sel0 & ~en1
与en1 <= sel1 & ~en0
形成“你关我才开”的互锁。理论上可用形式验证/断言证明!(en0 & en1)
永真。 - 低电平更新:在
negedge clk
改变enX
,意味着enX
的翻转发生在clkX
低电平期间,不会截断高电平造成短脉冲。 - 允许短暂停顿:切换窗口内可能出现
en0=en1=0
(输出保持低电平若干 ns/周期),这不是毛刺,是安全空档。 - ICG/库原语:ASIC务必以**集成时钟门控单元(ICG)**替代
&
、|
,从而满足架构/版图上的时钟树规则与占空比/脉宽保障。 - 边界场景:若被关闭的时钟卡死在高电平导致
negedge
不来,上述算法会一直保持输出低电平(安全降级)。若要求“输入卡死也要强制切走”,需在ICG/库里使用带“停摆忽略”能力的专用门控/切换单元,或引入看门狗辅助。
三、FPGA实现的工程做法(强烈建议优先用厂商原语)
在FPGA中禁止用 LUT/组合逻辑直接门控时钟或在Fabric里“与/或”,应使用专用全局时钟资源。各家芯片都提供了可无毛刺切换的全局时钟 MUX/门控原语:
1) Xilinx(7-Series/UltraScale/UltraScale+)
推荐:BUFGCTRL
或 BUFGMUX_CTRL
/ BUFGCE
最稳妥的做法是用 CE 互斥实现“先关后开”,把选择握手逻辑产生的 en0/en1
直接接到 BUFG 的使能脚:
// 同步/互斥逻辑与你在ASIC方案一致(建议仍做双级同步+negedge更新)
// 这里展示用 BUFGCTRL 做无毛刺切换的骨架:wire en0, en1; // 互斥门控(同上文逻辑生成)BUFGCTRL #(.INIT_OUT(1'b0) // 上电输出默认值
) u_bufgctrl (.I0 (clk0),.I1 (clk1),.S0 (1'b1), // 置1:忽略S,完全由CE控制.S1 (1'b1),.CE0 (en0), // 只使能一路.CE1 (en1),.IGNORE0(1'b0),.IGNORE1(1'b0),.O (clk_out)
);
要点:
S0=S1=1
,让CE0/CE1
完全决定输出;做到“break-before-make”即可无毛刺切换,不要求两路同频同相。- 若输入时钟可能卡死,UltraScale 家族还可利用
BUFGCTRL
的更高级用法(IGNORE
等)实现“卡死时强制切走”,具体参照器件文档。 - 如果只是在一条时钟上启停,用
BUFGCE
就够;两路切换仍建议BUFGCTRL
。
2) Intel(Altera)
使用 Clock Control Block(老名 ALTCLKCTRL
),选择 glitch-free switchover 选项。原则同上:
- 让工具把你的门控/切换逻辑映射到全局时钟控制块;
- 互斥 CE + 先关后开;
- 具体端口名/参数在不同代器件略有差异(Cyclone/Arria/Agilex),用 IP 向导生成后把CE互斥逻辑接上即可。
3) Lattice(ECP5、CrossLink-NX、MachXO等)
使用器件提供的 全局时钟控制/多路选择原语(例如 CLKMUX2
、ECLK
相关原语、DCCA
/CLKDIVF
等)。同样遵循:
- 不要用 LUT 对时钟
&
或|
; - 使用官方时钟MUX/门控资源,并用互斥 CE 驱动;
- 具体原语名与接法以器件 TRM 为准。
四、时序与CDC约束(必须做)
-
Clock Groups:
clk0
与clk1
大多无关/异步,在 STA 里:set_clock_groups -asynchronous -group {clk0} -group {clk1}
或在 Vivado 用set_clock_groups -exclusive
(取决于你的语义:确实不会同时到下游)。
-
派生/生成时钟:把
clk_out
定义为生成时钟,并指明它与激活源之间的关系(静态检查要知道这个树)。 -
选择信号路径:
sel_async -> sel_sync
的路径设为CDC/false_path,例如set_false_path -from [get_ports sel_async] -to [get_pins sel0_d1/D sel1_d1/D]
或用工具自带 CDC 例外标注。
-
最小脉宽约束(ASIC尤甚):对
clk_out
设置set_min_pulse_width -high/-low
,匹配目标工艺/库要求。 -
保持互斥:为
en0 & en1
添加综合/形式约束或 SVA 断言,禁止同时为1;并考虑dont_touch
保护关键互锁逻辑,避免综合优化破坏。
五、功能验证建议(强烈推荐)
-
随机切换:在仿真里对
sel_async
做随机抖动与突发翻转(远高于两路时钟频率),观察clk_out
波形:- 不出现小于阈值的高/低脉宽(可写断言主动检查)。
-
频比/相位覆盖:覆盖常见组合:同频同相/同频反相/不等频/一快一慢。
-
停摆场景:将被关闭的时钟“卡高/卡低”,确认
clk_out
行为(安全降级或利用原语强制切走)。 -
SVA断言样例(思路示意):
assert never (en0 && en1);
assert clk_out
的任一高脉宽 ≥ T_min_high,低脉宽 ≥ T_min_low(基于周期测量模块或专用checker)。
六、常见坑与规避
- 在Fabric里用门电路直接改时钟:在FPGA是大忌,务必用全局时钟原语。
- 只做组合MUX:
assign clk_out = sel?clk1:clk0;
必出毛刺。 - 同步缺失:
sel
不双级同步,容易把亚稳态直接“扩散”到门控。 - 非互斥CE:两路 CE 可能瞬间同时为1(或先开后关),会叠加出“怪脉冲”。
- negedge更新被禁止:若公司规范不允许
negedge
触发器,可改为低电平透明锁存的 ICG 方案,或者在FPGA用带“glitchless”承诺的原语,仍保证“低电平窗口更新”。
七、一个更“厂味”的Xilinx落地骨架(推荐在FPGA中用)
把上文“互斥CE + 双域同步”的思想直接喂给 BUFGCTRL
,实现最标准的无毛刺切换:
module xilinx_bufgctrl_switch (input wire rst_n,input wire clk0,input wire clk1,input wire sel_async, // 1->clk1, 0->clk0output wire clk_out
);// 双域同步(同上)reg sel0_d1, sel0_d2, sel1_d1, sel1_d2;always @(posedge clk0 or negedge rst_n) beginif (!rst_n) {sel0_d1, sel0_d2} <= 2'b00;else {sel0_d1, sel0_d2} <= {sel_async, sel0_d1};endalways @(posedge clk1 or negedge rst_n) beginif (!rst_n) {sel1_d1, sel1_d2} <= 2'b00;else {sel1_d1, sel1_d2} <= {sel_async, sel1_d1};endwire sel0 = sel0_d2;wire sel1 = sel1_d2;// 在各自域低电平窗口更新互斥CE(与ASIC方案一致)reg en0, en1;always @(negedge clk0 or negedge rst_n) beginif (!rst_n) en0 <= 1'b1;else en0 <= (~sel0) & (~en1);endalways @(negedge clk1 or negedge rst_n) beginif (!rst_n) en1 <= 1'b0;else en1 <= ( sel1) & (~en0);end// 用BUFGCTRL做最终无毛刺切换(CE互斥,S固定=1)BUFGCTRL #(.INIT_OUT(1'b0)) u_bufgctrl (.I0(clk0), .I1(clk1),.S0(1'b1), .S1(1'b1),.CE0(en0), .CE1(en1),.IGNORE0(1'b0), .IGNORE1(1'b0),.O(clk_out));
endmodule
这套在 7-Series/UltraScale(+) 都是“顺着工具”的推荐用法;Intel/Lattice 用各自的 Clock Control Block 同理迁移。
八、结语(选型建议)
- FPGA项目:优先用官方时钟控制原语(BUFGCTRL/ALTCLKCTRL/CLKMUX2…),在其外围用互斥CE + 低相位更新 + 双域同步的模板即可。
- ASIC项目:用ICG + 低电平锁存的门控结构替代
&/|
,维持同样的状态机互斥与更新时机;在STA里做好生成时钟、最小脉宽与时钟组隔离约束。 - 若有“输入卡死也要切”的强需求,选用带忽略/强制切换能力的专用单元(不同工艺/FPGA原语的具体脚位名称不同),或增加看门狗配合强制降级策略。