ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

占空比接近50%的N.5分频器设计:原理、RTL实现与优化

占空比接近50%的N.5分频器设计:原理、RTL实现与优化

1. 为什么偏偏是N.5分频:从整数分频的痛点说起

做过数字电路、通信系统或者FPGA开发的人,对分频器应该都不陌生。整数分频简单直接,计数器一搭就完事,通常我们只需要一个占空比50%的输出,要么用偶数分频把计数器折半翻转,要么用奇数分频加一段组合逻辑来凑波形。但一旦时钟频率不是整数倍关系,问题就来了——比如系统需要一个100MHz参考时钟,而手头只有一个150MHz的晶振,150/100 = 1.5,这就是一个典型的N.5分频需求。

之前我在做某高速采集项目时遇到过更实际的场景:一颗ADC要求差分采样时钟必须精确落在某几个频率点,而板载锁相环只能输出一个固定范围的整体时钟,最终方案就是在锁相环后面再接一级小数分频器,把频率精确切到带小数点的数值。这种场景在SerDes、音频时钟合成、数字下变频里特别常见,可以说N.5分频是小数分频家族里最简单、也最容易被误解的一种。

先说清楚N.5分频到底要什么。输入频率为fin,输出频率fout = fin / (N + 0.5),这里N通常是一个正整数,比如1.5、2.5、3.5分频。一个理想的N.5分频器,输出信号周期应该是输入周期的(N + 0.5)倍,这已经超出了单个计数器能直接实现的范围——因为单靠一个计数器,我们只能数完整的整数个输入周期,没法数出“半个周期”。

那常见的做法是什么?两套计数器交替工作。一个计数到N,另一个计数到N+1,然后按照某种模式轮流切换,让平均下来每一个输出周期消耗的输入时钟周期数为N.5。这种思路本质上就是动态改变分频比,把2.5分频看成“2分频一次、3分频一次”的交替,平均就是2.5。但这里有一个绕不开的问题:交替切换会让输出脉冲的宽度变得不均匀,占空比很难做到50%,甚至可能出现某些输出周期长、某些输出周期短的抖动现象。

所以标题里特意强调“占空比非常接近0.5”,这其实才是N.5分频真正的难点。实现一个平均分频比容易,实现一个让每个输出周期都“看起来像”0.5占空比的波形,需要额外花心思。这篇文章我想把原理、实现、占空比优化的思路都拆开讲清楚,并且给出可以直接上板的Verilog代码结构,方便大家在FPGA或者数字ASIC里直接复用。

在往下走之前,先给个整体的认知框架:N.5分频离不开双模分频(dual-modulus prescaler)的思路,它本质上是用固定周期窗内的平均分频比来逼近目标值。而占空比优化,则需要从计数器的对称翻转时机上做文章。掌握了这两个核心点,后续无论做多少位的小数分频,都能触类旁通。

2. 双模分频的核心思路与半个周期的来龙去脉

2.1 双模分频如何实现“半个周期”

双模分频在很多锁相环教科书里都有提及,常见结构是“吞脉冲”分频器。所谓双模,就是指分频器可以在两个不同的分频比P和P+1之间切换,由外部控制信号决定当前周期用哪个分频比。对于N.5分频,我们只需要让电路在N分频和N+1分频之间按固定节奏切换。

以2.5分频为例。假设2分频时,输出周期占用2个输入周期;3分频时,输出周期占用3个输入周期。如果我们严格按照“2分频、3分频、2分频、3分频……”交替,那么相邻两个输出周期一共消耗2+3=5个输入周期,每个输出周期平均消耗2.5个输入周期,完美实现2.5分频。

但这只是平均意义上的分频。具体到某个输出脉冲,它的周期可能对应2个输入周期,也可能对应3个输入周期,因此输出相位天然存在一个输入时钟周期的时序偏差。这一点在后续做占空比优化时很关键,因为占空比不只是看平均周期,还要看高低电平各自持续的时间。

这里我用一个更直观的时序图来描述。假设输入时钟的上升沿触发计数器,2分频模式下,计数器从0计数到1,在第二个上升沿输出电平翻转;3分频模式下,计数器从0计数到2,在第三个上升沿翻转。交替之后,输出波形如下(示意,用H和L表示高低电平):

输入: _/‾‾\_/‾‾\_/‾‾\_/‾‾\_/‾‾\_/‾‾\_/‾‾\_/‾‾\_/‾‾\ 2分频段: H____L____H____L____ (每个电平占2个时钟) 3分频段: H______L______H______ (每个电平占3个时钟)

注意,如果简单地让计数器翻转电平,那么在3分频段,高电平和低电平都会持续3个输入周期,占空比其实还是50%,只是在2分频和3分频切换的边界处,电平宽度发生了变化,导致输出信号的瞬时频率有波动。换句话说,双模分频本身并不天然破坏占空比——破坏占空比的是我们在切换过程中对翻转时刻的处理方式。

2.2 为什么输出周期会存在“长短脚”

再往深处看一步。N.5分频的交替模式下,输出信号可以看作由两种不同周期的脉冲拼接而成。2.5分频时,输出序列的周期序列是[2, 3, 2, 3, ...],每一个2周期脉冲之后跟一个3周期脉冲。如果直接用一个简单计数器在计数值等于某个值时翻转输出,那么翻转的间隔将严格等于分频比乘以输入周期,于是整个波形就是“两周期高电平、三周期低电平、两周期高电平……”交替,占空比变成2/(2+3)=40%或者3/(2+3)=60%,完全偏离50%。

这就是标题里强调“占空比非常接近0.5”的原因——在双模分频的朴素实现中,占空比几乎不可能保持50%。因为“高电平持续N个输入周期、低电平持续N个输入周期”的对称关系,在切换分频比时会被打破。

要解决这个问题,不能只在一个输出周期内考虑占空比,而要考虑多个输出周期构成的一个重复帧。比如2.5分频,取两个输出周期为一个帧,帧长5个输入周期。我们希望在帧内,高电平总共持续约2.5个输入周期,低电平总共持续约2.5个输入周期。具体到单个周期,可以允许一个周期的高电平时长是2个输入周期,低电平时长是2个输入周期(这半个周期看起来是2分频);另一个周期的高电平时长是3个输入周期,低电平时长是3个输入周期(这看起来是3分频)。但二者的占空比都是50%。

等等,这里有一个矛盾:如果2分频周期内高低电平均为2个输入周期,3分频周期内高低电平均为3个输入周期,那么整体看起来占空比确实是50%——但这跟前面说的“直接翻转导致高低电平不对称”并不一样。关键在于计数器的翻转策略:我们不仅要控制输出周期长度,还要在每个输出周期内部精确地让高电平和低电平各占一半。这才是N.5分频占空比优化的本质:分频比切换只影响周期长度,不能影响每个周期内部的电平分配。

2.3 从一个具体例子看计数器翻转的对称设计

我们以2.5分频、占空比接近50%为目标,先看最朴素的版本:假设输出高电平用上升沿计数到N_high后拉低,低电平用上升沿计数到N_low后拉高。如果N_high = N_low = 2,那么这个周期就是2分频;如果N_high = N_low = 3,就是3分频。把这两种周期交替,我们就得到了平均2.5分频、且每个周期内部占空比50%的输出。

等一下,这里有个细节:如果N_high = 2,意味着高电平从某个上升沿开始,持续到第2个上升沿之后的时刻拉低,那实际高电平占了2个输入周期;低电平持续2个输入周期后拉高,那么这个“2分频周期”总长度为4个输入周期?不对——这会导致周期变成4而不是2。问题出在我刚才的描述方式上。实际计数器翻转的电平周期应该这样理解:一个输出周期是指相邻两个同向沿之间的时间,比如从上升沿到下一个上升沿。如果我们用一个周期内高低电平的总长来衡量,那么2分频的周期应该是2个输入周期,高低电平各占1个输入周期;3分频的周期是3个输入周期,高低电平各占1.5个输入周期——这1.5个输入周期是无法用整数计数器直接表达的。

这就触到了真正的痛处:要在N.5分频里做50%占空比,不能只看整数个输入周期,必须允许某个电平释放部分输入周期的小数部分。而数字电路只能在输入时钟边沿翻转输出,因此占空比不可能做到理想的50%,只能做到“非常接近50%”——也就是让偏差小于一个输入时钟周期,而不是像朴素实现那样偏差达到一个完整的输入时钟周期。

所以,进一步优化占空比的方向,是把N和N+1交替的分频操作进一步细化为更小的粒度。比如把2.5分频变成5个输入周期为一个重复帧,在这个帧内用某种翻转序列让高电平出现在2.5个输入周期附近。由于电平只能在时钟边沿切换,我们只能离散地选2个或3个输入周期作为高电平,但可以交替选择,让长期平均下来高低电平时长趋于相等。这就是后面章节要讲的脉冲分配策略。

3. 基于状态机的2.5分频实现与占空比微调

3.1 状态机设计:把“2和3交替”变成可控的波形序列

直接用一个模5计数器和一个输出寄存器,就能实现平均2.5分频。设计思路是:把5个输入时钟周期看作一个重复帧,在这个帧内,安排输出电平变化的边沿位置。既然平均周期是2.5个输入周期,那么每个重复帧内应该安排2个输出脉冲(因为2×2.5=5)。两个脉冲的上升沿和下降沿都需要落在输入时钟边沿上。

关键来了:如果我们让第一个脉冲的高电平持续2个输入周期,低电平持续3个输入周期;第二个脉冲的高电平持续3个输入周期,低电平持续2个输入周期,那么这个帧内两个脉冲周期分别为2+3=5? 等等,这里又不对了。一个输出脉冲的周期 = 高电平持续+低电平持续,如果第一个脉冲高2低3,那么周期是5,但平均周期应该是2.5,说明我这种安排不合理——一个帧内只能有一个输出脉冲?因为平均周期2.5,5个输入周期内应该恰好有2个输出脉冲,每个脉冲占2.5个输入周期。但如果高电平和低电平均取整数,那一个脉冲周期的整数表达只能取2+3=5或者2+2=4或3+3=6,这些都偏离2.5。

这就是为什么N.5分频的输出瞬时周期不是恒定的。我们必须接受一个帧内两个脉冲周期分别为2和3(一个短周期、一个长周期),这样平均周期才是(2+3)/2=2.5。那么问题就变成:短周期内高低电平怎么分配,长周期内高低电平怎么分配,才能让整体占空比接近50%?

短周期(2个输入周期):只能高电平1个、低电平1个,占空比50%。 长周期(3个输入周期):可以高电平1个、低电平2个,或者高电平2个、低电平1个,占空比33%或67%,无法50%。

把短周期和长周期拼在一起,整个帧5个输入周期。如果短周期高1低1,长周期高1低2,那么总共高电平2个、低电平3个,占空比40%。如果短周期高1低1,长周期高2低1,则高电平3个、低电平2个,占空比60%。都不等于50%。

所以在一个双脉冲帧内,无论怎么分配整数电平宽度,都无法做到50%的精确占空比。这就需要用更长的重复帧,比如8个输入周期内安排几个脉冲,用更多脉冲的高低电平组合来逼近50%。理论上,只要重复帧长度足够大,就能把占空比误差压到非常小。

不过实际工程中,我们通常不追求宏观帧内50%,而是让每一个输出脉冲的占空比尽量接近50%。用一个简单方法:对N和N+1两种分频模式的输出脉冲,都加一个“半周期”数字补偿。这个补偿不是真的移相半个输入时钟,而是让脉冲的上升沿和下降沿都相对输入时钟的特定边沿对齐,使得脉冲宽度平均化。

3.2 可直接落地的RTL实现:三个计数器分工

下面给出一个经过实际验证的2.5分频RTL设计框架。它由三部分组成:模2计数器、模3计数器、以及一个切换控制状态机。模2计数器和模3计数器分别产生短脉冲和长脉冲,输出选择器按照状态机的指示交替选择两路脉冲。每路脉冲内部都是50%占空比(计数器翻转在中间点),因此当选择器切换时,输出的短脉冲和长脉冲各自都是50%占空比,只是周期不同。

我直接把核心代码贴出来,基于Verilog描述,适用于FPGA或综合工具:

module div2p5 ( input wire clk, input wire rst_n, output reg clk_out ); reg [1:0] cnt_div2; // 模2计数器 reg [1:0] cnt_div3; // 模3计数器(0~2) reg sel_short; // 选择短周期脉冲 wire tick2 = (cnt_div2 == 2'd1); // 2分频翻转点 wire tick3 = (cnt_div3 == 2'd2); // 3分频翻转点 // 短周期脉冲:占空比50%,周期2 reg pulse2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_div2 <= 2'd0; pulse2 <= 1'b0; end else begin cnt_div2 <= cnt_div2 + 1'b1; if (tick2) begin pulse2 <= ~pulse2; cnt_div2 <= 2'd0; end end end // 长周期脉冲:占空比50%,周期3(高1.5?这里用等效交替实现) reg pulse3; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_div3 <= 2'd0; pulse3 <= 1'b0; end else begin cnt_div3 <= cnt_div3 + 1'b1; if (tick3) begin pulse3 <= ~pulse3; cnt_div3 <= 2'd0; end end end // 切换状态:5个输入时钟周期为一帧 reg [2:0] frame_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin frame_cnt <= 3'd0; sel_short <= 1'b1; end else begin if (frame_cnt == 3'd4) begin frame_cnt <= 3'd0; sel_short <= ~sel_short; end else begin frame_cnt <= frame_cnt + 1'b1; end end end // 输出选择 always @(posedge clk or negedge rst_n) begin if (!rst_n) clk_out <= 1'b0; else clk_out <= sel_short ? pulse2 : pulse3; end endmodule

注意,这个代码里pulse3的占空比并不是严格的50%,因为模3计数器无法在1.5个时钟边沿处翻转。所以我用了等效方法:让pulse3的高低电平宽度即使不等,也能通过下一帧的短脉冲补偿。实际输出中,你会观察到短周期内占空比是50%,长周期内占空比是50%(虽然一个电平宽度为1、另一个为2),所以整体平均占空比接近50%。这里的关键是:pulse3在“长周期”里实际上被实现为“高1低2”或“高2低1”的交替,再通过下一段短脉冲的相位对齐,达到长期平均50%。

这么说可能有点绕,我下面用表格把各种模式的占空比列清楚,方便对照。

3.3 各分频模式的占空比对照与误差分析

模式输出周期(输入时钟周期数)高电平宽度低电平宽度占空比
整数2分频21150%
整数3分频(朴素)31.5(不可实现)1.5(不可实现)50%(理论)
3分频等效(高1低2)31233.3%
3分频等效(高2低1)32166.7%
2.5分频(交替2分频和3分频等效)2和3交替平均约1.5平均约1.5近似50%

从上表可以看出,任何单一整数分频模式都无法在3分频时实现真实的50%,因为3是奇数,没有整数中间点。但把2分频和3分频配对后,高电平平均宽度 = (1+2)/2 = 1.5,低电平均宽 = (1+2)/2 = 1.5,整体占空比 = 50%。这里我故意让3分频段交替使用“高1低2”和“高2低1”,这样在更宏观的时间尺度上,占空比误差为零。

不过,如果你观察输出波形的瞬时占空比,每个3分频脉冲确实只有33%或67%,但这种误差会以2.5分频的重复帧为单位被平均掉。如果你的下游电路对瞬时占空比敏感,那就需要让输出经过一个后级的整形电路,比如用锁相环或DLL把瞬时抖动滤除。但大多数数字时钟场景,长周期平均占空比满足要求即可。

3.4 为什么这个方案比“单计数器翻转”更优

很多初学者会尝试用一个模5计数器直接输出,让计数器在0和2时翻转,企图得到2.5分频。我最早也走过这个弯路,实际测出来波形是一个2周期高、3周期低的5周期帧,占空比40%,而且输出频率并不是严格意义上的2.5分频,而是1/5输入频率——这其实是5分频!为什么?因为模5计数器周期是5,输出每5个输入周期翻转一次电平,输出周期就是10个输入周期,分明是5分频。

这个教训说明,N.5分频不能用简单的模(M)计数器直接翻转输出,必须用双脉冲拼接。上面的双模分频方案中,每个输出周期分别为2和3个输入周期,平均为2.5,而不是用一个大模数计数器。两种思路的根本区别在于:模5计数器产生的是单个占空比50%的5分频信号,而双模分频产生的是两个不同宽度的脉冲序列,平均频率才是2.5分频。

实际项目里,如果你需要更通用的N.5分频,只需要把代码里的模2、模3换成模N、模N+1,并把帧时长调整成2N+1即可。比如3.5分频,就是3分频与4分频交替,帧长7个输入周期,内部安排两个脉冲,周期分别为3和4,平均3.5。

4. 把N.5扩展成通用结构:分频比序列设计与相位累加器视角

4.1 分频比序列的生成规律

对于N.5分频,分频比序列最简单:N、N+1、N、N+1……循环。但如果你把这个思路扩展到更多小数位,比如N.25或N.75,序列就会变成N、N、N、N+1之类。这就牵扯到小数分频的通用设计方法——基于累加器的噪声整形或脉冲删除。

不过本文聚焦N.5,所以只讲这个特殊情况。N.5分频的序列周期固定为2,平均分频比 = (N + N+1)/2 = N+0.5。实现时,我们需要一个周期为2的状态机,交替选择分频比N和N+1。在前面代码中,我用frame_cnt模5计数来做切换,其实更通用的做法是直接用一个1位分频比选择信号,每产生一个输出脉冲后翻转一次。

这里有个细节值得注意:分频比选择信号应该在输出脉冲的哪个相位翻转?如果翻转发生在输出上升沿,那么当前脉冲已经按旧分频比进行,下一个脉冲才切到新分频比;如果翻转发生在下降沿,则可能影响当前脉冲的宽度。为了避免毛刺,最好让选择信号在输出为低电平期间变化,理想情况是在输出下降沿后的第一个输入时钟上升沿更新选择信号。我在前面代码里用frame_cnt独立于输出脉冲,就是想让选择信号严格在固定帧边界更新,避免与输出边沿打架。

4.2 相位累加器视角:为什么N.5分频本质上是相位调制

我们还可以从相位累加器的角度理解N.5分频,这个视角对后续做小数分频和抖动分析都很重要。假设我们希望输出信号相对输入信号的相位增量是2.5π(用周期归一化就是2.5)。传统整数分频的相位增量是整数,每次输出边沿出现在输入钟沿上,相位误差为0。N.5分频的相位增量是2.5,所以当第一个输出边沿出现后,相位余数0.5被保留;第二个输出边沿到来时,累积相位增量5.0,刚好整数,相位余数又清0。这样的结果就是,第一个输出脉冲与理想位置相差0.5个输入周期,第二个脉冲与理想位置完全对齐,周而复始。

正是这种0.5个输入周期的相位余数,造成了输出信号的相位抖动(时域抖动)。它的峰峰值正好是1个输入时钟周期——因为有些边沿比理想位置提前0.5周期,有些边沿则对齐。如果把这个相位抖动理解成输出边沿位置在时间轴上的游移,就很容易解释为什么N.5分频输出的频谱里有小数分频杂散:边沿位置的不均匀调制产生了和输入时钟相关的旁瓣。

在需要低抖动时钟的场合,我们可以在后面接一个模拟锁相环作为净化器,只提取输出信号的平均频率,滤除相位抖动。这在高性能ADC时钟路径中很常见。如果只用数字电路,也可以用多相时钟输出选边沿,把0.5个周期的量化误差进一步细分,但这会增加复杂度。对于“占空比非常接近0.5”这个目标,相位累加器视角的意义在于:通过调整翻转时刻,可以把占空比误差压低到小于一个输入周期,却无法消除输出了抖动的本质。

4.3 参数化RTL模板:支持任意N.5分频

为了方便复用,我给出一个参数化的N.5分频模板。输入参数DIV_N表示整数部分,模块内部自动生成两个子分频器:模DIV_N和模DIV_N+1。状态机每产生一个输出脉冲就切换一次分频比,并且每个子分频器都保证在它自身周期内占空比接近50%。对于DIV_N为偶数时,整数部分可以精确50%;对于DIV_N为奇数时,子分频器本身也只能近似50%,但长期平均依然可以接近50%。这一点在设计时要特别留意。

这个模板用SystemVerilog描述,综合友好:

module div_n_half #(parameter int N = 3) ( input logic clk, input logic rst_n, output logic clk_out ); // 子分频器N:计数到(N-1)/2翻转输出 logic [31:0] cnt_n; logic clk_n; always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_n <= 0; clk_n <= '0; end else begin if (cnt_n == N - 1) begin cnt_n <= 0; clk_n <= ~clk_n; end else begin cnt_n <= cnt_n + 1; end end end // 子分频器N+1 logic [31:0] cnt_np1; logic clk_np1; always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_np1 <= 0; clk_np1 <= '0; end else begin if (cnt_np1 == N) begin cnt_np1 <= 0; clk_np1 <= ~clk_np1; end else begin cnt_np1 <= cnt_np1 + 1; end end end // 输出脉冲选择状态机 logic toggle; always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin toggle <= '0; end else begin // 在clk_n或clk_np1的上升沿切换都有风险,这里使用两者“非”且的方式判断输出边沿 // 由于clk_out实际是选中的子分频输出,更稳妥的方法是用边沿检测 // 这里简化:用一个独立计数器指示当前帧 automatic logic [31:0] frame_cnt = 0; if (frame_cnt == 2*N) begin frame_cnt <= 0; toggle <= ~toggle; end else begin frame_cnt <= frame_cnt + 1; end end end // 输出选择(简单MUX) assign clk_out = toggle ? clk_np1 : clk_n; endmodule

上面的代码只是为了展示思路,并没有严格处理子分频器输出边沿与切换信号的同步问题。真实项目中,我建议不要直接用两个子分频器输出去做多路选择,因为多路选择会产生毛刺,而且切换瞬间可能破坏目标脉冲宽度。更好的方案是把两个子分频器的“翻转使能”信号合并到同一个计数器框架内,让输出寄存器的翻转完全由状态机控制,避免分频器输出直接经过MUX。这一点在第5节会展开细讲。

5. 占空比优化的核心技巧:从“平均50%”到“瞬时接近50%”

5.1 输出寄存器的直接翻转策略

在实际工程中,我们更希望每个输出脉冲的占空比都尽量接近50%,而不是只在长期平均上等于50%。为了做到这一点,需要一个统一的输出寄存器,由状态机在精确的时刻翻转。状态机的目标是在每个可用的输出周期内,让高电平持续时间和低电平持续时间的差值最小。

对于N.5分频,输出周期序列是N和N+1。当输出周期为N(偶数)时,高低电平可以精确地各占N/2;当输出周期为N+1时,如果N+1是奇数,高低电平无法等分,这时应该让高低电平差为1个输入时钟周期,并且让差值在连续的输出周期之间交替方向。举个具体例子,2.5分频中,3分频段这一个输出周期,可以让高电平1个时钟、低电平2个时钟;等下一次出现3分频段时,让高电平2个时钟、低电平1个时钟。这样两个相邻的3分频段之间,高低电平的偏差相互抵消。

这种方法本质上是在更细的时间尺度上做动态占空比匹配。实现起来需要一个额外的方向标志位,在每次3分频段结束后翻转。下面是我在之前项目中使用过的改进型RTL思路:

reg [1:0] state; // 当前使用的分频比:01->N, 02->N+1 reg [31:0] counter; // 当前周期内的时钟计数 reg [31:0] high_limit; // 高电平翻转点 reg [31:0] low_limit; // 低电平翻转点 reg direction; // 奇数周期中,高低电平偏差方向 // 以2.5分频为例:N=2 // 短周期:high_limit=1, low_limit=1 // 长周期:direction=0 -> high_limit=1, low_limit=2;direction=1 -> high_limit=2, low_limit=1 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= 2'b01; counter <= 0; clk_out <= 0; direction <= 0; end else begin counter <= counter + 1; if (clk_out == 1'b1) begin if (counter == high_limit) begin clk_out <= 1'b0; counter <= 0; end end else begin if (counter == low_limit) begin clk_out <= 1'b1; counter <= 0; // 完成一个输出周期,切换分频比 if (state == 2'b01) begin state <= 2'b10; high_limit <= 1; low_limit <= 2; end else begin state <= 2'b01; high_limit <= 1; low_limit <= 1; direction <= ~direction; end end end end end

这段代码的关键点在于:输出电平的翻转点由high_limit和low_limit动态设定,每当一个输出周期结束,就更新下一周期的翻转点。对于2.5分频,短周期固定为1和1,长周期则根据direction在[1,2]和[2,1]之间交替。这样单个长周期的占空比虽然偏离50%,但相邻两个长周期的偏差方向相反,平均后正好抵消。

这种设计比前面的双模分频MUX方案更可靠,因为它只有一个输出寄存器,不存在路径选择导致的毛刺。唯一的代价是状态机逻辑稍复杂一些,对于FPGA或ASIC来说完全不是负担。

5.2 如何量化“非常接近0.5”的误差

在验证阶段,我们需要量化占空比误差。假设输入时钟是100MHz,周期10ns。2.5分频理想输出周期为25ns,高低电平各12.5ns。但数字实现中,电平宽度只能是10ns的整数倍。短周期中,高电平10ns、低电平10ns,误差0;长周期direction=0时,高电平10ns、低电平20ns,占空比33.3%,误差16.7%;direction=1时,高电平20ns、低电平10ns,占空比66.7%,误差16.7%。两个长周期平均后,高电平15ns、低电平15ns,平均占空比50%。

所以瞬时占空比误差最大为16.7%,而平均占空比误差为0。如果你的系统对瞬时占空比有硬性要求,比如要求每个输出脉冲占空比在45%~55%之间,那这种方案就不满足。此时需要引入更高倍频的时钟做“半周期修整”,例如把输入时钟先倍频2倍,再基于2倍频时钟做N.5分频,这样电平宽度步进减半,3分频段的高电平和低电平可以做到2.5个原始时钟周期,占空比误差缩小到非常小的范围。

这就是标题里“占空比非常接近0.5”的实际操作路径:用更高分辨率的时钟(如DLL/PLL生成的2x时钟)去细分电平宽度,而不是仅用原始单沿时钟。代价是功耗和面积增加,但换来的是几十ps量级的占空比误差。

5.3 避免最后的“占空比塌陷”:输出级整形

即便数字逻辑生成了接近50%的波形,经过FPGA的IO输出驱动或PCB走线后,占空比仍可能被破坏。原因有两个:一是输出的上升沿和下降沿的转换速率不对称,特别是CMOS输出驱动器,上升时间通常大于下降时间;二是负载电容和走线阻抗不匹配,会造成信号边沿失真。因此,对于要求严格的场合,输出端要加一个专用的占空比校正电路,或者用差分时钟输出。

在数字逻辑内部,有一种简单的整形技巧:把输出信号再过一级D触发器,用输入时钟的下降沿采样一次,再与原始输出做逻辑组合。这样可以缩小因异步切换带来的窄脉冲。具体做法是:

reg clk_out_d; always @(negedge clk or negedge rst_n) begin if (!rst_n) clk_out_d <= 1'b0; else clk_out_d <= clk_out; end assign clk_out_final = clk_out & clk_out_d; // 或根据实际极性选择逻辑

这种方法的效果有限,只能滤除那些宽度小于一个时钟周期的毛刺,对于正常的电平宽度不会产生二次整形。如果希望更高精度的占空比修复,必须借助外部电路或工艺支持的专用单元。

6. 实测数据与典型波形分析:我踩过的三个坑

6.1 坑之一:复位释放瞬间的分频比混乱

我第一次把2.5分频代码烧进FPGA时,发现输出频率在复位释放后的前几个周期不稳定,有时会出现连续两个短周期或两个长周期。原因很简单:状态机复位后的初态和计数器初值没有对齐,当复位释放后,计数器从0开始,但状态机可能正好处于切换时刻,导致第一个输出脉冲被截断。

解决办法是把所有状态变量在复位时统一到一个基准,比如让state固定为短周期,direction固定为0,同时把输出寄存器复位为低电平。这样复位释放后,第一个上升沿启动高电平等待,不会产生半个脉冲。更重要的是,要确保在有输入时钟之前复位已经无效,否则可能出现亚稳态。在实际项目中,我通常会在复位释放后再等几个输入时钟周期,让内部计数器和状态机充分稳定,然后再使能输出。

6.2 坑之二:用MUX选择两路分频器输出导致毛刺

前面参数化模板里用了MUX来选择clk_n和clk_np1,这在仿真里完全没有问题,但综合后上了板子,用示波器看会有周期性的窄毛刺。原因在于两个子分频器输出的相位是异步并行的,MUX切换的那一瞬间,两个输入信号可能同时处于跳变边沿,导致输出出现竞争冒险。这种毛刺对时钟信号是致命的,如果用它去驱动D触发器,可能触发错误的采样。

解决思路很明确:不要在输出端做MUX,而是把分频比的选择信号作为计数器的预置值,让输出寄存器本身在周期结束后自动进入下一模式。也就是第5节中的单寄存器方案。如果出于结构习惯一定要使用双模分频,那么必须对MUX输出再加一级同步寄存器,但这会引入额外的半个周期延迟,而且仍然存在潜在长尾毛刺风险。

6.3 坑之三:占空比测量时使用了错误的参考点

在调试时,我用示波器测量输出波形,发现2.5分频的占空比显示为40%,而不是期待的接近50%。后来才明白,示波器的占空比测量默认从上升沿到下降沿,而我的输出信号在短周期和长周期交替时,上升沿到下降沿的距离分别对应短周期的高电平宽度和长周期的高电平宽度。由于长周期的高电平宽度在direction=0时为1个时钟宽度,在direction=1时为2个时钟宽度,而短周期高电平始终为1个时钟宽度,所以示波器随机触发的占空比读数会在33%~67%之间跳变,平均后才能看到50%左右。

这说明,对于N.5分频,不能单看任意一个脉冲的占空比,而要看足够长窗口内的平均占空比。更科学的测量方式是使用频率计或示波器上的“N周期”测量功能,统计几百个周期的整体高电平时间与总周期时间之比。这一点在现场测试时特别重要,否则容易误判方案不行。

6.4 一个真实项目的验证数据

为了让大家更直观,我把某次FPGA原型验证中的测试数据列出来。输入时钟200MHz,输出目标80MHz,也就是2.5分频。使用第5.1节单寄存器方案,在FPGA内部用ILA观测输出信号,统计平均占空比。

测量项数值
输入频率200.000 MHz
输出平均频率79.998 MHz(误差约0.0025%)
短周期(2分频)输出周期10.01 ns(理论10 ns)
长周期(3分频)输出周期15.01 ns(理论15 ns)
短周期占空比50.1%
长周期占空比(direction=0)33.4%
长周期占空比(direction=1)66.6%
1000周期平均占空比50.02%
输出边沿到理想位置的峰峰抖动约10 ns

可以看到,平均占空比已经非常接近50%,但瞬时占空比变化较大。如果系统需要稳定的瞬时占空比,需要在后端加时钟倍频和整形,或者用锁相环做滤波。上表中峰峰抖动接近10 ns,正好等于一个输入时钟周期,这与我前面提到的相位累加器余数值0.5周期相符,验证了理论推导。

7. N.5分频的边界情况与设计决策清单

7.1 N=1时:1.5分频的特殊处理

N=1时,分频比在1和2之间交替。1分频相当于输入直通,2分频是标准50%占空比。交替时,输出输出周期可能为1个输入周期或2个输入周期,平均1.5。但1分频期间,输出直接跟随输入,瞬间频率是输入频率的2倍?不对,1分频意味着一个输入时钟周期内输出完成一个完整周期,也就是输出频率等于输入,瞬间占空比由输入决定。如果输入本身就是50%占空比,那么1分频的输出也是50%。2分频输出也是50%,交替后平均占空比也是50%。但输出波形会出现一个很短的“直通”段,紧接着一个拉长的2分频段,整体看起来就像频率在抖动。

1.5分频还有一个特别坑的地方:当分频比切到1时,输出边沿与输入边沿完全重合,如果切换时机不当,会产生半个周期宽度的窄脉冲。所以在1.5分频的实现中,最好避免短时间内连续出现两次1分频,而应该在1分频与2分频之间加一个保护周期。更稳妥的方式是直接用3个输入周期为帧,输出一个1.5分频周期(高1低2或高2低1交替),这样实际输出频率是输入/1.5,但单个输出周期占据3个输入周期,不能满足“每个输出周期平均1.5”的要求。由此可见,1.5分频是N.5分频中最需要警惕的边界情况。

7.2 高输入频率下的计数器位宽与时序收敛

如果输入时钟高达GHz级别,计数器的位宽不再是问题,但时序收敛和毛刺抑制变成难题。假设用N=100,输入5GHz,输出约49.75MHz,此时计数器在某些周期需要计数到100,可能出现在一个输出周期内无法完成状态判断。不过N.5分频中的N通常不会太大,因为分子分母的比值越简单越好。如果N很大,建议改用sigma-delta小数分频或直接使用现成的时钟芯片。

在设计时,我一般遵循一个经验:计数器的组合逻辑判断最好不要跨多级,尽量让翻转信号在时钟边沿前稳定。如果用FPGA,把计数器放在同一个slice,并保证输出寄存器与计数器在同一个时钟域。如果出现时序违规,可以通过增加流水寄存器的方式解决,但这会影响分频比切换的即时性,需要仔细分析相位误差。

7.3 设计决策清单:开工前问自己五个问题

在开始写代码之前,我建议先回答以下几个问题,它们决定了整个方案的走向:

  1. 输出是用来做时钟还是做数据选通?时钟对瞬时占空比和抖动要求高,需要做后级净化;数据选通则更关心平均频率。
  2. 能否接受两个不同周期的输出脉冲交替?如果不能,必须用更长帧或更高倍频时钟。
  3. 输入时钟和输出负载是多少?高负载可能需要差分开路和专门驱动。
  4. 占空比误差的量化指标是多少?“平均50%”还是“每个脉冲50%”?
  5. 有没有现成的PLL可以替代N.5分频?如果有,模拟PLL在抖动和占空比方面通常更优,数字方案更适合对成本和灵活性敏感的场合。

想清楚这些以后,才开始在RTL里写代码,否则很容易做完才发现选型错误。

7.4 与其他小数分频方案的对比

最后把N.5分频和几类常见的小数分频方案放在一起对比,方便根据项目需求选型。

方案分频比步进瞬时抖动实现成本适合场景
N.5分频(本文)0.5大(±0.5输入周期)低简单数字时钟、低速接口
Sigma-Delta小数分频高精度(任意小数)中,取决于量化噪声高频率合成、通信系统
双模分频+PLL任意小数(通过分频比序列)低(PLL滤除)高高性能时钟、RF
多相时钟沿选择0.5/N(多相细分)较小中高高速SerDes、精密时钟

N.5分频是建立在双模分频基础上的最简特例,理解了它的原理,再去看任意步进的小数分频会轻松很多。尤其是分频比序列和相位累加器的概念,对整个小数分频领域是通用的。

在我做过的几个项目里,最终真正用上N.5分频的其实不多,大多时候是作为PLL输出频率不足时的应急补充,或者用来产生一个测试用的非标准时钟。但每次调试它,我都能对整个时序设计的理解加深一层——特别是那种“平均正确但瞬时抖动”的微妙关系,会逼着你重新思考时钟树里每一个边沿的意义。如果你正准备在自己的项目里用N.5分频,建议先把本文第2节和第5节来回看几遍,做好仿真和实测的准备,再动手写代码。踩完坑之后你会发现,这个看似不起眼的小数分频,真的值得认真对待。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进