ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

两个二分频时钟相位不确定?从RTL到STA的完整解析

两个二分频时钟相位不确定?从RTL到STA的完整解析 做数字IC或者FPGA开发的朋友几乎都碰过这样的需求“把主时钟分一下频给某个模块当工作时钟”。我之前在做一个双通道采集控制模块时就遇到一个很有意思的场景——同一个clki时钟源想要得到两个二分频时钟分别叫div2_clk1和div2_clk2。需求描述得很简单这两个分频时钟都要跟源时钟clki保持同步但div2_clk1和div2_clk2之间的相位关系却没法提前确定可能是相同的也可能是反相。乍一看这像是一句废话或者说像是需求文档随便写的半句话。但真把这个设计落到RTL、做完综合、跑完时序约束之后你会发现这句话背后藏着一大串东西二分频的实现结构、分频器初始状态对相位的影响、时钟树综合CTS前后的相位变化、两个同源生成时钟的STA约束甚至还有跨时钟域路径的处理方式。这篇文章就把这条链路从头到尾拆开讲清楚也把我在这类场景里踩过和见过的坑一并说掉。适合刚接触时钟分频设计的初学者也适合正被两个分频时钟相位问题折磨的工程师。1. 同步二分频背后的相位问题1.1 “跟源时钟同步”到底指什么很多同学看到“div2_clk1和div2_clk2都能跟源时钟clki同步”这句话第一反应是“这有什么好说的频率是clki的一半不就是同步吗”。实际上这里的“同步”不是指频率成整数倍而是指相位相干两个分频时钟的边沿都要对齐到clki的某个边沿上。我们可以打个比方。你把节拍器调到每分钟120拍用手每两拍拍一次桌子。只要手始终跟着节拍器的节拍走你拍桌子的频率就是60次每分钟而且每一次拍下去都精确落在节拍器的某一拍上。这就是“分频”和“同步”的关系div2_clk1和div2_clk2的频率都是clki的1/2它们每个上升沿都精确对齐在clki的某个上升沿上两个分频时钟之间也只是相差整数个clki周期或者半个clki周期的问题。这里要特别注意到二分频属于偶数分频输出占空比是50%所以它的上升沿和下降沿都对应源时钟的边沿。而“跟源时钟同步”这个描述其实只保证了每个分频时钟内部与clki的沿对齐关系并没有限制多个分频时钟之间的相位差。也就是说div2_clk1可以跟div2_clk2同相也可以反相这在逻辑上都仍然满足“各自与clki同步”。搞清楚这一点后面的问题就有了解答方向。1.2 同相还是反相谁说了算为什么两个结构完全一样的二分频器用同一个clki驱动相位却可能不一样根本原因在于分频器是有记忆的它的输出取决于当前状态而不只是输入时钟本身。我们看单个二分频时钟的本质一个寄存器每个clki上升沿翻转一次。如果这个寄存器当前是0那下一个沿过来它就变成1如果当前是1下一个沿过来就变成0。关键来了——两个分频器的当前状态可以是不同的。div2_clk1在奇数个clki边沿时翻成1在偶数个边沿时翻成0div2_clk2如果恰好状态相反它就在偶数个边沿时翻成1在奇数个边沿时翻成0。那么这两个输出看起来就是反相的一个的上升沿正好对着另一个的下降沿。那初始状态到底由什么决定在RTL仿真里寄存器如果没有被复位仿真器通常会把它当X态处理如果代码里写了复位逻辑那就是复位值决定。实际到芯片里上电后寄存器的初值受工艺、环境、复位释放时序等多方面影响是不确定的。FPGA相对好一些有全局复位网络和初始配置位但如果你没有统一复位依然可能出现某种“随机”的相位关系。ASIC就更明显了扫描链插入、DFT复位策略、上电时序都会改变分频器初值。所以“可能是同相也可能是反相”并不是需求文档写得含糊而是物理现实就是如此。2. 二分频的原理与相位关系推导2.1 一个D触发器如何变成一个二分频器二分频器最经典的实现就是把D触发器的Q反接到D端。假设寄存器是上升沿触发那么每个clki上升沿到来时输出Q都会翻转到当前Q取反后的值。如果初始Q是0那么clki第一个上升沿后Q变成1第二个上升沿后Q变成0如此反复Q端的频率正好是clki的一半占空比50%。用Verilog写一个最简单的二分频器一般是这样module div2 ( input wire clki, input wire rst_n, output reg div_clk ); always (posedge clki) begin if (!rst_n) div_clk 1b0; else div_clk ~div_clk; end endmodule这个写法里的复位是同步复位。实际工程中更推荐用异步复位、同步释放的结构主要是为了避免复位撤销时刻离时钟沿太近导致亚稳态。有些初学者会把复位做成异步复位always (posedge clki or negedge rst_n) begin if (!rst_n) div_clk 1b0; else div_clk ~div_clk; end这种写法本身没问题但前提是rst_n必须经过异步复位同步释放处理。否则复位撤销的时候如果刚好落在时钟上升沿附近分频器可能进入亚稳态输出波形杂乱。这个点在多个分频器同时复位时尤其致命因为多个寄存器同时面对同一个亚稳态窗口各自恢复的时间不一样相位关系就可能跑偏。2.2 两个二分频时钟的相位是如何“漂移”的回到两个二分频时钟的场景。div2_clk1和div2_clk2都从clki驱动逻辑结构也完全一样唯一可能不一样的就是初始状态。假设clki周期是10ns上升沿出现在0ns、10ns、20ns、30ns这些时刻。div2_clk1从0开始那么它在10ns翻成1、20ns翻成0、30ns翻成1输出的上升沿在10ns、30ns这些奇数编号沿。div2_clk2如果从1开始那么它在10ns翻成0、20ns翻成1、30ns翻成0输出的上升沿在20ns、40ns这些偶数编号沿。两个输出上升沿相差10ns也就是clki的一个周期而两个输出本身的周期是20ns所以它们之间就是180度反相。如果div2_clk2也从0开始那它的输出同样在10ns、30ns出现上升沿两个分频时钟完全同相。所以两个分频器初值相同相位就相同初值不同相位就反。这不是一个需要算法计算的问题而是一个由初始条件决定的问题。但有一个容易忽略的点所谓“反相”指的是理想情况下的180度相位差。真实场景里两个分频器到输出负载的路径延迟不可能完全一样布局布线后clki到达两个分频器的时刻也可能有偏差所以严格来说div2_clk1和div2_clk2之间可能是“略微偏离180度”的反相。这时候如果两个时钟之间还有数据交互路径时序收敛就会比较头疼后面我会在时序约束和排障部分细说。2.3 从二分频扩展到任意整数分频二分频只是分频家族里的最小单位。工程上做偶数分频比如四分频、六分频通常用计数器实现。计数器从0数到N-1输出在计数值满足条件时翻转得到的就是2N分频的50%占空比时钟。比如四分频就是计数到1时翻转一次计数到3时再翻转一次输出周期就是clki的4倍。奇数分频稍微麻烦一些因为只用上升沿计数输出占空比没法做到严格50%。常见的做法是上升沿和下降沿各做一个分频计数再把两路输出做或门。但这里我要提醒一句奇数分频的输出如果直接用组合逻辑或门合并可能会有毛刺而且这种“组合出来的时钟”直接驱动寄存器本身就是设计上的一个风险点。后面讲到时钟使能替代方案的时候你会看到更稳妥的做法。回到本篇文章的核心场景二分频虽然没有奇数分频那些破事但它暴露出的“多个分频时钟相位不确定”问题在更高分频比的多个输出时钟场景下一样存在。只要你用多个独立计数器去分别生成多个分频时钟相位关系能不能确定取决于它们有没有统一的计数基准和复位策略。3. RTL实现、仿真与布局布线中的相位变化3.1 一个能复现两种相位关系的RTL例子为了把这个场景可视化我写了一个简单的双二分频模块。注意第二个分频器的复位值我特意设成了1这样div2_clk2跟div2_clk1天然就是反相的。如果你把两个复位值都改成0得到的两个时钟就是同相的。这就是我在前面反复强调的初值不同相位就不同。module two_div2_clks ( input wire clki, input wire rst_n, output wire div2_clk1, output wire div2_clk2 ); reg r_div1; reg r_div2; always (posedge clki) begin if (!rst_n) r_div1 1b0; else r_div1 ~r_div1; end always (posedge clki) begin if (!rst_n) r_div2 1b1; else r_div2 ~r_div2; end assign div2_clk1 r_div1; assign div2_clk2 r_div2; endmodule这个写法把寄存器输出直接当时钟往外送。RTL综合时工具大概率会把它综合成两个带复位的T触发器。有些综合工具会警告“register output used as clock”这是正常的但也说明设计里已经出现了分频时钟网络。这种结构在低速场景下能用但如果你希望两个分频时钟的相位关系是确定的纯靠这段代码是不够的因为复位值只是其中一环真实上电后的初值仍然可能受到复位释放时序和布局布线的影响。测试平台也很简单module tb_two_div2; reg clki; reg rst_n; wire div2_clk1; wire div2_clk2; two_div2_clks u_dut ( .clki (clki), .rst_n (rst_n), .div2_clk1 (div2_clk1), .div2_clk2 (div2_clk2) ); initial clki 0; always #5 clki ~clki; initial begin rst_n 0; #20 rst_n 1; #200 $finish; end initial begin $monitor($time, clki%b div2_clk1%b div2_clk2%b, clki, div2_clk1, div2_clk2); end endmodule跑完仿真你会看到div2_clk1和div2_clk2的上升沿刚好相差一个clki周期。由于div2_clk2的初值是1它在第一个clki上升沿就被拉低而div2_clk1在第一个clki上升沿被拉高。一个上升沿、一个下降沿两个输出自然就是反相。3.2 仿真全绿不代表上电稳妥这里我想多说一句很多工程师在仿真时都习惯把所有寄存器统一复位成0结果仿真波形漂亮得不行两个分频时钟同相所有模块都工作正常。但到了FPGA上板或者芯片回来之后偶尔就出现某个分频寄存器初值不一样导致两个时钟反相然后模块间通信偶发失败。我见过一个真实案例系统里有A、B两个模块分别用两个分频时钟驱动。RTL仿真时两个分频器都复位成0模块间握手一直正常。上板后其中一个分频器因为复位释放时序稍有偏差跟另一个分频器形成了反相关系结果两个模块之间的握手信号总是差半拍系统跑几分钟就出现一次数据错误。这种问题最难排查因为不是每次上电都必现环境温度一变、供电电压稍微波动错误概率就变了。所以我的建议是如果你确实要知道两个分频时钟的相位关系就要在仿真阶段故意制造“坏情况”。比如第二个分频器的复位值设成1或者随机化初值然后看看设计逻辑是否依然正确。如果设计依赖“两个分频时钟必须同相”才能工作那你一定要在RTL里面明确复位策略把两个分频器都纳入同一个复位域并且用异步复位同步释放模块统一产生复位信号。3.3 CTS之后相位关系为什么“变味了”RTL仿真里我们看到的都是理想时钟沿。到了布局布线阶段时钟树综合CTS会带来一个现实问题clki从时钟源到每个分频器寄存器的时钟端延迟是不一样的。CTS的目的就是尽量让这些延迟平衡但绝对平衡是不可能的于是就有了时钟偏斜clock skew。对于两个分频器来说如果clki到达div2_clk1所在寄存器的时间比到达div2_clk2所在寄存器的时间早2ns那么即便它们在RTL层面是严格反相实际输出的边沿也不会正好错开一个clki周期的半周期。更麻烦的是这两个分频时钟分头驱动各自的逻辑模块模块内部还有各自的时钟偏斜最终到达目的寄存器的相位关系跟你在仿真里看到的很可能不一样。因此如果你真的打算让两个分频时钟分别驱动两个模块又把它们当成“同频同相”的时钟来做时序收敛CTS之后大概率会碰到时序违例。反过来如果两个时钟之间没有数据交互只是各自驱动独立模块那skew的影响就小很多。关键还是要想清楚两个分频时钟之间到底有没有数据路径如果有那就要么严格约束相位关系要么明确走跨时钟域处理别让工具去猜。4. 时序约束与更优的工程方案4.1 让STA工具认识两个生成的时钟既然div2_clk1和div2_clk2都是clki的分频时钟那在SDC约束文件里就要用create_generated_clock把它们声明出来让STA工具知道它们是源自clki的生成时钟。典型写法如下create_clock -name clki -period 10 [get_ports clki] create_generated_clock -name div2_clk1 \ -source [get_ports clki] \ -divide_by 2 \ [get_pins u_div1/r_div1_reg/Q] create_generated_clock -name div2_clk2 \ -source [get_ports clki] \ -divide_by 2 \ [get_pins u_div2/r_div2_reg/Q]这里有个细节create_generated_clock的-source要指向源时钟clki而分频锁存节点要指向分频寄存器的输出Q。工具会根据这个寄存器的逻辑自动推导出生成时钟的波形。如果工具推导出来的相位跟你实际设计不符可能就需要手动指定边沿关系。比如你已知div2_clk1和div2_clk2是反相可以显式指定waveformcreate_generated_clock -name div2_clk1 \ -source [get_ports clki] \ -edges {1 3 5} \ [get_pins u_div1/r_div1_reg/Q] create_generated_clock -name div2_clk2 \ -source [get_ports clki] \ -edges {2 4 6} \ [get_pins u_div2/r_div2_reg/Q]在10ns周期的clki下edges {1 3 5}表示输出上升沿在0ns、20ns、40ns下降沿在10ns、30nsedges {2 4 6}表示输出上升沿在10ns、30ns下降沿在0ns、20ns、40ns。两条波形错开了半个周期正好表达反相关系。不过要说明一点这个写法在Synopsys和Cadence工具里的支持程度略有差异。有些工具即使不指定edges也能从分频器的逻辑自动推断相位关系有些工具则需要你手动帮助。建议写约束之后先打开生成的clock report仔细确认一下看两边的waveform是不是你预期的关系。4.2 两个分频时钟之间的跨时钟路径怎么处理如果div2_clk1和div2_clk2只各自驱动完全独立的模块模块之间没有任何数据交互那约束起来很简单可以设置set_clock_groups让工具不去分析它们之间的时序路径。set_clock_groups -asynchronous \ -group {div2_clk1} \ -group {div2_clk2}但如果两个模块之间存在数据交互比如模块A用div2_clk1产生一个数据模块B用div2_clk2采样这个数据那就不能盲目设clock group了。因为两个时钟同源频率相同相位关系可能是确定的也可能是反相。如果相位确定工具可以正常分析setup和hold如果相位不确定工具分析出来的结果就变得非常悲观或者乐观两种情况都不安全。我的工程经验是只要两个分频时钟之间存在跨模块的数据交互就不要把“相位是否反相”这个赌注押在STA分析上。更可靠的做法是把数据路径做成真正的跨时钟域路径加两级同步器或者用异步FIFO。虽然两个时钟明明同源做成异步FIFO听起来有点浪费但至少系统行为和时序收敛是可控的。否则一旦上电时初值改变了相位关系你的setup/hold分析结论可能就全废了。这里也需要提醒一下“同源时钟”不代表“同步安全”。同步不安全的核心条件是相位关系可预期且满足时序要求。如果相位关系因为初值不确定而随机变化那不管频率是否成整数倍都应该按跨时钟域处理。不要被“频率一致”迷惑。4.3 更好的工程方案用时钟使能替代分频时钟聊到这里肯定有朋友会问既然分频时钟的相位关系这么麻烦那我不做分频时钟了行不行答案是可以而且很多时候这才是最优解。分频时钟驱动模块真正的目的是让模块里寄存器的翻转频率降下来。但问题在于分频时钟本质上还是一个来自寄存器的信号在时钟树里它会引入很大的延迟和偏斜也会增加时钟网络的功耗。更麻烦的是你还要额外处理生成时钟的约束和可能的相位不确定性。一种替代思路是所有寄存器仍然用clki作为时钟再用一个使能信号控制它们只在特定周期才动作。比如二分频场景可以这样写module clk_en_div2 ( input wire clki, input wire rst_n, output wire clk_en ); reg r_cnt; always (posedge clki) begin if (!rst_n) r_cnt 1b0; else r_cnt ~r_cnt; end assign clk_en r_cnt; endmodule然后下游模块的寄存器都这样写always (posedge clki) begin if (!rst_n) begin data_q 1b0; end else if (clk_en) begin data_q data_next; end end这样一来data_q每两个clki周期才采样一次新数据行为上跟二分频时钟驱动寄存器基本一致但所有寄存器用的都是同一个时钟clkiCTS只需要平衡一棵时钟树也没有两个分频时钟的相位不确定性问题。这其实就是数字设计里非常常用的“时钟使能”模式。我个人的习惯是低速控制逻辑、外设接口时钟、有一定数据交互的模块优先用时钟使能只有对外提供真实时钟信号的场景比如给外部ADC、DAC提供采样时钟或者FPGA上需要输出一个分频时钟引到引脚才真正用分频时钟。这个取舍能让后面的综合、时序收敛、调试都轻松不少。5. 现场排障相位、同步与亚稳态的几个典型场景5.1 仿真全绿上板偶发故障这个现象在前面已经提到过。如果你遇到“RTL仿真、后仿真都正常但上板后系统偶尔工作异常”并且代码里恰好有多个分频时钟我建议你第一件事就是检查所有分频寄存器的初值。具体排查思路分几步走。先把RTL里所有寄存器复位值列出来看看有没有哪个分频器复位值是1、哪个是0确认复位策略是不是统一了。再用示波器或者逻辑分析仪分别抓div2_clk1和div2_clk2看它们上电后的相位关系是否符合预期。如果你是FPGA还可以通过在线逻辑分析仪抓芯片内部的寄存器输出确认复位释放后第一个沿的行为。如果是ASIC那就重点查复位树的时序看两个分频器是否真正在同一拍完成复位。很多情况下问题不在分频器本身而在复位释放。两个分频器距离较远复位信号到达时间不同一个分频器先退出复位多跑了半个周期另一个后退出复位相位关系就比设计预期差了一拍。解决方法是把两个分频器都放到同一个复位同步模块之后并且确保复位树收敛。最简单的办法就是前面写的异步复位同步释放电路统一产生内部复位信号再送给所有分频器。5.2 两个分频时钟“看着不对”怎么办有时候你并没有上板只是用仿真工具看波形发现div2_clk1和div2_clk2不像预期那样同相这时候别急着改代码。先确认你的复位值、复位时序、仿真初始值到底是怎么设置的。有一次我在写一个双通道采集模块时遇到的现象很奇怪两个分频器复位值都是0但仿真出来的相位就是反相的。最后发现原因是testbench里时钟从0时刻开始就跑而复位信号在一个时钟周期之后才拉高。在复位拉高前的那个周期分频器已经受到了clki的上升沿触发状态已经翻转过一次。等复位真正释放的时候两个分频器虽然复位值是0但它们经历的时钟沿次数不一样导致相位错开。这个问题的本质是复位期间分频寄存器有没有被clki触发取决于复位是同步复位还是异步复位。同步复位只有在clki上升沿才生效如果复位拉低前已经来过一个时钟沿寄存器可能已经翻转。异步复位则不受时钟影响能更快把寄存器拉到确定状态。所以如果你用的是同步复位一定注意复位拉高的时刻跟clki沿的关系不要在关键场景里让分频器在复位期间“乱翻”。5.3 分频时钟驱动寄存器带来的亚稳态最后一个典型问题是分频时钟作为“真实时钟”去驱动下游寄存器时可能出现亚稳态。我遇到过一次模块A在div2_clk1的控制下产生一个ready信号模块B用div2_clk2采样这个ready信号。两个分频时钟同源频率也一致但相位刚好反相那么ready信号的上升沿可能正好出现在div2_clk2的采样沿附近也就是说正好处于亚稳态窗口里。结果模块B采到的ready有时候是0有时候是1甚至有时候是不定态。这种问题的标准解法是在模块B入口加两级同步器把ready信号同步到div2_clk2时钟域。很多工程师会觉得两个时钟都是同一个clki分出来的频率相同、相位固定何必加同步器但问题是相位“固定”的前提是初值确定和时钟树偏差可控而实际工程中这些条件都可能被打破。既然你在标题里都明确说了“可能是同相也可能是反相”那按最坏情况处理就是最稳妥的方案。还有一点如果两个分频时钟之间确实没有数据交互那这个问题的风险等级就低很多。判断标准很简单时钟域A的寄存器输出有没有直接被时钟域B的寄存器采样有就按跨时钟域处理没有就随便。别把所有分频时钟都当成“绝对安全”的同步时钟。回到最开始那个需求同一路clki分出div2_clk1和div2_clk2两个都能跟源时钟同步但彼此相位不确定。这个描述本身没有错它准确反映了一个分频器网络在无额外约束下的行为。真正需要做的是让这个不确定性变得可控要么在复位策略和时序约束里把相位关系定死要么在逻辑设计上彻底绕开分频时钟改成时钟使能方案。我在实际项目里最常用的就是后者。把数据路径全部放在clki这一棵时钟树上用使能信号制造“等效二分频”的效果这样既省掉了两个分频时钟的约束烦恼也避免了分频时钟相位不确定带来的跨时钟域坑。如果确实需要真实的分频时钟输出那就老老实实做异步复位同步释放、统一复位所有分频器并且在SDC里把两个生成时钟的边沿关系约束清楚。这套组合下来大部分跟分频时钟相位相关的偶发问题都能提前消掉。
返回列表