尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

无毛刺时钟切换RTL设计:原理、代码与验证

发布时间:2026/9/28 1:33:37

资讯中心
01
ARTICLE

无毛刺时钟切换RTL设计:原理、代码与验证

无毛刺时钟切换RTL设计:原理、代码与验证
在数字IC的笔面试里“无毛刺时钟切换”是一个公认的高频手撕代码题。它看起来特别简单——两个时钟输入、一个选择信号、一个输出写一个MUX不就行了但真正做过SoC的人都知道时钟是芯片的脉搏切换瞬间只要冒出一个半截高电平的窄脉冲下游成千上万个触发器的采样沿就会出问题轻则功能异常重则全片逻辑错乱。所以这个问题被反复拿出来考考的不是你能不能写出MUX而是你懂不懂毛刺从哪来、电路怎么设计才能从根上把毛刺消掉。这篇文章我按做项目的思路拆开讲先分析毛刺产生的根本原因再推导经典的无毛刺切换电路结构给出可直接抄作业的可综合RTL代码然后写一个能自动验证“确实没毛刺”的testbench最后把我在实际项目中踩过的一些坑列出来。无论你是准备面试还是在做时钟管理、电源管理相关的模块这套内容应该都能直接用上。1. 为什么时钟切换会产生毛刺1.1 组合MUX切换的竞争与冒险先说毛刺的本质。所谓毛刺是指在时钟输出线上出现一个宽度远小于正常时钟半周期的异常脉冲。正常的clk0高电平比如是5ns如果输出上突然冒出一个2ns的小脉冲下游寄存器可能把它误判为一个有效时钟沿导致数据被提前采样这就是最典型的毛刺危害。产生毛刺的直接原因是选择信号sel与时钟沿之间存在竞争。用一个普通组合逻辑MUXassign clk_out sel ? clk1 : clk0;假设当前sel0输出clk0。sel在clk0的高电平期间突然变成1MUX的输出立即从clk0的高电平切换到clk1而如果此刻clk1恰好是低电平输出就被硬生生拉低。原本应该在clk0下降沿才结束的高电平被提前截断了于是形成一个窄脉冲毛刺。反过来如果sel在clk0低电平期间变化输出也可能因为clk1的高电平提前到来形成一个过窄的低电平影响后续逻辑的复位和触发。用个生活化的类比普通MUX做时钟切换就像两条车道的车流在路口合并切换信号就是交警指挥。如果交警在绿灯通行到一半的时候突然放另一条车道的车进来必然有一辆车被“夹断”——这就是毛刺。要做到无毛刺核心思想是必须保证每条车道的车都是完整通过路口再放另一条车道。1.2 无毛刺切换的应用场景与必要性也许有人觉得毛刺在仿真里偶尔能看到实际芯片里概率很低。但真实产品里无毛刺时钟切换几乎是刚需几个典型场景非常明确应用场景时钟切换需求不处理的后果DVFS动态调频CPU在PLL高频与分频低频之间切换毛刺导致CPU核心逻辑采样错误跑飞低功耗模式切换系统在慢速时钟和快速时钟之间切换时钟毛刺让唤醒逻辑误触发时钟源冗余备份主时钟失效后切到备份时钟毛刺损坏关键链路的数据多IP工作频率切换USB、DDR等IP在不同频率模式切换接口协议时序违例测试模式功能时钟与扫描测试时钟互切测试向量采样错误误判良品这些场景有一个共同点切换不能停机太久又绝对不允许出现异常脉冲。所以“无毛刺”是底线要求切换的短暂停顿反而是可以接受的。理解了这一点下面再来看电路实现就顺理成章了。2. 无毛刺时钟切换的核心设计思路2.1 使能信号在低电平期间切换负沿打拍的秘密先看无毛刺切换电路的最基本输出形式。工程上几乎都是这样做的assign clk_out (clk0 en_clk0) | (clk1 en_clk1);不是用MUX直接选而是给每路时钟加一个“使能开关”通过与门控制是否输出。这就是把“选择”变成了“使能”让每路时钟的每一个高电平脉冲要么完整放行、要么完整屏蔽。关键在使能信号什么时候变。如果使能信号在时钟高电平期间变化与门输出照样会被截断和组合MUX没有本质区别。所以必须保证使能信号只能在对应时钟的低电平期间翻转。要实现这一点最直接的手段就是——用时钟的下降沿打拍。always (negedge clk0 or negedge rst_n) begin if (!rst_n) en_clk0 1b1; else en_clk0 ~sel; enden_clk0在clk0的下降沿更新意味着它永远在clk0为低电平的时刻变化。当clk0下一个高电平到来时en_clk0早已稳定输出要么是一个完整的高电平脉冲要么完全不出现。这样就保证了不会出现截断的高电平毛刺。2.2 反馈互锁严格保证“先关后开”解决了毛刺的“截断”问题还有一个逻辑问题如果切换时两路时钟同时被使能输出会同时出现clk0和clk1的脉冲对于下游逻辑来说一个周期内可能出现两个上升沿这在功能上等价于灾难。所以高质量的设计还要做到两路使能互斥。经典做法是让两个使能信号互相反馈、互锁。我用的版本是这样的// clk0 域sel1 时关闭 clk0sel0 且 clk1 已关闭时才能打开 always (negedge clk0 or negedge rst_n) begin if (!rst_n) en_clk0 1b1; else en_clk0 ~sel ~en_clk1; end // clk1 域sel0 时关闭 clk1sel1 且 clk0 已关闭时才能打开 always (negedge clk1 or negedge rst_n) begin if (!rst_n) en_clk1 1b0; else en_clk1 sel ~en_clk0; end逐一推演切换过程。假设当前sel0输出clk0en_clk01、en_clk10。现在sel变1请求切到clk1在clk0的下降沿en_clk0更新为~sel ~en_clk1 0clk0通道关闭。由于这个变化发生在clk0的低电平期间clk0最后一个高电平是完整的。此时~en_clk01但en_clk1要等下一个clk1下降沿才会更新。在clk1的下降沿en_clk1更新为sel ~en_clk0 1clk1通道打开。同样地这个变化发生在clk1低电平期间clk1第一个高电平一定是完整的。切换回clk0也是同样的对称过程sel拉低后clk1先关clk0后开。整个过程两路使能永远不会同时为1而且每一路输出脉冲都是完整的从结构上彻底消除了毛刺产生的条件。这里注意一个细节切换过程中输出会有一段低电平停顿。因为先关了旧时钟等新时钟的下降沿到来才把新时钟放行这个间隙可能持续几个周期。无毛刺时钟切换允许这个停顿实际SoC里的下游逻辑在设计时也都会容忍时钟停几个周期重点是不能有窄脉冲。2.3 一个容易被忽视的关键点复位初值我见过网上很多版本的代码把两个使能信号的复位值都写成1。表面上看复位后两路都允许输出似乎问题不大但如果结合上面那个互锁表达式分析初始状态如果两个使能都为1而sel0在第一个clk0下降沿en_clk0就会被清成0随后在clk1下降沿en_clk1也会被清成0输出会短暂变成无时钟状态电路要经过好几个周期才能收敛到正确的一路。这种不确定行为在工程上是不可接受的。所以复位初值必须明确指定默认输出哪一路。如果系统上电默认走clk0就把en_clk0复位为1、en_clk1复位为0如果默认走clk1反过来设置。这个细节也是面试官常追问的点。3. 可综合RTL经典实现与异步安全版本3.1 经典版代码与模块说明先给出完整经典版代码模块名用clk_mux_glitch_freemodule clk_mux_glitch_free ( input wire clk0, input wire clk1, input wire sel, // 0: 输出 clk0, 1: 输出 clk1 input wire rst_n, output wire clk_out ); reg en_clk0; reg en_clk1; // clk0 通道使能 always (negedge clk0 or negedge rst_n) begin if (!rst_n) en_clk0 1b1; // 复位默认输出 clk0 else en_clk0 ~sel ~en_clk1; end // clk1 通道使能 always (negedge clk1 or negedge rst_n) begin if (!rst_n) en_clk1 1b0; // 复位关闭 clk1 else en_clk1 sel ~en_clk0; end assign clk_out (clk0 en_clk0) | (clk1 en_clk1); endmodule两个always块分别由clk0和clk1的下降沿触发。前一个块在sel1时关闭clk0只有当sel0且en_clk10时才把clk0放行后一个块在sel0时关闭clk1只有当sel1且en_clk00时才把clk1放行。两者形成互锁——你要打开自己的通道前提是对方已经关掉。这段代码的时序逻辑很简单综合出来就是一个负沿触发的D触发器加少量门电路没有任何锁存器也不依赖异步复位后的不稳定状态可以安全用于流片。3.2 带两级同步器的异步安全版本经典版虽然能解决毛刺但它有一个隐藏假设sel信号相对两个时钟都是稳定的至少不能恰好在触发器建立保持时间窗口内变化。如果sel是异步信号直接用会有亚稳态风险。更严格的做法是先把sel同步到每个时钟域再把反馈信号也同步回去。完整代码如下module clk_mux_glitch_free_sync ( input wire clk0, input wire clk1, input wire sel, input wire rst_n, output wire clk_out ); reg sel_clk0_sync1, sel_clk0_sync2; reg sel_clk1_sync1, sel_clk1_sync2; reg en_clk0_fb_sync1, en_clk0_fb_sync2; reg en_clk1_fb_sync1, en_clk1_fb_sync2; reg en_clk0, en_clk1; // sel 分别同步到 clk0 域 always (posedge clk0 or negedge rst_n) begin if (!rst_n) begin sel_clk0_sync1 1b0; sel_clk0_sync2 1b0; end else begin sel_clk0_sync1 sel; sel_clk0_sync2 sel_clk0_sync1; end end // sel 分别同步到 clk1 域 always (posedge clk1 or negedge rst_n) begin if (!rst_n) begin sel_clk1_sync1 1b0; sel_clk1_sync2 1b0; end else begin sel_clk1_sync1 sel; sel_clk1_sync2 sel_clk1_sync1; end end // en_clk1 反馈同步到 clk0 域 always (posedge clk0 or negedge rst_n) begin if (!rst_n) begin en_clk1_fb_sync1 1b0; en_clk1_fb_sync2 1b0; end else begin en_clk1_fb_sync1 en_clk1; en_clk1_fb_sync2 en_clk1_fb_sync1; end end // en_clk0 反馈同步到 clk1 域 always (posedge clk1 or negedge rst_n) begin if (!rst_n) begin en_clk0_fb_sync1 1b1; en_clk0_fb_sync2 1b1; end else begin en_clk0_fb_sync1 en_clk0; en_clk0_fb_sync2 en_clk0_fb_sync1; end end // 控制逻辑与经典版相同只是输入换成同步后的信号 always (negedge clk0 or negedge rst_n) begin if (!rst_n) en_clk0 1b1; else en_clk0 ~sel_clk0_sync2 ~en_clk1_fb_sync2; end always (negedge clk1 or negedge rst_n) begin if (!rst_n) en_clk1 1b0; else en_clk1 sel_clk1_sync2 ~en_clk0_fb_sync2; end assign clk_out (clk0 en_clk0) | (clk1 en_clk1); endmodule这段代码比经典版多了四组同步器换来的是即使sel在两个时钟域中到达时间不同或者反馈路径存在跨时钟域传播也不会出现亚稳态导致的未知状态。代价是切换延迟增加每个域都需要等反馈信号同步两级之后才能开新时钟整个切换过程可能多出几个周期。如果两个时钟本身就是同源分频的相关时钟经典版就够用没必要上这套复杂的电路。如果两个时钟完全异步且频率相差较大异步安全版更稳。3.3 工程化细节时序约束、复位策略与库单元RTL写得再漂亮放进真实项目还要过约束和工艺两关。第一时序约束。两个时钟之间的切换路径属于异步路径综合时要在SDC里显式声明否则工具会默认它们相关插入不必要的时序优化甚至时序违例。常见的写法是set_clock_groups -asynchronous \ -group {clk0} \ -group {clk1}对于反馈路径和sel同步路径可以用set_false_path约束为假路径set_false_path -from [get_pins clk_mux_inst/en_clk0] -to [get_clocks clk1] set_false_path -from [get_pins clk_mux_inst/en_clk1] -to [get_clocks clk0]第二复位策略。复位信号必须与系统复位同步释放但模块内部两个时钟域各自使用自己的复位触发。实际项目中我更推荐使用异步复位、同步释放的复位结构确保复位释放时使能信号不产生毛刺。第三工艺库单元。很多工艺库直接提供集成好的无毛刺时钟切换单元名字通常叫CLK_MUX或者ICG_CLK_MUX内部已经做好了反馈互锁和负沿处理。指定使用这类单元有两个好处库单元经过后端工程师专门验证时序和毛刺特性有保证更重要的是综合工具可以直接对它的CLK输出端口做时钟树综合不需要额外加特殊约束。如果库里有这样的单元我一般建议直接例化自研RTL版本更多用于面试、教学和需要快速迭代的原型阶段。4. 仿真与验证怎么证明它真的没有毛刺4.1 Testbench基本框架手撕代码之后验证同样重要。搭一个最小化的testbench输入时钟分别设为10ns和16ns周期覆盖从clk0切到clk1、再从clk1切回clk0两个方向timescale 1ns/1ps module tb_clk_mux_glitch_free; reg clk0, clk1, sel, rst_n; wire clk_out; clk_mux_glitch_free dut ( .clk0 (clk0), .clk1 (clk1), .sel (sel), .rst_n (rst_n), .clk_out(clk_out) ); initial begin clk0 0; forever #5 clk0 ~clk0; // 周期 10ns end initial begin clk1 0; forever #8 clk1 ~clk1; // 周期 16ns end initial begin rst_n 0; sel 0; #20; rst_n 1; #30; // 稳定运行一段时间 sel 1; // clk0 - clk1 #60; sel 0; // clk1 - clk0 #60; $finish; end endmodule4.2 自动监控脉冲宽度光用眼睛看波形不够严谨特别是毛刺出现的位置很随机。更可靠的办法是在testbench里加一个宽度监控器检测clk_out上的每一个高电平和低电平脉冲宽度任何一个小于设定的最小阈值就报错。module tb; // 接上面的 tb_clk_mux_glitch_free real last_rise, last_fall; real min_pulse; always (posedge clk_out) begin if (last_fall ! 0) begin $display(high pulse width: %0t, $realtime - last_fall); if ($realtime - last_fall min_pulse) $error(Glitch detected: high pulse too narrow); end last_rise $realtime; end always (negedge clk_out) begin if (last_rise ! 0) begin $display(low pulse width : %0t, $realtime - last_rise); if ($realtime - last_rise min_pulse) $error(Glitch detected: low pulse too narrow); end last_fall $realtime; end initial begin min_pulse 4.0; // 小于两个时钟中最短半周期的一半如 10ns 周期的一半是 5ns end endmodule阈值设置很关键。两个时钟周期分别是10ns和16ns最短高电平时间是5nsclk0的半周期。正常情况下输出任意一个高电平时长都不应该小于5ns。为了留出一点时钟沿传播裕量我把阈值设成4ns一旦出现小于4ns的高或低脉冲立刻报错。4.3 波形查看与异常分析仿真跑完在波形窗口看两点。第一切换点附近有没有宽度明显异常的窄脉冲第二从clk0切到clk1之后上升沿间隔是否逐渐稳定到16ns。实测无毛刺切换的波形特征是切换前输出一直是10ns周期切换命令发出后输出先结束当前clk0的完整周期然后保持一段时间低电平接着开始按16ns周期输出clk1。整个过程中输出高电平的宽度要么是5ns、要么是8ns左右绝对不出现3ns、4ns这样的异常值。如果在波形上真的看到了窄脉冲不要急着怀疑设计先检查仿真时间和精度设置。把timescale设成1ns/1ps还不够就改1ns/100ps排除精度不够造成的显示误差。RTL仿真阶段没有门延迟理论上不会出现工艺偏差导致的毛刺如果出现了几乎可以肯定是RTL写法的问题而不是仿真环境问题。5. 高频排查清单与我的实操心得5.1 有毛刺时的快速排查表毛刺现象可能原因排查方向切换点出现窄高脉冲使能条件没有在低电平翻转检查是否用了posedge打拍切换后输出频率不对sel同步器初值或极性反了核对复位值和sel极性两路时钟同时出现反馈互锁逻辑被综合优化掉了检查综合选项是否保留冗余保护逻辑复位后输出短暂无时钟两个使能复位值都为1明确默认输出设置合理的复位初值切换卡死输出一直停住目标时钟被门控或停振确认两路时钟都是free-running综合后仿真毛刺使能路径存在竞争或库单元时序问题检查SDC约束考虑直接例化库单元5.2 异步时钟切换容易踩的坑经典版在面试里够用但真实项目中如果两个时钟完全异步有几个坑必须提前想清楚。第一个坑是亚稳态。sel如果来自外部总线的配置域直接用经典版采样可能在某个瞬间让两个使能都进入亚稳态仿真里难以复现芯片上随机出现功能错误。异步安全版里的两级同步器正是为这个准备的。第二个坑是反馈信号的传播时间。异步安全版里关掉一路时钟后反馈信号要经过两级同步才能让另一路打开中间的时间可能很长。如果切换频率很高连续切换会让时钟停摆时间累积下游逻辑可能因为超过最长等待时间而出错。设计时最好加一个简单的状态标志让配置软件知道切换是否完成。第三个坑是约束误设。有些人喜欢把所有跨时钟域路径都设成set_false_path这在大方向上是可靠的但如果同时把时钟切换模块本身的输出路径也当假路径处理时钟树综合会出问题。我的经验是切换电路内部路径设假路径但clk_out后面的时钟网络不要动让时钟树工具正常处理。5.3 实际项目里的几点建议最后分享几条实操建议。第一能用库单元就别自己造轮子。菜市场有现成的CLK_MUX单元除非你是IP设计或者面试否则没必要在项目里手写这个电路。自研版本要面对工艺库没有专门timing model的问题后端验证工作量会明显增加。第二复位策略要跟系统时钟策略统一。如果上电默认走低频慢时钟复位后en_clk0和en_clk1的设置必须满足“慢时钟立即输出、快时钟保持关闭”否则PLL还没锁定时快时钟可能被直接放行触发一整片逻辑。第三验证不能只看功能仿真。有条件的话加一条SVA断言专门检查输出时钟的上升沿间隔始终大于某个阈值然后在回归测试里挂着跑比人工看波形可靠得多。断言代码不复杂但能给验证团队减少很多沟通成本。第四如果切换的是同源分频的相关时钟还有一个更简单的方案在分频器级别做同步切换先把两个分频分支都保持在同一相位再通过一个统一的使能切换输出。这种方案天然没有毛刺但只适用于同源时钟和本文讨论的通用无毛刺切换电路适用场景不完全一样。我在实际项目中用过这个模块做过DVFS的时钟切换也帮同事排查过几次“时钟切换后整个模块跑飞”的问题最后定位到的原因基本都是复位初值没配对或者约束漏配。这种看起来只有二十行的模块恰恰是最容易在细节上出问题的地方。希望这篇内容能帮你少踩几个坑。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。