ARTICLE DETAIL

资讯详情

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

无毛刺时钟切换原理与Verilog实现:从毛刺成因到工程实践

无毛刺时钟切换原理与Verilog实现:从毛刺成因到工程实践 干这一行的人对手撕代码这个词应该都不陌生。数字IC设计和验证岗位的笔试面试里总有那么几个题目是绕不开的其中无毛刺时钟切换Glitch-free Clock Switching绝对算得上高频题。有些同学背了一段Verilog就上考场结果被追问一句为什么下降沿采样就卡壳了说到底还是没把这个电路从原理上吃透。我前后在FPGA验证和ASIC前端都折腾过不少项目时钟切换这个功能几乎是必用的低频的、高频的、同源的、异步的时钟都切过踩过不少坑。这篇就把这道经典手撕题从根上拆一遍毛刺怎么来的、电路怎么设计的、代码每一行为什么这么写、仿真怎么确认它真的无毛刺最后聊几个工程落地时才会遇到的细节。1. 切换时钟会闪断先搞清楚毛刺从哪来1.1 两种典型的毛刺产生路径时钟切换看起来只是用一个MUX把两个时钟二选一但问题恰恰就出在这个简单的MUX上。假设你直接写assign clkout sel ? clk1 : clk0;当sel变化的瞬间如果该时刻clk0和clk1的电平不一致输出就可能被截掉半个脉冲、或者拼出一个比正常时钟周期窄得多的短脉冲。这个短脉冲在数字电路里就是glitch后续寄存器可能把它当真时钟沿采进去逻辑瞬间乱套。拆开看毛刺主要有两种来源。第一种来源是选择信号sel在时钟高电平期间变化。数字电路里sel往往是从配置寄存器同步过来的它的翻转时刻和两个时钟沿都没有严格对齐的关系。假如sel在clk0为高电平时才从0变到1组合逻辑里clk0通道的与门关闭动作和高电平本身会产生竞争输出端原本应该完整输出的高电平脉宽被硬生生截断落在时序上就是一个窄脉冲。第二种来源是两路时钟的相位差异。clk0和clk1即使频率相同相位也不一定对齐更常见的情况是它们完全异步。切换瞬间clk0的上升沿可能刚过clk1的上升沿还没来输出从一个时钟的节奏跳到另一个时钟的节奏如果关断和开启的时机没有落在低电平区间就会出现一个远短于正常半周期的畸形脉冲。理解了毛刺怎么来的就明白无毛刺切换的核心诉求了切换动作必须保证输出时钟的完整性和连续性任何时刻的输出脉冲宽度不能小于正常时钟的最小半周期并且切换的过程要在一个稳定可预测的低电平区间内完成。工程上更严格的说法是切换前后的时钟脉冲不允许出现被截断、被拼接或者小于后端库中最小脉宽要求的假沿。1.2 简单MUX为什么不行用一个例子说透举个我调试过的实际场景。芯片里有两个时钟源一个是低频的慢速时钟一个是PLL倍频出来的高速时钟上电时系统先用慢速时钟跑初始化等PLL锁定之后再切到高速时钟。这个切换必须平滑否则CPU核心里那几千个寄存器一旦被毛刺时钟驱动状态就全乱了。在这个场景里毛刺的风险点集中在切换的瞬间。慢速时钟的周期比如是20ns高速时钟周期是2ns20ns脉冲切到2ns时钟的刹那如果输出先出现了大约0.5ns的碎片脉冲那这半个纳秒的被当成一个时钟沿传给下游整个时钟树的一致性就崩了。更麻烦的是毛刺问题在仿真里不一定立刻暴露很多毛刺是在后仿或者芯片实测时才出现的因为组合逻辑的门延迟、走线延迟会把毛刺塑造出来。这也是为什么面试官反复追问这个题目因为这不是一道纯理论题它直接影响芯片能不能稳定跑起来。2. 低电平握手原理无毛刺切换的经典架构2.1 核心思路先完全关断源时钟再打开目标时钟传统MUX的问题是一瞬间完成选择而正确的思路是把切换过程拆成两步握手。第一步把当前使用的源时钟通道彻底关掉注意这个关闭动作要发生在源时钟的低电平区间第二步把目标时钟通道打开打开动作同样要发生在目标时钟的低电平区间。两个通道一个先关、一个后开输出中间会出现一小段稳定的低电平但不会有任何毛刺。这个思路听起来简单实现起来有几个关键约束。第一个约束是关闭要等源时钟的下降沿。如果你在源时钟高电平期间关断与门输出就会留下一个不完整的脉冲。必须让关断信号在源时钟的下降沿采样并生效确保关断之后源时钟的输出立即变低这样最后一个脉冲是完整的高电平脉冲。第二个约束是打开要等目标时钟的下降沿。目标时钟通道如果任意打开可能恰好赶上目标时钟高电平的中段开始输出这样新时钟的第一个脉冲也是不完整的。只有在目标时钟的下降沿打开目标时钟先从低电平长出下一个上升沿才能给出完整脉冲。这一步握手是无毛刺的灵魂所在也是面试时最容易被追问的细节。2.2 为什么用两级同步器加一个下降沿触发器电路结构上每一路时钟通道需要三个核心部分。第一两级上升沿同步器用来把跨时钟域的选择控制信号同步到本时钟域第二一个下降沿采样触发器用来生成最终的时钟使能信号第三一个与门把使能信号和真正的时钟做逻辑与输出到最终时钟MUX。两级同步器的意义不仅仅是防亚稳态。在这个电路里同步器还承担了通道间互锁的功能。打开目标通道的前提是源通道已经关闭这个状态需要跨时钟域传递而两级同步器能保证这个状态在一个时钟域内被稳定采样。具体来说目标通道使能端的逻辑是选择目标时钟并且源通道反馈信号为0源通道反馈信号从源时钟域跨到目标时钟域必须经过同步器过滤否则可能采样到亚稳态。下降沿触发器是很多人容易忽略的部分。它把两级同步器输出的使能信号在时钟的下降沿进行采样。这么做的原因是要强制使能信号只在低电平期间发生变化。试想使能信号如果在时钟高电平期间变0与门输出就会截断脉冲如果在高电平期间变1输出就会冒出一个高电平碎片。把使能变化锁在下降沿之后与门的输出变化也就锁在了低电平区间。3. 手撕RTL代码逐行拆解一份可直接使用的Verilog实现3.1 模块骨架两个通道、一个sel、一个复位我直接给一份我常用的标准写法。这个结构从经典的Cypress应用笔记演化而来也广泛出现在各种教科书和面试答案里经过大量仿真验证工程上可以放心使用。module glitch_free_clk_mux ( input wire clk0, input wire clk1, input wire sel, // 1b0: 选择clk0, 1b1: 选择clk1 input wire rst_n, // 异步复位低有效 output wire clkout ); reg sync0_r0, sync0_r1; reg sync1_r0, sync1_r1; reg clk0_neg_en, clk1_neg_en; wire clk0_gate; wire clk1_gate; // ---------- clk0 通道 ---------- always (posedge clk0 or negedge rst_n) begin if (!rst_n) begin sync0_r0 1b0; sync0_r1 1b0; end else begin sync0_r0 ~sel ~sync1_r1; sync0_r1 sync0_r0; end end always (negedge clk0 or negedge rst_n) begin if (!rst_n) clk0_neg_en 1b0; else clk0_neg_en sync0_r1; end // ---------- clk1 通道 ---------- always (posedge clk1 or negedge rst_n) begin if (!rst_n) begin sync1_r0 1b0; sync1_r1 1b0; end else begin sync1_r0 sel ~sync0_r1; sync1_r1 sync1_r0; end end always (negedge clk1 or negedge rst_n) begin if (!rst_n) clk1_neg_en 1b0; else clk1_neg_en sync1_r1; end // ---------- 输出逻辑 ---------- assign clk0_gate clk0 clk0_neg_en; assign clk1_gate clk1 clk1_neg_en; assign clkout clk0_gate | clk1_gate; endmodule这份代码最大的特点就是非阻塞赋值写同步器两级寄存器之间没有任何组合逻辑插在中间时序干净利落。可能有同学会问为什么同步器的数据端不只是sel还要带上另一个通道的反馈~sync1_r1这里恰恰就是互锁逻辑。clk0通道只有在需要clk0且clk1还没真正接管的前提下才会把使能置1。clk1通道同理。两个通道的关系就像一扇双开门永远保证至少有一侧被锁住不允许同时敞开。3.2 关键代码行逐条解释为什么这么写才安全先看clk0通道的第一级同步器赋值sync0_r0 ~sel ~sync1_r1;当sel为0要clk0并且sync1_r1为0clk1通道的反馈同步输出还没有激活时sync0_r0才能置1。注意这里的sync1_r1是clk1域经过两级同步后的信号它跨到clk0域时本身可能处于亚稳态窗口但作为控制握手信号它两拍之内足够稳定下来。逻辑上即使采样到亚稳态的中间值也不会让两个通道同时打开因为两个通道的互锁条件是相互的这个保守设计反而形成了安全的握手。再看下降沿触发器always (negedge clk0 or negedge rst_n) begin if (!rst_n) clk0_neg_en 1b0; else clk0_neg_en sync0_r1; endclk0_neg_en只在clk0的下降沿更新所以它由0变1或由1变0都发生在clk0低电平区间。这样clk0_gate clk0 clk0_neg_en的输出沿不会出现在clk0高电平的中段关闭和开启天然无毛刺。输出逻辑是纯组合assign clkout clk0_gate | clk1_gate;两个与门的输出做或。平时只有一个通道的使能为1输出就是那个通道的时钟切换瞬间两个使能的交叠被互锁逻辑禁止中间经过一段低电平然后无缝切到目标时钟。3.3 扩展到三路及以上多时钟切换多路切换是这道题的加分项。工程上经常出现两个以上时钟源比如慢速晶振、PLL0、PLL1三选一。扩展的思路不是简单把两个通道贴成三份而是把互锁逻辑推广成其他所有通道都不激活。假设三路时钟是clk_a、clk_b、clk_c选择信号是one-hot的sel_a、sel_b、sel_c那么第一级同步器赋值可以写成// clk_a通道 sync_a_r0 sel_a ~sync_b_r1 ~sync_c_r1; // clk_b通道 sync_b_r0 sel_b ~sync_a_r1 ~sync_c_r1; // clk_c通道 sync_c_r0 sel_c ~sync_a_r1 ~sync_b_r1;下降沿触发器和输出与门照旧。这种结构的优点是切换任意两个通道时行为一致缺点是随着路数增多同步器输入端的与门扇入变大时序压力会增加。实际工程里三路以上时钟做无毛刺切换时我更倾向于用两个两路切换模块级联第一级先切A/B第二级再和C切虽然会多引入一点切换延迟但每级电路保持简单时序验证更容易收敛。4. 仿真验证如何用波形证明它真的无毛刺4.1 设计一个把切换逼到极限的Testbench代码写出来只对了一半仿真必须证明它无毛刺。我每次拿到这类模块第一件事就是写一个刁钻的testbench故意让sel在时钟高电平期间翻转看电路能不能扛住。基础的测试点包括异步复位释放后输出为0不出现意外上升沿sel在clk0高电平区间切到clk1观察输出最后几个脉冲是否完整sel在clk1低电平区间切回clk0观察切换过渡低电平是否稳定两个时钟频率相差很大的情况下切换验证慢时钟侧握手不会被快时钟的快速采样打乱连续快速来回切换验证互锁逻辑不会出现死锁。我常用的激励写法给一个参考片段initial begin rst_n 1b0; sel 1b0; #100 rst_n 1b1; // 等待几个时钟周期稳定后切换 repeat (5) (posedge clk0); // 故意在clk0高电平期间翻转sel (posedge clk0); #2; sel 1b1; // 观察切换完成 repeat (20) (posedge clk1); // 切回来 (posedge clk1); #1; sel 1b0; repeat (20) (posedge clk0); $finish; end这里#2、#1这种偏移是为了制造非时钟沿对齐的真实情况。如果sel翻转恰好总在下降沿后的一瞬间那说明你的激励太友善了没有真正考验电路。4.2 波形检查的四个关键点仿真跑完先别急着看漂亮波形要用下面四个点来卡结果。先看源时钟最后的高电平脉冲。切出之前源时钟输出的最后一个高电平脉宽必须等于源时钟正常的高电平脉宽不能有截断尾巴。再看切换过渡段。两个时钟之间必须有一段低电平这段低电平可能很短也可能很长取决于两个时钟的相位关系和同步器延迟但绝对不能出现两个时钟同时输出高电平或者跳变发生在高电平中段的情况。然后看目标时钟的第一个高电平脉冲。切入之后目标时钟输出的第一个高电平脉宽必须等于目标时钟正常的高电平脉宽不能有变窄或者变宽。最后从整个波形尺度看clkout的上升沿间隔必须合理。正常的上升沿间隔是各时钟自身的周期切换时的上升沿间隔会变成半个源时钟周期过渡低电平半个目标时钟周期也就是说切换产生的间隔只能比单个周期长不可能比正常半周期短。一旦出现明显小于正常半周期宽度的脉冲毛刺就来了。4.3 用断言自动抓毛刺而不是靠肉眼肉眼盯着波形看太容易漏尤其是后仿中毛刺宽度只有几百皮秒。我在实际仿真里会顺手写一个SVA断言专门检测输出时钟的最小脉宽。简单粗暴的做法是监控时钟沿的间隔property min_pulse_check; realtime last_edge_time; // 在每个边沿时记录时间检查与上一个边沿的间隔 // 间隔小于阈值则报错 endpropertySVA写起来最直接的版本是reg clkout_d; always (posedge clkout) clkout_d 1b1; always (negedge clkout) clkout_d 1b0;再用一个始终块检查clkout和clkout_d之间的组合延迟是否小于某个阈值但这个写法不够精确。更推荐的做法是在testbench里用系统时间戳函数监测所有上升沿和下降沿的时间间隔是否符合最小脉宽约束time last_rise_time; always (posedge clkout) begin if (last_rise_time ! 0) begin time interval $time - last_rise_time; if (interval 1ns) begin $display([ERROR] glitch detected, interval %0t, interval); $fatal; end end last_rise_time $time; end阈值1ns只是举例实际要根据两个时钟中最小的半周期来设置一般设为最小半周期的四分之一既能抓到毛刺又不会误报。这个断言我强烈建议加入回归测试里它可以帮你自动发现问题省掉人工看波形的力气。5. 工程落地的真实经验和踩坑记录5.1 时钟约束不能漏否则后仿全是warning功能仿真里看到无毛刺波形只能说成功了一半到了FPGA下板或者ASIC后端还有一个关键步骤约束。无毛刺时钟切换电路里clk0_gate和clk1_gate都是门控时钟工具需要知道这两个信号和主时钟的关系。我一般会对切换后的输出时钟做如下约束create_generated_clock -name clkout -source clk0 -divide_by 1 [get_pins u_mux/clkout]同时clk0和clk1跨域握手路径需要设置异步时钟约束。如果两个时钟确实完全异步直接约束成set_clock_groups -asynchronous。如果两个时钟同源只是不同分频比那就一定要告诉工具它们的公共源关系否则工具会按异步路径对待时序分析变得极其悲观后端收敛非常痛苦。这里有个常见坑有的同学在RTL里写了无毛刺切换就以为约束不重要了结果综合时序报告里出现大量clock gating check violation查了半天才发现忘记创建generated clock导致门控逻辑被当成数据路径分析时序根本过不了。5.2 复位信号的处理比想象中更重要这个电路里用了异步复位但复位信号本身也是跨时钟域的。如果rst_n不是同步释放的复位释放瞬间可能正好落在某个同步器的建立保持时间窗口内导致同步器输出不等于复位值那上电后的第一个状态就是未知的。我在实际项目中习惯加一个标准的复位同步释放模块把外部异步复位进行两级同步后再接入无毛刺切换模块。释放之后的rst_n与clk0和clk1都满足时序关系状态机初始化才可靠。另外注意这个模块的复位必须把内部所有触发器和同步器清零尤其是下降沿触发器否则上电时clk0_neg_en和clk1_neg_en是X态输出就会有一段未知时钟。5.3 时钟会停的情况必须特殊考虑无毛刺切换电路默认的前提是当前时钟和目标时钟都在正常翻转。但现实中有一种情况某个时钟源还没稳定比如PLL在锁定过程中输出可能是乱的甚至直接不翻转。如果此时把它作为目标时钟切入同步器一直等不到下降沿握手就卡住了输出只能停在低电平系统永久死等。解决思路是在切换控制里加入时钟稳定标志。PLL有lock信号晶振有clk_valid信号这些标志要参与选择逻辑比如assign sel_grant sel_request target_clk_valid ~source_clk_need_disable;更笨但更实用的一种兜底是加超时看门狗。切换请求发出后如果经过N个源时钟周期仍然没有检测到目标时钟的下降沿就上报错误并回滚到原时钟。这个逻辑在低成本芯片里很常见毕竟不能指望系统里每个时钟源都完美无缺。5.4 常见问题速查表我把实际调试中经常遇到的现象和对应的排查方向整理成了速查表做FAE和后端debug的时候对照着用很方便。现象可能原因排查方向仿真里输出出现小于半周期的窄脉冲sel和时钟沿竞争或互锁逻辑失效检查两个通道的第一级同步器是否被复位污染切换后输出一直为低时钟起不来目标通道的下降沿触发器没有置位查目标时钟是否真的在翻转查复位是否被意外拉低两个通道同时输出高电平互锁逻辑的反馈信号跨域采样出错检查同步器级数反馈信号必须经过两级同步后仿出现glitch但前仿正常组合逻辑门延迟和布线延迟导致检查门控时钟约束是否完整是否需要用ICG单元切换耗时过长两个时钟相位关系差握手等下降沿的时间长确认sel请求是否过早发出检查切换完成标志的时机来回切几次后状态卡死前一次切换未完成又开始新的切换控制逻辑必须等切换完成标志再发下一次请求5.5 面试和手撕代码的加分细节如果这是为了面试准备我建议在写代码前先画一遍电路结构图画清楚两个通道的同步器、下降沿触发器、与门和或门然后指着图讲一遍先关后开、低电平握手再动笔写代码。代码部分抓住几个评分点异步复位每个always块都要有两级同步器必须连续打拍互锁逻辑不要漏输出必须用组合逻辑assign完成。讲清楚为什么在下降沿锁存使能这一个点就能和大多数人拉开差距。我自己带过的新人里能把多路扩展和时钟停止场景考虑进来的基本当场就能过。如果时间充裕还可以主动提一句实际项目中会在切换完成后产生一个done标志避免控制状态机盲目重复切换这会让面试官觉得你确实做过实际项目而不只是在背代码。无毛刺时钟切换这个题目表面上考的是几十行RTL实际考察的是对时钟完整性、跨时钟域握手、复位同步和后端约束的综合理解。这几年面试数字IC岗位这道题的出场率一直很高因为它小巧但五脏俱全能把一个工程师的基本功看得清清楚楚。我个人的体会是代码背熟并不难难的是每一行触发器为什么存在、每个逻辑为什么这么接、切换的时序边界在哪里这些想明白了不管面试官从哪个角度追问都能接住项目里遇到实际切换问题也不会慌。
返回列表