☰
二分频时钟相位关系探析:从同步到反相的物理机制与RTL约束实践
2026/10/2 1:31:43 网站建设 项目流程

一个看似简单但很容易栽跟头的需求:两个二分频时钟的相位关系

以前接到过一个需求,需求方原话是这样的:用同一个时钟源clki,做两个二分频时钟,得到div2_clk1和div2_clk2,这两个时钟都能跟源时钟clki同步,但是div2_clk1和div2_clk2的相位可能是相同的,也可能是反相。第一眼看到这句话,我以为是需求文档写得含糊,后来仔细琢磨,发现这个描述其实非常精准——它正好戳中了无数数字逻辑设计者对分频器理解的一个盲区:很多人默认“同步”就等于“同相”,默认“同一个源时钟分频出来的时钟”就一定是严格对齐的。

这篇文章想把这个问题彻底讲清楚。我会从现象出发,拆解两个二分频时钟为什么可能同相也可能反相,背后的机制是什么,工程上怎么去实测验证,以及作为设计者怎么通过RTL写法和时序约束把相位关系变得可控。无论你是刚接触FPGA的在校生,还是已经在写RTL的工程师,只要你需要处理分频时钟、时钟树、复位释放、跨时钟域相关的设计,这篇文章都值得花十分钟看完。

1. 先理解标题描述的现象:两个分频时钟为什么会“同相或反相”

1.1 最朴素的两个D触发器二分频模型

要理解标题里的现象,最直接的办法是把电路结构画出来。假设我们有两个D触发器,它们的时钟端都接到clki上,两个触发器的D端都接自己的反相输出端,也就是标准的二分频接法。

// 两个独立的二分频触发器 reg div2_r1; reg div2_r2; always @(posedge clki or negedge rst_n) begin if (!rst_n) div2_r1 <= 1'b0; else div2_r1 <= ~div2_r1; end always @(posedge clki or negedge rst_n) begin if (!rst_n) div2_r2 <= 1'b0; else div2_r2 <= ~div2_r2; end assign div2_clk1 = div2_r1; assign div2_clk2 = div2_r2;

这段代码看起来没有任何问题,两个触发器都由同一个clki驱动,复位也都是同一个rst_n。如果最终div2_clk1和div2_clk2都取寄存器的Q端输出,那么这两个时钟天然就是同相的——你以为故事到这里就结束了?不。

真正的坑在于:div2_clk2不一定非得从Q端引出。如果你在设计时用了另一种等价写法,比如从触发器的QN端引出,或者两个分频器分散在不同的模块里、由不同的人维护,有人多写了一级取反逻辑,又或者综合工具优化时把某个寄存器的输出极性做了调整,那么div2_clk1和div2_clk2之间就可能变成反相关系。标题里说的“可能是相同的,也可能是反相”,最直接的解释就是:极性选择不同,相位就不同。

1.2 “同步”到底指什么

这里必须先把“同步”两个字掰扯清楚。标题说两个时钟都能跟源时钟clki同步,这句话的意思是:两个分频时钟的有效沿(上升沿)都精确对应到clki的某一个时钟沿上,不存在相对于clki任意漂移的相位关系。换句话说,div2_clk1的上升沿必然出现在clki的某个上升沿之后的固定延迟时间点,div2_clk2也一样。

但“都和clki同步”并不等于“div2_clk1和div2_clk2之间一定同相”。因为div2_clk1可能对应clki的第1、3、5个上升沿翻转,而div2_clk2可能对应clki的第2、4、6个上升沿翻转。这样看波形时,两个分频时钟都是clki的二分频,但彼此之间差了整整一个clki周期——这就是我们常说的反相。

下面这个真值表能直观说明不同输出选择对相位关系的影响:

分频器1使用输出端分频器2使用输出端clki上升沿后的状态对比两者相位关系
QQ同时翻转或同时保持同相
QQNQ与QN始终相反反相
QNQQN与Q始终相反反相
QNQN同时翻转或同时保持同相

这个表说明,光看“是不是同一个clki分频出来的”根本不够,还要看每个时钟实际取自哪个输出端,或者说,要看分频器初始状态被定义成了什么。

1.3 为什么相位只会是“同相或反相”这两种离散值

可能有人会追问:两个分频时钟的相位差能不能是90度或者其他任意角度?答案是:在直接用同一个clki驱动D触发器做二分频的场景下,不可能。

原因在于D触发器的翻转动作只发生在clki的有效沿瞬间。每个分频器的输出只有两个稳定电平,0和1,它的状态转移也只能发生在clki的沿上。所以每个分频时钟的上升沿,要么落在clki的第奇数个有效沿,要么落在clki的第偶数个有效沿。对两个分频器来说,它们各自的沿落在奇数还是偶数,就决定了它们之间是0度相位差还是180度相位差。这是一个离散的选择,不存在中间态。

这也是标题这句话在设计上非常重要的一个含义:你不需要在两个分频器之间做任何相位调整电路,因为从结构上就已经决定了相位只能取两个值之一。问题在于——到底是哪一个值,很多时候不由你控制。

2. 相位不确定性的三个根源:采样沿、复位时刻与时钟偏斜

2.1 根源一:触发器的初始状态和复位释放时刻

如果说第1章讲的“极性选择”是设计者主动造成的相位差异,那么这一小节要说的就是被动造成的、也是最容易翻车的场景:两个分频器初始状态不一致。

还是回到那两段Verilog代码。很多人以为复位信号rst_n只要接到两个触发器的复位端,它们复位后的状态就一定一样。但如果复位信号是异步复位,释放时刻又没做同步处理,那么两个触发器从复位态回到工作态的时机可能相差那么一点点。这个“一点点”在时序上可能刚好导致一个触发器先看到一个clki上升沿并翻转,而另一个要等到下一个clki上升沿才翻转。于是两个分频器进入不同的初始相位组合,表现出来就是同相和反相两种情况都有可能出现。

如果把两个分频器的复位逻辑写成下面这样,问题会更隐蔽:

// 模块A内部的分频器 always @(posedge clki or negedge rst_n_a) begin if (!rst_n_a) div2_clk1 <= 1'b0; else div2_clk1 <= ~div2_clk1; end // 模块B内部的分频器 always @(posedge clki or negedge rst_n_b) begin if (!rst_n_b) div2_clk2 <= 1'b0; else div2_clk2 <= ~div2_clk2; end

只要rst_n_a和rst_n_b不是完全同一个信号,或者它们在板级到达FPGA引脚后经过了不同的路径延迟,那么两个分频器复位释放后的第一个采样沿就可能不是同一条。即便它们复位后都从0开始,也可能因为后续触发器的采样沿不同步而错开。

2.2 根源二:时钟偏斜与布局布线

第二个容易被忽略的因素是时钟偏斜。在理想模型里,clki到达触发器1和触发器2的延迟是零。但真实FPGA内部,clki要从全局时钟缓冲器经过时钟树网络分发给每个触发器,到达时间有差异。这个差异就是clock skew。

对于低频的二分频时钟,比如说clki是100MHz,时钟周期10ns,而时钟偏斜通常在几百皮秒量级,这确实不会造成分频时钟之间出现“任意中间相位”。但它会造成一种更讨厌的情况:在观测时钟边沿时,div2_clk1和div2_clk2的跳变时刻存在很小的偏移,导致在两者同时作为触发时钟时,建立时间和保持时间余量不一致。

更关键的是,如果两个分频器在布局时被放得很远,或者一个在时钟树根部附近、一个在叶子末端,那么它们对clki沿的敏感时刻就有了系统性偏差。这个偏差不会改变“同相或反相”的分类,但会让“同相”变成“近似同相但差了1ns”,在一些对边沿关系敏感的下游电路里,这个1ns可能就足以造成功能错误。

2.3 根源三:组合逻辑极性调整带来的“假反相”

第三种相位不确定来源更加隐蔽,经常出现在那种把分频时钟又经过了一级组合逻辑再输出的设计里。比如:

assign div2_clk2 = ~div2_r2;

或者:

assign div2_clk2 = div2_r2 & clki;

这两段代码都在分频器输出后又做了处理。第一段纯粹取反,会让两个分频时钟从同相变反相;第二段把分频信号和源时钟做与操作,生成的是一个占空比非50%的脉冲信号,严格来说已经不是“二分频时钟”了,但如果你拿示波器去看,它的上升沿位置可能和另一个分频时钟有至少半个clki周期的偏移。

这里给刚入行的读者提个醒:在RTL里看到assign语句对时钟信号做逻辑变换,一定要格外小心。时钟信号最好从寄存器直接引出,中间不要经过任何组合逻辑,否则你得到的可能是毛刺、畸变波形和不可预期的相位关系。

2.4 为什么“复位后第一次翻转”会成为竞争条件

如果把两个D触发器的复位释放过程放到时间轴上分析,会发现一个典型的竞争条件:在复位撤销沿附近,两个触发器同时被释放,而它们下一次采样到的clki沿可能是同一条。这种情况下,如果两个触发器本身的传输延迟有差异、复位撤销到达两个触发器的时间有差异,就可能一个触发器已经开始翻转,另一个还没反应。

这正是标题说的“可能是同相,也可能是反相”在真实硬件上的微观来源。很多工程师在仿真时发现不了这个问题,因为仿真模型里复位信号到所有触发器的延迟通常都是0,两个分频器永远同相。一旦上了板子,2051个触发器里有那么一两个因为温度、电压波动导致延迟变化,相位关系就可能翻转。仿真过了、上板挂了、查半天查不到原因,最后发现是复位释放竞争引起的相位不确定性——这种事情在项目里并不少见。

3. 实测观察:用示波器和片内逻辑分析仪分别能测到什么

3.1 示波器测量:注意触发方式和探头延迟

要验证标题描述的“可能是同相也可能是反相”,最直观的办法是拿一块FPGA开发板,把div2_clk1和div2_clk2引到两个空闲IO上,用示波器两个通道同时观察。

实际操作有几个细节需要注意。第一,示波器的触发源不要选div2_clk1也不要选div2_clk2,最好选一个与两者都无关的参考信号,或者直接用clki触发。否则示波器每次触发后显示的波形会因为触发通道不同而看起来相位不一致,干扰判断。第二,两个探头必须做deskew校准,否则探头本身几十纳秒的延迟差会让你误判相位关系。第三,用短地线、弹簧地针,避免长地线引入的噪声让边沿抖动被放大。

实测中你会看到什么?按下复位按键后,如果两个分频器的初始状态随机,你会看到div2_clk1的上升沿和div2_clk2的上升沿有时候对齐,有时候正好错开一个clki周期。多按几次复位,两种状态都可能出现。这不是示波器抽风,而是硬件本来的行为。

3.2 用ILA观察片内信号:采样时钟的选择是成败关键

如果不想把信号引到外部IO,也可以用FPGA片内的集成逻辑分析仪(ILA)来观察。但这里有一个很容易犯的错误:ILA的采样时钟必须认真选择。

如果ILA的采样时钟用clki本身,那么你看到的是clki每个上升沿时刻div2_clk1和div2_clk2的电平值。假设div2_clk1和div2_clk2同相,那么在clki的奇数沿,两个信号的值相同(要么都是0,要么都是1);如果反相,那么在clki的奇数沿,两个信号的值始终相反。这个特征非常清晰,很容易判断。

如果ILA的采样时钟用了一个与clki无关的高速时钟,那么采样点相对于clki的位置是不确定的,波形上看起来就像div2_clk1和div2_clk2之间有一个随机的时间偏移,反而把问题搞复杂了。

我自己实测时还喜欢加一个计数器辅助判断:每次复位释放后,用软件记录第一次采样到的div2_clk1和div2_clk2组合值。统计100次复位,如果出现大约50次组合相同、50次组合相反,那就说明两个分频器的初始相位确实是不确定的。这个实验虽然土,但对验证硬件行为非常有效。

3.3 把波形画出来:同相与反相的辨识方法

下面用伪波形示意一下两种情况。假设clki的上升沿用“|”表示:

同相关系:

clki | | | | | | | | div2_clk1 _|‾‾‾|___|‾‾‾|___|‾‾‾|___|‾‾‾ div2_clk2 _|‾‾‾|___|‾‾‾|___|‾‾‾|___|‾‾‾

反相关系:

clki | | | | | | | | div2_clk1 _|‾‾‾|___|‾‾‾|___|‾‾‾|___|‾‾‾ div2_clk2 ‾‾‾|___|‾‾‾|___|‾‾‾|___|‾‾‾|_

从波形上可以看到,反相时div2_clk2的上升沿正好落在div2_clk1的下降沿附近,也就是差了180度。这个辨识方法在调试时非常常用:先看两个分频时钟的上升沿是否对齐,对齐就是同相,错开一个源时钟周期就是反相。

4. RTL写法与时序约束:如何让相位关系变得可控

4.1 推荐写法:统一例化,明确输出极性

如果你在设计阶段就想把相位关系固定下来,不要在多个模块里各写各的分频器,而是把分频器收敛成一个统一模块,并在例化时明确使用同一组寄存器的不同输出端或者同一输出端。

比如:

module clk_div2 ( input wire clki, input wire rst_n, output wire div2_clk1, output wire div2_clk2 ); reg div2_r; always @(posedge clki or negedge rst_n) begin if (!rst_n) div2_r <= 1'b0; else div2_r <= ~div2_r; end assign div2_clk1 = div2_r; // 统一用Q端 assign div2_clk2 = ~div2_r; // 显式反相输出,或者也直接用Q端 endmodule

这个模块最大的好处是:div2_clk1和div2_clk2共享同一个寄存器div2_r的同一状态,不存在两个独立触发器初始状态竞争的问题。如果你需要同相,两个输出都取div2_r;如果需要反相,一个取div2_r,一个取~div2_r。相位关系由代码显式决定,不再依赖复位时序和布局布线。

4.2 避免把分频时钟当普通数据信号使用

这里想强调一个很容易被忽视的问题:分频器的输出一旦被明确设计为时钟,就应该走时钟路径,而不是数据路径。

有些工程师为了省事,在顶层直接用wire把div2_clk1接到另一个模块的时钟端口,中间没有任何处理。这在功能仿真时没问题,但综合实现时工具不一定能自动把这条网络分配到全局时钟资源上。如果div2_clk1和div2_clk2最终走的是普通布线资源,到达不同寄存器的延迟会很大,相位关系就算在RTL里定义好了,实际测得也可能会差出好几个纳秒。

更稳妥的做法是:如果需要把分频时钟送给多个模块,在顶层加一层BUFG(全局时钟缓冲器)。Xilinx FPGA里可以直接用(* keep = "true" *)约束保持BUFG不被优化掉,也可以依赖工具的自动推断。关键是你要在时序报告里确认这两个时钟确实占用了时钟资源,而不是被当成普通信号在跑。

4.3 时序约束:不要让两个分频时钟变成“异步”关系

很多工程里的相位错乱问题,不是RTL写错,而是约束里把两个分频时钟之间的关系定义错了。特别是当div2_clk1和div2_clk2反相时,有人觉得它们相位相反、边沿不齐,就顺手在SDC里写了set_clock_groups -asynchronous,把两个时钟当成完全异步处理。

这个做法大错特错。div2_clk1和div2_clk2同源于clki,也就是说它们的每一个有效沿都有确定的相对位置关系。异步约束会告诉时序分析工具:这两个时钟之间不需要分析时序路径。于是跨时钟域的路径时序余量完全不检查,等到板子上跑起来,采样关系错误,你连报错都看不到。

正确的做法是保留两个分频时钟之间的同步关系约束,让工具严格按照“相差0度”或“相差180度”来分析路径。在SDC里可以这样写:

create_generated_clock -name div2_clk1 -source [get_pins clki] -divide_by 2 [get_pins div2_clk1] create_generated_clock -name div2_clk2 -source [get_pins clki] -divide_by 2 [get_pins div2_clk2]

如果工具能自动推断出divide_by 2关系,那最好;如果推断不出来,需要手动指定对应的源时钟沿。关键是-source必须指向同一个物理时钟节点clki,这样两个生成时钟的相位关系才会被工具正确计算。

4.4 相位对齐的经典约束:edges与edge_shift

针对标题这种“可能同相也可能反相”的需求,约束里值得花点时间处理的是边沿定义。假设时序分析工具默认认为div2_clk2的上升沿和div2_clk1的上升沿对齐,但实际需求是反相关系,那么你可以通过-edges来显式定义生成时钟的边沿位置。例如,clki周期为10ns,div2_clk1在clki的第一个上升沿之后翻转,div2_clk2在clki的第二个上升沿之后翻转,那么可以这样定义:

create_generated_clock -name div2_clk2 -source [get_pins clki] -edges {2 4} [get_pins div2_clk2]

这条约束告诉工具:div2_clk2的上升沿和下降沿分别对应clki的第2条和第4条边。这样工具在分析div2_clk1到div2_clk2的跨时钟路径时,会按真实的180度相位关系去计算建立时间和保持时间余量。

当然,很多FPGA综合工具自带生成时钟的自动推断,手动指定边沿容易发生冲突。我的建议是:先让工具自动推断,然后在时序报告里仔细查看两个分频时钟的Waveform,确认它们的相位关系是否符合预期。如果不符合,再手动增加约束,并且用report_clocks检查覆盖率。

5. 如果真的依赖“同相”或“反相”关系,怎么修最稳

5.1 举个会翻车的功能场景

想象一个具体的场景:模块A用div2_clk1作为工作时钟,输出一组数据;模块B用div2_clk2作为工作时钟,采样这组数据。两个模块都工作在同频时钟下,设计的初衷可能是利用div2_clk1和div2_clk2的相位差来做类似乒乓操作或双沿采样的功能。

假设设计者默认两个分频时钟同相,模块B的采样沿和模块A的驱动沿对齐。但上板后复位时序导致div2_clk2反向,那么模块A在哪个沿驱动数据、模块B在哪个沿采样数据就完全错位了。这时候模块B采到的不是稳定的有效数据,而是边沿正在变化的中间态。功能表现为偶发数据错误,而且只在上电或者复位后的特定条件下出现,特别难复现。

这种问题最坑的地方在于:单次运行可能一直“碰巧正常”,因为两个分频时钟的相位关系在上电后通常是固定的,只有重新上电或复位才可能翻转。所以你测十次可能都是正常的,第十一次就挂了。

5.2 修复思路一:消除初始相位竞争

要彻底消除这种随机性,核心是让两个分频器的初始状态在复位释放后可控。做法是:先对异步复位做同步撤离,再用同步后的复位去驱动两个分频器。

reg rst_sync1; reg rst_sync2; always @(posedge clki or negedge rst_n) begin if (!rst_n) begin rst_sync1 <= 1'b0; rst_sync2 <= 1'b0; end else begin rst_sync1 <= 1'b1; rst_sync2 <= rst_sync1; end end wire rst_sync_n = rst_sync2; // 同步后的复位,高有效 always @(posedge clki or negedge rst_sync_n) begin if (!rst_sync_n) div2_r1 <= 1'b0; else div2_r1 <= ~div2_r1; end always @(posedge clki or negedge rst_sync_n) begin if (!rst_sync_n) div2_r2 <= 1'b0; else div2_r2 <= ~div2_r2; end

这里的关键是rst_sync2在clki域的同一个上升沿之后稳定撤销,两个分频器从这个时刻开始下一次计数。由于它们采样到的第一个clki沿是同一条,只要两个触发器本身没有差异,初始相位就固定了。

5.3 修复思路二:用“初态强制脉冲”做上电对齐

如果系统不允许动复位逻辑,还有一个思路:加一个初态强制脉冲。具体做法是在系统启动后的第一个clki周期内,用一个同步脉冲把两个分频器的状态分别置为已知值——比如div2_clk1置为0,div2_clk2置为1。这样无论复位释放时两个触发器的状态如何,启动后都会被强制拉到预定义相位关系。

这个方法的优点是改动范围小,只需要在分频器模块里加一个同步装载信号。缺点是如果强制脉冲本身和clki的相位处理不好,可能引入毛刺。更稳妥的做法是从统一状态机里发出一个与clki严格对齐的enable信号,在enable有效的那个沿对两个分频器同时装载。

5.4 修复思路三:从根本上绕开“分频时钟当工作时钟”的设计

最后给出一个我觉得最值得推广的建议:只要条件允许,就不要把div2_clk1和div2_clk2作为工作时钟使用。

大部分情况下,你真正需要的是“每两个clki周期产生一个有效的采样窗口”,而不是“真的存在一个独立的二分频时钟域”。这种情况下,更推荐使用时钟使能(clock enable)方案:所有逻辑继续使用clki作为唯一时钟,用一个使能信号表示偶数周期或奇数周期。

reg [0:0] cnt; wire clk_en = (cnt == 1'b0); always @(posedge clki or negedge rst_n) begin if (!rst_n) cnt <= 1'b0; else cnt <= cnt + 1'b1; end // 使用使能信号替代分频时钟 always @(posedge clki or negedge rst_n) begin if (!rst_n) data_out <= 1'b0; else if (clk_en) data_out <= data_in; end

这样做之后,整个设计只有一个时钟域clki,不存在两个分频时钟之间的相位对齐问题,不需要额外的时序约束,也不会出现复位竞争。付出的代价仅仅是多几行代码和一个使能信号,换来的是所有跨模块路径都变成纯同步路径,时序分析逻辑简单得多。

我在这类需求上踩过不少坑,回过头来看,标题描述的现象之所以让很多人头疼,本质上是因为大家既想让分频时钟存在,又没想清楚分频时钟之间到底需不需要确定的相位关系。如果你的设计接受“同相或反相”都行,那就用独立分频器加同步复位,别的不用管;如果下游功能依赖确定的相位关系,那就用统一分频器或者时钟使能方案,把相位关系写死在代码里。

最后一句话总结我个人的经验:所有分频时钟的相位风险,都源于“初始状态不可控”和“边沿关系未约束”这两个地方。把其中任何一个控制住,标题里的问题就解决了;把两个都控制住,你的设计基本不会因为分频时钟相位问题翻车。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询