ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

无毛刺时钟切换设计:原理、Verilog实现与验证要点

2026/9/28 5:37:42 拓冰建站 浏览量
无毛刺时钟切换设计:原理、Verilog实现与验证要点 1. 为什么切换时钟会冒毛刺先把问题定义清楚无毛刺时钟切换Glitch-free Clock Switching在数字IC里是个高频面试题同时也是实际项目中很常见的需求。通信芯片里有多档工作频率CPU子系统有低频待机模式SoC里有多个PLL动态调频这些场景都需要在系统运行过程中把某个时钟域的主时钟从一路切到另一路。关键是切换动作发生时电路不能停更不能因为切换本身产生一个不完整的时钟脉冲。很多人第一次听到这个问题第一反应是“用MUX不就行了吗”CLK_SEL为低选A钟为高选B钟一拍搞定。但稍微画一下时序就会发现如果特权切换发生在A钟的高电平中间输出时钟会变成一个窄脉冲如果发生在低电平中间输出时钟会出现一个比正常周期短很多的半拍。更糟的情况是两路时钟完全异步相位关系完全随机输出时钟的最后一个沿和第一个沿间隔完全不可控后端时序约束根本没法做。我当年第一次被问到这个问题时面试官让我先画“错误切换”的波形再画“理想切换”的波形。画完才发现问题的核心根本不在MUX本身而在“切换的那一拍”。理想切换行为是旧时钟完整走完最后一个周期然后新时钟从第一个完整周期开始输出中间不能出现任何窄脉冲、直通毛刺和亚稳态传递。这里先说一个基本概念毛刺的本质是什么数字电路里时钟毛刺就是一个比规范最小脉宽还窄的脉冲它可能触发生成触发器采样到错误数据也可能让计数器状态错乱还可能让异步复位生效的时序完全失控。哪怕毛刺只窄了0.1ns后仿中触发器的hold时序也可能挂掉这不是概率问题是确定性违例。所以无毛刺切换设计的最终检查标准就一句话切换前后输出时钟的每一个高电平宽度、每一个低电平宽度都必须满足目标时钟所要求的脉冲宽度约束。那为什么老设计里偶尔会看到直接用MUX切时钟的设计因为这类设计往往限制了切换发生的时刻——比如只在旧时钟为低电平时切换且两路时钟同源、频率成整数倍关系。一旦限制条件不满足就会出问题。无毛刺时钟切换电路的价值就在于去掉这些限制让切换动作在任意时刻发起都能安全完成。这也是为什么它在数字IC校招面试和社招手撕代码环节里出现率这么高。2. 经典无毛刺切换架构拆解每一步都为了解决一个具体问题先看教科书里最常见的结构。它由三部分组成输入选择信号的前级同步、两套独立的“沿检测下降沿打拍”链路以及最后的时钟门控与输出组合。整个设计的目标是把“任意时刻发起的异步切换请求”转换成“只发生在旧钟下降沿和新钟下降沿的安全切换时刻”。2.1 核心思路用时钟下降沿统一所有动作为什么一定要用下降沿想象一下输出时钟的高电平宽度是怎么形成的。输出钟的高电平由两段拼接前半段来自旧钟后半段来自新钟。如果切换动作发生在某一时刻旧钟的最后一个高电平和新钟的第一个高电平连在一起拼接点两侧的电平都是高高电平宽度就是“旧钟在此之前的剩余高电平时间”加上“新钟从零开始的高电平时间”。这个值跟两路时钟的相位差直接相关可能远小于正常脉宽。但下降沿不一样——切换发生在旧钟下降沿之后、新钟下降沿之后拼接点落在低电平区间新旧两钟的完整高电平都被保留拼接出来的高电平宽度恰好等于其中某一路的完整半周期。因为低电平时钟输出为0所以拼接点不会产生毛刺。这就是这个电路的核心手筋寄存器在下降沿打拍产生一个带有“下降沿之后才改变”性质的控制信号用它来门控时钟使时钟切换只可能发生在下降沿附近。所有路径都对齐到下降沿从根本上避开高电平脉冲拼接问题。2.2 同步器为什么要在这里插两级触发器切换请求信号CLK_SEL通常来自系统控制总线跟A钟、B钟毫无相位关系。如果直接把它送进门控逻辑CLK_SEL可能在A钟上升沿附近变化第一级触发器可能采到亚稳态。亚稳态的Q端在解析完成前会振荡、漂移如果这个Q直接参与时钟门控输出会直接冒毛刺。所以标准做法是在A钟域用两级触发器同步沿检测信号B钟域同样用两级触发器。目的是什么第一级触发器可能进入亚稳态但多给一个周期让它有足够时间恢复到确定电平。两级触发器的输出虽然仍可能在极端情况下出错但至少是“确定性的错”不是亚稳态的不确定行为。这个在功能仿真里看不出来但在真实硅片上亚稳态窗口会导致输出的时钟沿时间抖动后端时序收敛极其困难。要注意的是这里的同步器不能省略。有些同学面试时会说“CLK_SEL是静态信号切过去之后不会再变”言下之意不用同步。这在某些受控场景下也许成立但在通用设计里谁也不能保证软件不会在切换过程中改寄存器、不复位模块不会发出重复请求。同步器的代价只是一个周期左右的延时换来的是设计对输入时序的完全不敏感这笔账很划算。2.3 沿检测与下降沿打拍请求信号如何变成安全的使能再看每个时钟域内部的具体逻辑。以A钟为例当外部选择信号要求切到A钟时经过A钟域同步后得到一个内部信号sel_a_sync。我们希望当sel_a_sync变为高时不是立刻把A钟放出来而是要等到A钟的下降沿才真正让A钟穿过门控。为什么如果A钟目前没在输出说明B钟正在输出。这时A钟的上升沿如果直接打开门控A钟的高电平会插进当前B钟的高电平中间产生毛刺。等到A钟下降沿才打开门控是因为当前输出如果是B钟B钟此刻很可能处于高电平或低电平的任意位置只有让A钟的上升沿与B钟的某个边界错开才安全。设计里用一个下降沿触发的触发器来寄存“A钟请求已经到达”的状态。这个寄存器的输出sel_a_gated只在A钟下降沿变化。然后把它和A钟做与门输出gated_a。B钟域做同样的事情sel_b_sync经过B钟下降沿打拍得到sel_b_gated与B钟相与得到gated_b。最后gated_a和gated_b相或就是输出CLK_OUT。为什么两路要同时置位而不是只置位一路因为切换前旧钟的控制信号还在高电平新钟的控制信号还没拉高这一拍内两个门控输出都是低电平输出时钟维持在低。等到新钟下降沿到来新钟的控制信号拉高新钟开始输出高电平。输出从低到高的跳变必然是新钟的上升沿因为此时旧钟已经关断新钟的低电平区间覆盖了切换点。整个过程中输出时钟不会出现一个不完整的脉冲。2.4 几条关键路径的时序博弈这个架构里有几条路径值得单独拎出来想。第一条路径是从外部CLK_SEL变化到最终的CLK_OUT恢复稳定。这条路径包含同步器的两级触发器、逻辑门和门控单元。一共两个周期左右的延时。对实时性要求高的系统需要注意切换不是瞬时的它需要一个安全窗口。第二条路径是“请求保持”路径。见很多初学者会问如果CLK_SEL在切换完成前又变回去了怎么办答案是电路里专门设计了“状态保持”——sel_a_gated一旦拉高不会因为CLK_SEL变低而立刻撤销它会保持到对方时钟域完成切换逻辑更新之后。这需要所谓的“请求-确认”握手逻辑。教科书里的基本型电路其实已经隐式包含了这个机制sel_a_gated和sel_b_gated同时为高时两个门控都打开输出是两个时钟“竞争”的结果若CLK_SEL此时又要切回A钟由于两个使能都已拉高输出会在一拍内重新选中A钟但这个过程仍然受下降沿约束所以不会冒毛刺。第三条路径是两路时钟都关闭时的“全零窗口”路径。若时钟切换过程中两个时钟域的下降沿各自来了一次控制信号都变成了低此时输出被钳在低电平。这个窗口最长可能跨过两个时钟周期对下游逻辑而言相当于时钟“停摆”了几个周期。如果下游有异步FIFO的读指针、有实时采样逻辑必须能容忍短暂停摆。3. 可综合Verilog实现与逐段注释手撕代码的正确姿势直接上代码。下面这个版本是经典结构的完整实现带参数化位宽适合面试时边写边讲。我强烈建议你把这个版本背下来、理解透面试时在此基础上做扩展。module glitch_free_clk_mux ( input wire clk_a, // 时钟A input wire clk_b, // 时钟B input wire clk_sel, // 0: 选择clk_a, 1: 选择clk_b input wire rst_n, // 异步复位低有效 output wire clk_out // 无毛刺切换输出 ); // ------------------------------------------------------------ // 第一级同步器消除亚稳态 // ------------------------------------------------------------ reg [1:0] sel_a_sync_ff; reg [1:0] sel_b_sync_ff; always (posedge clk_a or negedge rst_n) begin if (!rst_n) begin sel_a_sync_ff 2b00; end else begin sel_a_sync_ff {sel_a_sync_ff[0], (~clk_sel)}; end end always (posedge clk_b or negedge rst_n) begin if (!rst_n) begin sel_b_sync_ff 2b00; end else begin sel_b_sync_ff {sel_b_sync_ff[0], clk_sel}; end end wire sel_a_sync sel_a_sync_ff[1]; wire sel_b_sync sel_b_sync_ff[1]; // ------------------------------------------------------------ // 第二级下降沿打拍使切换动作发生在安全边界 // ------------------------------------------------------------ reg out_a_r1; reg out_b_r1; always (negedge clk_a or negedge rst_n) begin if (!rst_n) begin out_a_r1 1b0; end else begin out_a_r1 sel_a_sync (~out_b_r1); end end always (negedge clk_b or negedge rst_n) begin if (!rst_n) begin out_b_r1 1b0; end else begin out_b_r1 sel_b_sync (~out_a_r1); end end // ------------------------------------------------------------ // 第三级门控与输出组合 // ------------------------------------------------------------ wire clk_a_gated clk_a out_a_r1; wire clk_b_gated clk_b out_b_r1; assign clk_out clk_a_gated | clk_b_gated; endmodule这一段代码逐行看下来逻辑非常清晰。注意几个细节。同步器的输入为什么要取反因为约定clk_sel为0选择A钟但A钟域内部的有效请求是“我要输出A钟”这个请求逻辑上等于“clk_sel为0”写成~clk_sel。你也可以反过来让clk_sel为1选A钟两个时钟域的输入都直接接clk_sel只是这样默认状态需要仔细处理。面试时一定要把选择极性说清楚否则很容易被追问。为什么下降沿打拍的输入是sel_a_sync (~out_b_r1)而不是直接sel_a_sync这就是握手逻辑的精髓。只有当对方控制器已经撤销输出out_b_r1为0本方才允许把请求置位。这样避免了两个门控同时打开的竞争态。原始教科书电路常写成sel_a_sync (~out_b_r1)其实还有另一种写法是(sel_a_sync | out_a_r1) (~out_b_r1)后者的含义是“保持当前A钟请求直到对方释放”对切换时序更鲁棒。握手这部分我在面试时见过不少候选人卡住。他们知道要在下降沿打拍但不知道为什么打拍后的信号要参与对方路径的逻辑。其实道理很简单切换请求“我要切A钟”需要被A钟域接收但接收的前提是“B钟域已经安全退出”。只要out_b_r1为高A钟域就不能把out_a_r1拉高因为此刻输出还在B钟两个门控同时打开会让输出同时出现两个时钟的波形组合逻辑上会产生竞争。等到B钟下降沿到来out_b_r1变成0下一拍A钟下降沿到来时out_a_r1才会拉高。这一来一回正好实现了安全交接。为什么复位只复位降沿打拍的寄存器不同步器的寄存器不参与输出同步器输出是内部中间信号就算复位前有不确定值两拍之后也会收敛。但out_a_r1和out_b_r1直接控制门控必须复位成0让输出时钟保持低电平。如果不复位上电后两个寄存器是X态门控输出会冒出大量毛刺。4. 仿真验证的构建思路与边界条件无毛刺不是仿真看出来的是约束出来的写完代码下一步就是验证。手撕代码阶段很多同学喜欢直接跑一个简单testbench看到波形没有毛刺就认为设计正确。我个人的经验是功能仿真里看不到毛刺并不代表设计真的无毛刺。因为仿真器里门控逻辑是理想模型0延时跳变真正的毛刺发生在后端布线之后。所以验证的思路应该是第一验证逻辑功能正确切换请求发出后输出时钟确实从A钟变成B钟频率切换正确。第二验证切换过程中没有产生“非预期边沿”在输出波形上打标记检查所有上升沿、下降沿是否都来自某一输入时钟且相邻沿之间的间隔满足最小脉冲宽度。第三验证极端边界两路时钟频率相近、相位对齐、频率相差很大、切换请求在A钟高电平中间发出、切换请求在B钟低电平中间发出、连续多次切换这些情况都要跑一遍。下面是一个基础testbench的框架包含可调的时钟频率和相位参数。我这里用Verilog直接写方便大家在仿真工具里快速跑通。timescale 1ns/1ps module tb_glitch_free_clk_mux; reg clk_a 0; reg clk_b 0; reg clk_sel 0; reg rst_n 0; wire clk_out; // 时钟A100MHz always #5 clk_a ~clk_a; // 时钟B150MHz带相位偏移 initial #2 clk_b 1; always #3.333 clk_b ~clk_b; glitch_free_clk_mux dut( .clk_a (clk_a), .clk_b (clk_b), .clk_sel(clk_sel), .rst_n (rst_n), .clk_out(clk_out) ); // 监控输出时钟的最小高电平宽度/低电平宽度 real min_high, min_low; real last_pos_edge, last_neg_edge; always (posedge clk_out) begin if (last_neg_edge 0) min_low (min_low ($realtime - last_neg_edge)) ? min_low : ($realtime - last_neg_edge); last_pos_edge $realtime; end always (negedge clk_out) begin if (last_pos_edge 0) min_high (min_high ($realtime - last_pos_edge)) ? min_high : ($realtime - last_pos_edge); last_neg_edge $realtime; end integer i; initial begin $dumpfile(wave.vcd); $dumpvars(0, tb_glitch_free_clk_mux); rst_n 0; repeat(10) (posedge clk_a); rst_n 1; // 等待A钟稳定输出 repeat(5) (posedge clk_a); // 在任意时刻发起切换到B钟 clk_sel 1; repeat(20) (posedge clk_out); // 再次切换回A钟 clk_sel 0; repeat(20) (posedge clk_out); // 连续快速切换 clk_sel 1; repeat(3) (posedge clk_out); clk_sel 0; repeat(3) (posedge clk_out); clk_sel 1; repeat(30) (posedge clk_out); $finish; end endmodule4.1 断言检查把“无毛刺”变成可自动判定的属性肉眼盯着波形看不是长久之计。工程上我建议顺手写几个SVA断言把验证标准明确化。这里给三条最关键的// 输出时钟高电平宽度不得小于B钟周期的45% property p_high_width; real high_start; (posedge clk_out) (1, high_start $realtime) | (negedge clk_out) ($realtime - high_start 3.0); endproperty跑仿真时如果断言报错就把时间点报出来回查远比手动看波形高效。注意这里的阈值不能设成50%因为实际电路里高电平宽度可能因为门控逻辑的传播延时产生微小偏差设成45%~48%比较合理能留出制造和仿真误差空间。4.2 后仿与时钟偏移功能仿真正确只是入场券真正到后仿阶段问题会多得多。主要原因是时钟树综合CTS之后clk_a和clk_b到达门控单元的时间有偏差逻辑门本身也有cell delay。这些偏差叠加在一起可能会让本来安全的切换点变成不安全的。标准做法是在综合网表上把门控单元的输入时钟设为set_clock_gating_check告诉工具“门控信号必须在时钟沿之前稳定”。这样工具会自动优化门控逻辑的时序关系保证功能正确性。另外对于多路时钟切换后端通常会要求统一用ICG单元Integrated Clock Gating做门控而不是直接写clk_a out_a_r1。ICG单元有专门的使能端工具能检查clk_en与clk之间的时序关系也能避免逻辑综合把与门优化掉导致的问题。很多面试官会追问“能不能在综合时把clk_a out_a_r1优化掉”答案是不会因为综合工具默认把时钟网络标记为不可合并逻辑但为了保险起见项目里还是推荐例化工艺库的ICG单元。4.3 关键边界同频同相时钟的切换一个容易被忽略的边界条件是两路时钟完全同频同相。这种情况下即使不做任何处理直接切换也不会冒毛刺因为波形完全重合。但我们的设计仍然要保证功能正确而且要注意同频时钟若相位有小偏差哪怕只有几百ps的偏斜切换时输出时钟的周期会产生一个微小的抖动。这个抖动在jitter容限要求高的系统里可能触发PLL失锁。所以即使是同频切换也应该让切换动作发生在新时钟的第一个完整周期而不是试图“无感切换”。这个设计天然满足这个要求。5. 从手撕代码到落地实现关于这个设计的进阶细节与替代方案讲完最基础的经典架构再聊几个面试和项目中经常深挖的方向。很多候选人会背标准答案但一旦被问到“如果不满足这个标准答案要怎么办”就卡壳了。这里我把常见进阶问题盘点一下。5.1 自切换请求与“保持”逻辑feedback模式详解前面代码里的握手逻辑是通过(~out_b_r1)实现的这是一种简化的握手形式。真正工业级的设计往往需要在每个时钟域内部把请求信号“锁存”住直到确认对方已释放。标准的写法是// A钟域内部 reg req_a_ff1, req_a_ff2; always (posedge clk_a or negedge rst_n) begin if (!rst_n) begin req_a_ff1 1b0; req_a_ff2 1b0; end else begin req_a_ff1 ~clk_sel; req_a_ff2 req_a_ff1; end end reg ack_a_ff1, ack_a_ff2; always (posedge clk_a or negedge rst_n) begin if (!rst_n) begin ack_a_ff1 1b0; ack_a_ff2 1b0; end else begin ack_a_ff1 out_b_r1; ack_a_ff2 ack_a_ff1; end end // 输出使能请求请求有效且对方已确认释放 wire req_a req_a_ff2 ~ack_a_ff2;这种结构把“请求/确认”完全显式化好处是不会出现CLK_SEL抖动导致两个时钟域反复抢输出的情况。代价是电路面积和复杂度增加。基础面试题里一般不要求做到这一步但能在面试中提到“还有反馈握手版本”会加分不少。5.2 多路时钟切换从2选1扩展到N选1两路时钟切换电路能不能扩展成多路当然能但有两种做法。一种做法是两步走先做一个两两拼接的二叉树N路时钟通过log2(N)级的无毛刺切换器串联每一级处理两个输入。这种做法实现简单但有个问题中间节点的时钟会引入额外延时对高频时钟不利另一个问题是不支持“任意两路之间直接切换”比如从clk_0直接切到clk_3需要先切到clk_1再切到clk_2这会增加切换时间期间还可能引入不必要的中间频率。另一种做法是状态机控制。先为每一路输入时钟设计一个独立的“请求/使能”位然后一个中央状态机负责仲裁哪一路的使能输出为高。当切换请求到来时先把旧路的使能撤掉等待旧路对应时钟的下降沿确认释放再把新路的使能拉高同样等待新路时钟的下降沿确认建立。仲裁逻辑可以防止两个输入同时被选中。两种做法对比下来工业界更常见的是第二种因为多路时钟切换往往伴随动态调频策略切换路径的确定性很重要。二叉树适合时钟数量少且切换路径不敏感的场景。5.3 时钟偏移对设计的影响为什么后端约束如此重要无毛刺切换电路在后端实现时最大的坑就是时钟偏移。假设clk_a和clk_b两个时钟树到达门控单元时存在1ns的偏移差那么即使功能逻辑正确输出端依然可能产生一个非常窄的毛刺。原因在于两个门控输出相或的时候如果一方已经拉低、另一方还没完全拉高中间的电平状态是悬空的。后端工程师的标准处理方式有三种第一对门控单元时钟端口设置set_clock_gating_check让工具自动在综合网表中保证门控信号与时钟沿之间的setup/hold关系。这是必须做的不做后面没法签核。第二在布局规划时尽量把切换电路放在两个时钟源共用路径的汇聚点附近减少两个时钟树到达延时差。第三如果工艺库支持尽量使用专门的ICG单元。初学者往往忽略后端的影响觉得功能仿真通过就够了。等到芯片回来量测发现特定电压温度下切换时偶尔冒出一个毛刺才会意识到这个问题的严重性。所以做这个设计时从一开始就灌输“时钟是模拟信号”的思维会少走很多弯路。5.4 从功能仿真到Formal验证如何证明“无法产生毛刺”除了动态仿真形式化验证Formal Verification也是证明无毛刺的重要手段。把“CLK_OUT上不存在短于X ns的高电平/低电平脉冲”写成形式化属性用工具去穷举所有输入序列和时序组合。因为状态空间不大这个属性通常能很快证明完毕。动态仿真可能覆盖几十万个时钟周期但形式化验证可以覆盖所有可能性两者结合基本能把毛刺问题堵死。5.5 和其他方案对比异步FIFO、握手协议和脉冲屏蔽最后说说替代方案。有些场景下与其切时钟不如把数据先缓存到异步FIFO里等时钟切换完成后继续运行。异步FIFO本身也需要无毛刺时钟切换器来产生读时钟和写时钟所以它并不是替代方案而是与无毛刺切换器共存的关系。另一些场景会用“脉冲屏蔽”逻辑在CPU里把时钟切换的窗口设置成指令间隙先让CPU停在某个固定的流水线阶段再等待一个安全窗口执行切换。这种方案对软件要求高硬件上反而可以简化。但对于TBMTraffic Block Multiplexing、SerDes的动态速率切换等场景不可能让流水线暂停所以还是要靠纯硬件自动切换。还有一种是“慢速握手协议”方案例如使用collection tree结构先让两个时钟域各自产生一个“准备好切换”的脉冲再经过一个同步握手结决定切换时刻。这个方案与经典结构在实现细节上不同但最终目的都是让切换沿对齐到下降沿。从我个人角度看经典下降沿打拍结构仍然是性价比最高的方案。它简洁、可综合、时序可控、逻辑清晰。如果你的目标是准备面试把这一段代码吃透再能把“为什么用下降沿”“为什么加同步器”“为什么需要握手”讲明白就足够了。如果目标是落地项目再多想一步后端约束和形式化验证踩坑的概率会大大降低。