ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

FPGA实现N.5分频:原理、Verilog代码与占空比控制

FPGA实现N.5分频:原理、Verilog代码与占空比控制

做数字逻辑和FPGA的同行应该都有这种经历:系统里明明给的参考时钟是100MHz,外设却需要40MHz或者66.67MHz这种非整数频率。整数分频好办,计数器一数就完事,但遇到“频率比是2.5、4.5、7.5”这种带小数的情况,很多人的第一反应是上PLL。PLL确实能搞定,可有些场景下锁相环成本高、功耗大、锁定时间又长,一个纯数字的N.5分频器反而更合适。

N.5分频是小数分频里最典型也最基础的一档。频率关系是 f_out = f_in / (N + 0.5),也可以写成 f_out = 2·f_in / (2N + 1)。这里头有个反直觉的地方:只用整数个输入时钟周期做交替计数,根本得不到N.5分频,做出来的其实是另一个整数分频;真正要落地,必须把分辨率细化到“半个输入时钟周期”。本文就想把这个原理讲透,给出可综合的Verilog实现,并重点讨论标题里那个“占空比非常接近0.5”——到底怎么来的,又该怎么控制。适合正在学FPGA、或者想在项目里脱离PLL做小数分频的硬件工程师。

1. N.5分频:为什么说带有特殊难度

1.1 数字分频器的基本约束

任何数字分频器都有一个绕不开的物理约束:输出信号发生跳变的时刻,只能落在输入时钟的边沿上。这听起来像废话,但对N.5分频来说,它是一切麻烦的根源。

普通整数分频比如4分频,输出周期是4个输入周期,高低电平各占2个输入周期,占空比天然就是50%,逻辑也非常简单:计数器从0数到3,数到边界就翻转。因为输出周期被切成两个等长的整数段,上升沿和下降沿都能对齐输入边沿。

N.5分频就不一样了。输出周期 T_out = (N + 0.5)·T_in,换算一下就是 (2N + 1)·T_in / 2。这里出现了“半个输入周期”。

假设上升沿落在某个输入上升沿上,那么下降沿如果要精确落在 (2N+1) 个半周期之后,它就得落在“输入下降沿”或者“输入上升沿”上,取决于奇数还是偶数。这就意味着,一个正确的N.5分频输出,它的两个边沿通常要分别和输入时钟的两个不同相位对齐。换句话说,你没法只用一个“上升沿计数”的计数器把这事干完。

1.2 一个输出周期里有奇数个半周期

更直白的表述:N.5分频下,一个输出周期包含的“边沿间隔数”是 2N+1 个半周期,这是个奇数。

打个比方,普通整数分频像是用一把只能量整厘米的尺子去量一段“整厘米”的长度,量出来的高低电平都是整数段,自然均分。N.5分频像是要用同一把尺子去量一段“X.5厘米”的长度,高低电平想均匀分配,必然有一半要踩在半厘米刻度上。如果坚持只用整厘米刻度去分配,要么高电平多出半厘米,要么低电平多出半厘米,不可能两个都严丝合缝。

这个“奇数个半周期”的结论有非常实际的指导意义——它告诉我们,占空比要做到数学上严格的50%,光靠“半个输入周期”这个最小分辨率是不够的,因为总量是奇数,不可能分成两个相等的整数。所以标题里的“非常接近0.5”是有讲究的,它指的是长期平均意义上的接近,或者说在有更高频时钟辅助的条件下,把分辨率进一步细化到四分之一输入周期,才能真正做到每个周期都是精确的50%。

1.3 工程里什么时候需要N.5分频

有人可能问,PLL这么普及,为什么还要纯数字N.5分频?我实际遇到的场景大致有这几类:

第一类是时钟精度要求不高、但切换速度要求快的场合。PLL锁定需要时间,动辄几十上百微秒,某些协议切换或测试模式下等不起。数字分频器从输入到输出只有几个寄存器的传播延迟,边沿立刻就能对上。

第二类是低成本系统,没有PLL资源或者不想为一个慢速时钟占用PLL。比如一个低速UART波特率时钟、一个传感器采样节拍,用N.5分频就够了,没必要动用高精度的模拟锁相环。

第三类是需要在多个小数分频之间动态切换的场景。比如某个数字信号处理链需要2.5、3.5、4.5分频来回变,PLL的重新配置和锁定时间很难满足,而参数化N.5分频器只需换两个常数,一个周期内就能生效。

这类需求看着小,但一旦实现错了,输出频率不对、占空比难看,下游采样逻辑就会被带偏。接下来先看一个常见但错误的实现,把坑讲明白。

2. 最容易写错的实现:N和N+1交替

2.1 “一个周期N、一个周期N+1”为什么不对

很多初学者拿到N.5分频,第一反应是:既然分频比是N.5,那就让计数器一次按N分频、下一次按N+1分频,交替执行,平均下来不就是N.5了吗?

我们实际推一下。假设N=2,目标2.5分频。先按2分频计数,计数器数到2个输入周期后翻转输出;再按3分频计数,数到3个输入周期后再翻转输出。

问题出在“翻转输出”这个动作上。第一次翻转后,高电平(或者低电平)持续了2个输入周期;第二次翻转后,另一半持续了3个输入周期。一个完整输出周期就是2+3=5个输入周期,这不是2.5分频,而是5分频。

我再把逻辑说直白一点:输出边沿之间的间隔,被设成了2个输入周期和3个输入周期,这两个间隔加在一起构成一个输出周期,总长5个输入周期,分频比是5。想要2.5分频,边沿间隔应该是“1.5个输入周期和1个输入周期”的组合,或者等价的“3个半周期和2个半周期”的组合。用整数个完整输入周期做间隔,根本拼不出N.5。

2.2 双模吞脉冲:平均频率对,但稳定度差

有人会指出,传统小数分频确实是用“N分频和N+1分频交替”实现的,比如双模前置分频器加吞脉冲计数器。这种说法本身没有错,但必须注意它和我们这里讨论的不是一回事。

传统的双模分频,是在一个很长的平均周期里,让分频器大部分时间工作在N分频,偶尔切到N+1分频,从而把边沿密度凑到平均值N.5。输出的时钟不是一个周期一个周期地等于N.5个输入周期,而是有些输出周期是N个输入周期,有些是N+1个输入周期。瞬时频率在抖,只有长时间平均后才是N.5分频。

这种实现适合做频率合成器的反馈分频器,因为锁相环本身有环路滤波器,能容忍这种边沿抖动。但如果直接拿这个输出当时钟用,下游的时序分析就很难做了:每个周期长度不一样,建立时间和保持时间都没法按固定值算。

所以我一般把这两种需求分开:只要平均频率、对瞬时周期不敏感的,可以用双模吞脉冲;需要输出时钟本身周期稳定、占空比还接近50%的,就要用下面这种半周期计数的方案。

2.3 问题根源:缺了半周期分辨率

前面两个小节其实指向同一个结论:N.5分频的难点不在“平均分频比”,而在“输出周期的瞬时长度”。

要做出一个周期严格等于 (2N+1)/2 个输入周期的方波,输出边沿必须能落在输入时钟的下降沿上。而普通寄存器只认上升沿(或下降沿),你用上升沿触发计数器,就只能整周期地翻转,永远切不出半周期的间隔。

这就像一个只能用奇数的加法器去凑偶数,再怎么调整顺序也凑不出来,除非你先引入一个能表示奇数的量。半周期就是这个“奇数”,它是解决N.5分频的核心钥匙。

3. 半周期计数:N.5分频的正解

3.1 把输入时钟看成一条边沿链

要做半周期计数,最简单的思路是先把输入时钟虚拟地看成一条由“上升沿、下降沿、上升沿、下降沿……”组成的连续脉冲链。这条脉冲链上的每个事件之间,间隔正好是半个输入周期。

如果在每个事件上都做一次计数,那计数器累加的自然就是半周期数。设计目标就变成了:输出每经过N个半周期翻转一次,下一次经过N+1个半周期再翻转一次,如此交替。

这样每个输出周期 = N + (N + 1) = 2N + 1 个半周期,正好是(N + 0.5)个输入周期。瞬时频率完全正确,每个周期都一样长,不会出现双模吞脉冲那种忽长忽短。

用2倍频时钟是最省事的方式:clk_x2的频率是输入时钟的两倍,它的上升沿天然对应输入时钟的每个边沿。计数器在clk_x2下工作,每计数一个周期就代表输入时钟过了半个周期。

3.2 短间隔与长间隔的定义

把参数先定好:

  • 短间隔 SHORT = N 个半周期
  • 长间隔 LONG = N + 1 个半周期
  • 一个完整输出周期 = SHORT + LONG = 2N + 1 个半周期

控制逻辑用一位状态sel来选当前该用哪个间隔。sel=0时按SHORT计数,计数到边界翻转输出并切到sel=1;sel=1时按LONG计数,计数到边界翻转输出并切回sel=0。

这里有个容易混淆的细节:SHORT和LONG指的是“两次输出翻转之间的间隔长度”,不是低电平和高电平各自的规律。在2.5分频例子里,SHORT=2个半周期(等价于1个输入周期),LONG=3个半周期(等价于1.5个输入周期)。所以输出信号的半周期不是平均分布的,而是短长交替。

3.3 2.5分频的完整走查

拿N=2的2.5分频走一遍时序:

初始状态:输出out=0,sel=0,计数器cnt=0。

  • sel=0,cnt数到SHORT-1=1时,cnt已经是第二个半周期,于是翻转out,out从0变1,sel切到1。此时低电平持续了2个半周期,即1个输入周期。
  • sel=1,cnt从0数到LONG-1=2,也就是3个半周期后,第二次翻转out,out从1变0,sel切回0。此时高电平持续了3个半周期,即1.5个输入周期。
  • 下一个周期重复,低电平2个半周期,高电平3个半周期。

于是输出周期 = 2 + 3 = 5个半周期 = 2.5个输入周期,频率正好是输入的40%。上升沿到下一个上升沿的距离始终是2.5个输入周期,周期稳定。占空比方面,这个周期里高电平3半周期、低电平2半周期,占空比60%;但下一个周期因为sel回到0,低电平又是3半周期、高电平2半周期,占空比40%。两个周期一平均,恰好50%。

这就是“占空比非常接近0.5”的来源:单个周期可能有偏差,但两个周期的平均值是严格的50%。N越大,单个周期的偏差1/(2N+1)也越小。

3.4 占空比偏差的定量分析

每个输出周期总长度是2N+1个半周期,理想50%要求高电平和低电平各占(2N+1)/2个半周期。但2N+1是奇数,实际高低电平只能是整数个半周期,必然一个多半个、一个少半个。

如果高电平是LONG=N+1、低电平是SHORT=N,那么这个周期的占空比是:

duty = (N+1) / (2N+1)

下一个周期高电平和低电平互换:

duty = N / (2N+1)

单个周期的相对偏差是:

Δ = |duty - 0.5| = 1 / (2(2N+1))

代入几个N值:

N分频比输出周期(半周期)单周期占空比范围偏差
11.5333.3% / 66.7%16.7%
22.5540% / 60%10%
44.5944.4% / 55.6%5.6%
1010.52147.6% / 52.4%2.4%

这个表很有用。它说明了两件事:N越大,单周期占空比越接近50%;但N比较小的时候,比如1.5或2.5分频,要想每个周期都接近50%,光靠半周期计数是不够的,得用下一节的4倍频方案。

4. RTL实现与参数化代码

4.1 基于2倍频时钟的N.5分频模块

下面是方案一的Verilog实现,思想就是3.2节里讲的短长间隔交替。clk_x2是输入时钟的2倍频,可以由PLL产生,也可以由其他高频主时钟代替。

module div_n_half #( parameter N = 2 )( input wire clk, // 原始输入时钟 input wire clk_x2, // 2倍频时钟,作为半周期计数时钟 input wire rst_n, output reg out ); localparam SHORT = N; // 短间隔:N 个半周期 localparam LONG = N + 1; // 长间隔:N+1 个半周期 reg [7:0] cnt; reg sel; // 0:按SHORT计数, 1:按LONG计数 always @(posedge clk_x2 or negedge rst_n) begin if (!rst_n) begin cnt <= 0; sel <= 0; out <= 1'b0; end else begin if (cnt == (sel ? LONG : SHORT) - 1) begin cnt <= 0; sel <= ~sel; out <= ~out; end else begin cnt <= cnt + 1; end end end endmodule

这段代码的核心就一个判断:cnt到没到当前模式的边界,到了就翻转输出并且切换模式。

有一点要注意:out在clk_x2的上升沿翻转,而clk_x2是输入时钟的2倍频,所以out的翻转实际发生在clk边沿之后的clk_x2沿上,跟输入时钟会有固定的半周期对齐关系。PLL输出和输入时钟之间的相位关系,决定了out上升沿相对clk的相位,但这个相位是固定的,不影响频率和占空比。

4.2 参数N的位宽与安全设计

示例代码里cnt用了固定8bit,参数N建议别超过200,否则计数器的位宽不够。更稳妥的写法是用$clog2自动计算:

localparam CNT_W = $clog2(N + 1); reg [CNT_W:0] cnt;

多留一位是为了防止sel切换时边界比较出现溢出。

还有一个容易被忽视的细节:复位后的初始状态。sel初始为0,out初始为0,所以复位后第一个输出周期一定是“低电平SHORT、高电平LONG”。如果你的下游电路对第一个周期的相位没有要求,那无所谓;如果要求输出必须是高电平开头,可以把out初值改成1,或者给sel赋1,让序列从长间隔开始。这个完全看系统需求。

4.3 关于“2倍频时钟从哪来”的实际选择

FPGA里最干净的办法是用PLL/MMCM把输入时钟2倍频,然后让分频器在clk_x2域里工作。PLL输出和输入之间的相位关系,常规配置下是有界且固定的,满足分频逻辑的建立保持时间没有问题。

没有PLL的时候,有些教程会建议用异或门加延迟线把两个边沿“拼”成一个2倍频时钟。这个在仿真里能跑通,但放到FPGA布线里,门延迟不受控制,很容易产生毛刺和竞争冒险,我不建议在实际项目里这么干。如果系统里本来就有一个比输入频率高很多的主时钟,比如MCU的外设时钟或者SerDes的参考时钟,也可以直接用那个高频时钟做半周期计数,只要它和输入时钟之间有确定的相位关系就行。

4.4 只关心上升沿时,这个方案的“完美”特性

再强调一个容易被忽略的优点:方案一的输出上升沿间隔严格等于N.5个输入周期,周期抖动为零。占空比变化只影响下降沿的位置,不影响上升沿。

很多下游电路,比如计数器、分频链、事件触发逻辑,只认一个沿,那么方案一其实已经是“完美”的N.5分频。只有那些同时用上升沿和下降沿的逻辑(比如DDR采样、双边沿触发器),才会真正在乎占空比的瞬时偏差。这也是我判断“到底要不要上4倍频方案”的一个依据。

5. 追求精确50%占空比:4倍频后整数分频

5.1 为什么4倍频刚好够

搞定了半周期,还要回答一个问题:如果应用要求每个输出周期的占空比都精确50%,怎么办?答案是把计数分辨率继续细化到四分之一输入周期。

用4倍频时钟clk_x4做计数时钟。N.5分频的输出周期如果不换算成输入周期,而是换算成clk_x4的周期数,就是:

T_out = (N + 0.5) × 4 = 4N + 2

这是一个偶数。既然输出周期是偶数个计数时钟周期,那高电平和低电平就可以各占一半,也就是2N+1个clk_x4周期。每个输出周期高低严格相等,占空比精确50%,没有任何周期和占空比抖动。

为什么2倍频不行?因为2倍频下输出周期是2N+1,奇数,没法均分。4倍频则把总量翻成了偶数。如果你用8倍频、16倍频,同样能精确50%,只是逻辑频率更高,没这个必要。4倍频是最低成本的精确解。

我把这个套路叫作“倍频到偶数周期再整数分频”。它本质上不是在小数分频,而是把一个带0.5的频率关系放大到整数域里做整数分频。

5.2 精确50%占空比的Verilog实现

module div_n_half_exact #( parameter N = 2 )( input wire clk_x4, // 4倍频时钟 input wire rst_n, output reg out ); localparam HALF_PERIOD = 2 * N + 1; // 半个输出周期内的clk_x4周期数 reg [7:0] cnt; always @(posedge clk_x4 or negedge rst_n) begin if (!rst_n) begin cnt <= 0; out <= 1'b0; end else begin if (cnt == HALF_PERIOD - 1) begin cnt <= 0; out <= ~out; end else begin cnt <= cnt + 1; end end end endmodule

这个模块看代码就是一个普通的D分频器,D = 2 × (2N+1) = 4N+2。相对于clk_x4,它做的是(4N+2)分频;相对于原始时钟clk,因为clk_x4是clk的4倍频,所以等效于(N+0.5)分频。输出高低电平各占2N+1个clk_x4周期,占空比严格50%。

5.3 实际工程里怎么选

方案一和方案二之间没有绝对好坏,取决于资源和需求:

如果N较大(比如10以上),方案一的单周期占空比已经在47.6%到52.4%之间,很多应用完全能接受,没必要多用一个PLL倍频。如果N很小(比如1.5、2.5分频),或者下游逻辑对占空比非常敏感,那就老老实实走方案二。

我处理过的一个模拟前端给采样保持电路供时钟的场景,就对占空比要求苛刻。那种情况下我不会用方案一,直接PLL出4倍频,然后整数分频,高电平宽度的误差能控制在PLL抖动范围内,比任何纯数字半周期方案都干净。

还有一点:方案二因为计数时钟频率高,计数逻辑工作在4倍输入频率上,功耗会高一些。如果输入频率本身非常高,比如2.5GHz输入、要做2.5分频,那clk_x4就是10GHz,这个数字逻辑根本跑不动。这种情况只能退回去用方案一,或者考虑模拟分频器。数字分频器的频率上限始终是绕不开的坎。

6. 工程现场的坑与排查经验

6.1 复位和初始相位

N.5分频器上电之后,sel随机,输出第一个周期的长短是随机的。很多项目在系统启动时要的是一个确定的时钟相位,比如ADC的采样时钟希望第一个采样点对齐某个外部触发,这就需要复位逻辑来保证初始状态。

做法很简单:分频器复位信号要和输入时钟同步释放,确保rst_n离开低电平的那个时刻离clk沿有足够的建立时间。如果在FPGA里,建议把复位信号打两拍再送到分频器,避免异步复位释放时的亚稳态。

还有一种常见需求:多个相同的N.5分频器要输出同相位时钟。此时所有分频器的sel和cnt要一起被复位到相同值,并且clk_x2的相位关系也要一致。多一个PLL输出路径的延迟差都要注意。

6.2 用示波器测占空比,容易被探针骗

调试N.5分频器时,最直观的手段就是示波器看波形。但占空比测量有个隐蔽的问题:示波器探头本身有输入电容,会拉缓信号的上升下降沿,测量值和真实值可能有偏差,频率越高偏差越大。

我踩过的坑是:同一路输出,用无源探头测得占空比是52%,换有源探头变成49.8%,两个结果差了两个百分点。原因就是无源探头带宽不够、电容负载大。要评价N.5分频的占空比,至少要保证探头带宽是被测信号频率的5倍以上,最好用有源探头或者差分探头。

如果只是验证数字逻辑,也可以不用示波器:在FPGA里把输出信号和另一路已知时钟做与/或,再用计数器测高电平宽度。这种方法能测出几个ns级别的宽度差,对确认分频器行为足够了。

6.3 下游电路对占空比失真的容忍度

用方案一时,2.5分频的单周期占空比是40%或60%,有人看到这个数字会慌,觉得是不是错了。实际上只要下游电路只在上升沿采样,完全没关系。比如分频后只用来产生使能脉冲、驱动计数器、做串行位时钟,占空比不是关键参数。

真正要小心的是两类电路:一类是DDR这种双边沿采样,占空比失真会直接影响建立保持裕量;另一类是模拟采样保持电路,保持时间随着占空比变短会变差,影响采样精度。遇到这两类,直接上方案二,别想着用精细的布线去补偿,省不了多少事。

6.4 从N.5走向任意小数分频

理解了N.5分频,再往任意小数分频走就容易了。比如N.25分频,核心思路相同:输出周期是(4N+1)个四分之一输入周期,也是奇数,需要再往上加分辨率,用8倍频甚至更高。

实际工程里还有一个变体:先用N.5分频器配合一个后置的分频器,拼出更复杂的小数比。比如先做2.5分频,再做3分频,总的分频比是7.5。这种串联结构也很常见。

如果你需要任意小数分频且对周期抖动不敏感,可以换用相位累加器加Sigma-Delta调制,那是另一个话题。但不管哪种方案,“边沿必须对齐某种频率的整数倍”这个底层逻辑是不变的。

7. 一点个人的经验小结

写了这么多,最后聊点直接的体会。

N.5分频不是个高深的技术,但网上资料很多都在讲“N和N+1交替”的平均小数分频,导致初学者总是绕不开“为什么频率不对”的困惑。我自己也在这个问题上懵过一阵子。后来想通一句话:N.5分频的实质是“输出周期里有奇数个半周期”,所有实现技巧都是围绕怎么去处理这个半周期展开的。

如果只能记住一个操作要点,那就是:要占空比接近50%,优先保证计数粒度是半周期;要严格50%,就倍频到偶数周期再做整数分频。方案一适合N较大、或者下游只看单沿的场景;方案二适合小N、或者模拟电路对占空比敏感的场合。

还有个小技巧值得分享:如果输出只需要一个稳定边沿,例如只需要上升沿,别纠结占空比是不是50%,直接把方案一的输出用在与门/或门里,当作使能或者节拍信号,往往能省一个PLL。做硬件设计,永远是在精度、成本和复杂度之间做取舍,搞清楚“到底哪一边沿才重要”,比死磕一个理论上的完美占空比更实际。

RELATED READING

延伸阅读

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