首页 / 资讯中心 / 文章详情

FPGA无毛刺时钟切换原理与实现:从BUFGCTRL原语到状态机方案

FPGA无毛刺时钟切换原理与实现:从BUFGCTRL原语到状态机方案 ★ FEATURED ARTICLE
做FPGA的人早晚都会遇到时钟切换。这个看似用二选一MUX就能解决的问题一旦上了真板子轻则波形难看重则把整条数据链路搞崩。我入行第一年就因为一次“随手写”的时钟切换被折腾到怀疑人生示波器上那一下尖刺让整个采集系统误触发从此对“无毛刺时钟切换”这几个字有了很深的敬畏。简单来说无毛刺时钟切换要解决的核心问题就是在电路运行过程中切换时钟源时输出时钟不能出现一个比正常时钟周期更短的脉冲、不能出现占空比异常、更不能让下游寄存器采到不确定的边沿。它最常出现在图像处理多分辨率切换、高速接口动态速率调整、多时钟测量系统等场景几乎所有与“运行时换频”有关的FPGA设计都会踩到这个坑。1. 时钟切换为什么这么容易翻车1.1 毛刺到底是怎么冒出来的很多人第一次做时钟切换第一反应就是写一个二选一MUXassign clk_out clk_sel ? clk_b : clk_a;仿真波形看着干干净净上板就露馅。原因在于clk_sel这个选择信号对于clk_a和clk_b来说到达两个门控逻辑的时刻不可能完全一致。clk_a通路上的AND门先关或者后关clk_b通路上的AND门先开或者后开中间的OR门就可能输出一个只有几百皮秒宽、电平还不确定的尖峰。打个比方你家里有两个水箱一个热水一个冷水手动阀门从热水端拧到冷水端。如果热水阀还没关死、冷水阀已经打开两股水会在管子里撞出乱流如果阀门中间停太久又会断流。数字电路里的“乱流”就是毛刺。组合逻辑MUX本质上就是让两个时钟在OR门处发生过路车抢道谁快谁慢完全看布线延迟不产生毛刺反而是运气好。更麻烦的是毛刺看不见摸不着仿真里通常也复现不出来因为它依赖布局布线后的具体延迟。等它真的在板子上出现时下游触发器可能被这个多余边沿打了一拍状态机可能无故跳转计数器可能多计了一个数整个系统就乱了。1.2 “无毛刺”到底意味着什么要做无毛刺切换得先把“无毛刺”拆成三个层次。第一层输出波形中不能出现比正常时钟高电平或低电平更窄的脉冲。这是最基础的要求一个组合MUX通常连这层都过不了。第二层切换后的输出高电平宽度不能异常展宽或缩短。很多自研切换逻辑能消灭窄脉冲但两个时钟相位不同步时可能出现旧时钟的最后一拍高电平和新时钟的第一拍高电平“首尾相连”OR门输出一个接近两倍宽度的脉冲。这个脉冲虽然不短但会让下游电路分不清沿的位置对相位敏感的设计同样是灾害。第三层整个切换过程的逻辑状态必须确定不能有亚稳态传播。选择信号是跨时钟域的如果直接打一拍就送进切换逻辑非法时间内采样的结果可能是个不定值导致“该关的门没关、该开的门没开”。实际工程中真正满足全部三个层次的方案只有专用原语或者经过严格时序设计的有限状态机。1.3 为什么光加同步器救不了场跨时钟域设计的基本常识是加两级同步器这在数据通路里很有效。但时钟切换这个问题难就难在它不是一个单纯的数据跨域问题而是两个时钟门控信号之间的“先后顺序”问题。假设你把切换信号分别同步到clk_a域和clk_b域每个域各自得到一个稳定的选择电平。问题是这两个同步器输出之间的相对时序完全随机可能clk_b域先看到切换请求先打开了新时钟通道而clk_a域还没关闭旧时钟通道。这时候OR门输出就是两个时钟的混叠波形而不是干净的单一时钟。同步器解决的是“这个信号是否稳定”解决不了“两个信号谁先谁后”。要想让输出不出现毛刺必须保证切换动作只发生在时钟低电平期间而且严格做到“先关旧时钟、再开新时钟”。这就要靠专门的切换控制逻辑而不是简单加两块寄存器。2. 无毛刺切换的几种主流实现方案2.1 厂商原语方案BUFGCTRL和BUFGMUXXilinx在7系列以及之后的平台里最推荐的做法是直接例化BUFGCTRL原语。这个原语内部专门做了无毛刺切换的时序控制所有逻辑资源、布局布线都已经由芯片厂商替你处理好了。BUFGCTRL的关键端口是I0、I1两个时钟输入S0、S1两个选择输入CE0、CE1两个时钟使能以及IGNORE和输出O。它内部有一组上升沿检测器当检测到S0上升沿并且CE0有效它不会立刻切到I0而是等当前输出的时钟进入低电平之后再在I0的下一个上升沿把输出真正切过去。这个“等低电平再开新沿”的动作就是整个无毛刺切换的核心。实际用起来很简单。假设clk_a是当前时钟要切到clk_b可以这样例化BUFGCTRL #( .INIT_OUT(0), .PRESELECT_I0(TRUE), .PRESELECT_I1(FALSE) ) u_bufgctrl ( .I0 (clk_a), .I1 (clk_b), .S0 (1b0), .S1 (switch_req), .CE0 (1b1), .CE1 (1b1), .IGNORE (1b0), .O (clk_out) );切换时把switch_req拉高即可。注意这里S0和S1不能同时拉高必须保证只有一个选择信号处于有效状态。PRESELECT_I0设为TRUE表示上电后默认输出I0这样整个系统启动阶段就有一个确定的时钟不会在复位释放瞬间出现自由振荡。还有一点很多人容易忽略BUFGCTRL的S输入是有最小脉冲宽度要求的。你给S1一个单周期脉冲它可能根本来不及完成内部的“等待低电平、再开新沿”这套动作导致切换失败或者干脆不切。所以实际项目中我通常会把切换请求展宽到至少几十个目标时钟周期确保原语有充足时间响应。2.2 自研逻辑方案下降沿控制状态机如果不用厂商原语比如你的目标平台是国产FPGA、或者想在跨平台代码库里保持统一风格那就得自己写切换逻辑。常见的思路是“下降沿采样、先关后开”。核心代码可以写成这样module glitch_free_mux ( input wire clk_a, input wire clk_b, input wire clk_sel, // 1: 选择clk_b, 0: 选择clk_a input wire rst_n, output wire clk_out ); reg sel_a, sel_b; always (negedge clk_a or negedge rst_n) begin if (!rst_n) sel_a 1b1; else if (clk_sel) sel_a 1b0; else sel_a 1b1; end always (negedge clk_b or negedge rst_n) begin if (!rst_n) sel_b 1b0; else if (clk_sel) sel_b 1b1; else sel_b 1b0; end assign clk_out (clk_a sel_a) | (clk_b sel_b); endmodule这段代码的思路是在clk_a的下降沿关闭旧通道在clk_b的下降沿打开新通道门控信号只在时钟低电平期间变化输出就不会被切出半个脉冲。但必须说清楚这段代码只能算“教学级”实现不是万无一失。如果clk_a和clk_b之间完全没有相位关系有可能出现clk_b的下降沿先到、sel_b先拉高而sel_a还没拉低输出OR门就把两个时钟的高电平混在一起产生一个展宽脉冲。要让它更可靠还需要把clk_sel先同步到两个时钟域并且用状态机保证sel_a和sel_b严格互斥。即便如此自研逻辑的无毛刺等级也仍然低于专用原语。2.3 同源时钟的动态变频方案还有一种场景不是异步时钟切换而是同一个PLL/MMCM动态改配置。比如图像系统需要从1080P切到4K最直接的办法是动态重配MMCM的分频系数让VCO输出新的像素时钟。动态重配本身不产生MUX竞争毛刺但它有一个更隐蔽的问题重配期间PLL的输出会短暂不稳定甚至直接丢失若干个时钟周期。这个窗口对下游电路来说跟毛刺一样致命。所以动态变频不能单独用必须配合业务逻辑的复位管理和工作窗口规划。我通常的做法是在帧消隐期、串口空闲期、或者DMA停止的窗口里发起重配PLL锁定完成后先复位消费时钟域的逻辑再解除复位继续工作。严格来说动态重配解决的是“同源多频切换”和BUFGCTRL解决的“异步双时钟无毛刺切换”不是一个问题。很多设计把两者混在一起结果两边都没处理好。2.4 方案对比速览方案无毛刺等级支持异步时钟资源与厂商依赖推荐场景组合逻辑MUX不满足否零资源不推荐BUFGCTRL原语完整满足三层支持1个BUFGCTRLXilinx专用异步时钟切换首选下降沿状态机基本满足第一层有宽脉冲风险需配合同步器FF和门电路通用教学验证、兜底MMCM/PLL动态重配重配期间输出不稳定不支持需重配控制器同源多频切换3. 实操中的关键细节3.1 时序约束必须第一时间跟上很多人写完切换逻辑就忘了约束结果综合器不知道两个时钟是异步的会很“好心”地把选择信号和门控逻辑按同步路径去做时序收敛。这会导致两个结果要么工具疯狂报时序违例要么工具为了满足一条错误约束把切换控制逻辑的布局布线改得乱七八糟。正确做法是先把两个时钟域声明为异步关系。Vivado里的典型写法是set_clock_groups -asynchronous -group {clk_a_clk} -group {clk_b_clk}同时所有跨域同步寄存器和BUFGCTRL的S端口路径需要加异步约束。对于FF同步链建议在RTL里给同步寄存器加上ASYNC_REG属性告诉工具这些寄存器是专门做跨时钟域同步的不需要在它们之间做复杂的时序收敛。S端口到原语的路径我会额外加一条set_max_delay约束限制选择信号线路上的布线延迟防止因线路绕远把S信号拖到时钟沿附近变化。3.2 切换请求不能直接发单周期脉冲跨时钟域的通用原则是“电平传递优于脉冲传递”。如果切换请求在源时钟域是一个单周期脉冲而目标时钟比源时钟慢很多这个小脉冲大概率会被目标时钟漏采。因此切换请求应该设计成电平信号请求拉高表示“我要切换到B时钟”完成切换后再拉低表示“切换请求结束”。同时BUFGCTRL的S输入有最小脉宽要求这也是为什么我前面强调请求要展宽。实际操作里我不会用软件直接写寄存器控制切换而是在硬件里做一个“请求展宽器”把CPU发来的短脉冲扩展成至少16个周期的高电平再送给原语。这样无论原语内部时序怎么走都有足够余量。3.3 RTL编码规范一切选择信号必须寄存器化选择信号不能来自组合逻辑。如果sel信号本身是组合逻辑输出它在原语S端口上的变化时刻无法预料原语内部就算等半天也很难等到一个干净的上升沿。所有控制切换的信号都必须在源时钟域先打一拍再进入同步链。另外一个容易被忽略的点是复位释放。如果切换控制逻辑使用异步复位复位释放时刻恰好落在时钟高电平中间控制信号可能在一个不确定的点被释放导致门控逻辑输出半个脉冲。我的习惯是给切换控制逻辑单独做一套异步复位、同步释放的复位模块保证复位释放沿与时钟上升沿对齐。3.4 仿真不能只跑一组相位写testbench时很多人设置好两个固定频率的时钟就开始测测了一次没问题就收工。这远远不够因为异步时钟切换的时候两路时钟边沿的相对位置是随机的。我做仿真时会让两个时钟的相位做随机偏移或者用变量控制相位差然后在随机时刻拉高切换请求连跑几千次统计输出波形里有没有短于正常周期的脉冲。仿真通过也还是不能掉以轻心。我碰到过仿真完全干净、上板后偶发异常的情况最后定位到是切换请求信号在PCB上有串扰导致S端口收到一个毛刺把原语内部检测器触发了。所以严谨的做法还要在切换完成后用ILA实时观察内部时钟的计数结果确认下游逻辑没有发生额外翻转。4. 典型应用场景里的“换挡”门道4.1 图像处理多分辨率像素时钟切换1080P到4K的切换不仅仅是改个分辨率参数。像素时钟从148.5MHz跳到594MHz整个图像处理链路的时序、延迟、行场同步计数器全都要跟着变。如果只做无毛刺时钟切换不处理下游逻辑即便时钟本身没有毛刺图像也大概率出现花屏或者水平错位。我的做法是分两步走。第一步在帧消隐期间发起时钟切换利用BUFGCTRL保证切换沿干净第二步切换完成后立即复位整条图像处理链路等MMCM锁定稳定、行场计数器重新同步之后再开始输出。不要试图“无缝切换并继续保持原来的计数器状态”那是在跟自己过不去。4.2 频率测量与串口计数器和状态机最怕多余边沿做频率测量或者TDC测时的时候计数器对时钟边沿极其敏感。一个额外沿可能就是几十个皮秒的误差。如果测量系统在运行中切换参考时钟比如为了在不同频段获得不同测频分辨率而切换闸门时钟毛刺会直接污染计数结果。串口也一样。如果系统支持动态切换波特率最好的方案不是砖头一样改波特率分频器而是预先用PLL生成两路目标波特率时钟再用无毛切步骤在串口空闲窗口切换。这样数据线上的每一位时序都是规整的不会出现起止位宽度异常的问题。4.3 高速接口LVDS和MIPI的时钟域切换LVDS接收、MIPI D-PHY这类高速接口恢复出来的字节时钟往往与源端像素时钟是异步关系。当你要切换通道速率、或者在不同视频源之间切换时本质上就是在恢复时钟和系统时钟之间做切换。这时候无毛刺切换只是前提真正要命的是切换后SerDes的初始化状态可能已经乱了。我的经验是高速接口的时钟切换必须由链路层协议配合先在协议帧的边界发起切换请求切换完成后再重新初始化接收端的deskew、以及FIFO指针。否则时钟干净了数据还是一团浆糊。5. 常见问题速查与避坑经验表现根本原因解决方案切换瞬间输出出现窄尖峰组合MUX竞争冒险改用BUFGCTRL原语切换请求偶发无效S信号脉宽不足展宽请求保持几十个周期切换后下游状态机乱跳选择信号未同步加两级同步器和ASYNC_REG输出出现展宽脉冲两路门控信号重叠原语方案或状态机严格互斥工具报一堆时序违例缺CDC约束set_clock_groups异步分组仿真正常上板异常S端口毛刺/布线偏斜加mux_delay约束、ILA实测最后说一个我自己的团队规范所有可切换时钟永远收口到一个统一模块里管理切换请求由CPU通过寄存器下发硬件把请求展宽后同时送给BUFGCTRL和状态机兜底。每次切换完成后统计模块会记录输出时钟边沿数如果发现切换前后边沿数偏差超过阈值直接拉高中断报警。这套机制下来这几年基本没有再被时钟切换的毛刺坑过。所以我的建议很简单能用原语就别自己造轮子原语背后是芯片厂商烧了很多钱验证过的行为比自己用Verilog拼出来的状态机靠谱得多。如果你实在要用自研逻辑也一定记得把“先关旧钟、等低电平、再开新钟”这句口诀刻在代码注释的第一行。
阅读完成 · 觉得有帮助?
咨询建站