☰
FPGA实战:N.5分频器的Verilog实现与占空比优化
2026/10/15 17:58:45 网站建设 项目流程

做数字电路或者FPGA开发,迟早会碰到一个不大不小的问题:系统里只有 100MHz 的参考时钟,可某个模块就是点名要 40MHz。100 除以 40 等于 2.5,这不是整数分频能解决的事,于是就得用 N.5 分频。N.5 分频的核心难点不在于“数数”,而在于输出周期里那“半个周期”怎么记账,以及输出占空比怎么才能尽量接近 50%。这篇文章会从原理讲起,给出可参数化的 Verilog 实现、完整的仿真验证思路,再聊聊从仿真到上板综合时最容易踩的坑,适合正在学 FPGA 的初学者,也适合想把分频器做得更严谨的工程师参考。

我在项目里实际调过这类分频器,最开始的想法很简单:计数到一半翻转不就行了?结果一测占空比,发现根本不是 50%,而且频率也差着一截。后来把“上升沿和下降沿分开处理、把半个周期的余数轮换分摊”这套思路理清楚,问题才算真正解决。下面把这套方法完整拆开讲。

1. 先看清痛点:N.5分频和整数分频差在哪

1.1 整数分频为什么容易

先复习一下整数分频。分频比是偶数时,比如 4 分频、6 分频,只要用一个计数器数时钟上升沿,在计数到一半时翻转输出,高电平和低电平各占整数个输入周期,占空比就是严格的 50%。分频比是奇数时,比如 3 分频、5 分频,稍微麻烦一点,因为输出周期是奇数个输入周期,如果只盯上升沿,高电平和低电平永远不可能一样长。

奇数分频的常见做法是“双沿结合”,利用一个上升沿触发的计数器和一个下降沿触发的计数器,分别产生两路相差半个输入周期的中间信号,再通过组合逻辑合成最终输出。这样 3 分频可以做到高电平 1.5T、低电平 1.5T,占空比是准的。这个思路说穿了就是:既然一个完整的输出周期里有奇数个整数周期,那就把“多出来的半周期”作为半个输入周期分散到两侧去。

把奇数分频的思想再往前推一步,就是 N.5 分频的起点。奇数分频的余数是“半个周期”,而 N.5 分频的整个输出周期本身就是“整数个周期加半个周期”,本质上是同一个问题被放大了。

1.2 半个周期到底怎么记账

以 2.5 分频为例。分频比 D=2.5,意味着输出周期是输入周期的 2.5 倍。输入 100MHz、周期 T=10ns 时,输出周期应该是 25ns,对应 40MHz。一个完美的方波输出,高电平应该是 12.5ns,低电平也应该是 12.5ns。

问题就出在这个 12.5ns 上。数字分频器只能在输入时钟的边沿处翻转输出,边沿与边沿之间的最小间隔是半个输入周期,也就是 5ns。12.5ns 是 2.5 个半周期,不是整数个半周期,所以永远没办法让高电平和低电平在“单个输出周期里”各自正好是 12.5ns。这就是 N.5 分频和整数分频最本质的区别:量化粒度决定了它不可能像偶数分频那样,每个周期占空比都严格等于 50%。

那工程上怎么处理?两个字:均摊。既然单个周期里做不完美,那就让高电平这一次持续 10ns、下一次持续 15ns,平均下来是 12.5ns;低电平反过来,第一次 15ns、第二次 10ns,平均也是 12.5ns。两个输出周期一平均,占空比就回到了 50%。原理上很简单,难的是怎么把轮换顺序写对,并且保证长期统计下高电平总时间严格等于低电平总时间。

1.3 什么时候必须较真占空比

很多人觉得占空比差不多就行,反正数字电路只用上升沿。这话对一半。如果你的分频输出只是给一个普通逻辑时钟,下游全部是上升沿采样的同步逻辑,那 40% 和 60% 的占空比区别不大。但下面这几类场景里,占空比误差会直接变成性能损失:

  • I/Q 混频或者 I/Q 采样,通常同时使用时钟的上升沿和下降沿,占空比不均衡会让两路增益不一致,镜像抑制变差。
  • DDR 接口的 DQS 和数据采样依赖双沿,占空比失真直接压缩建立保持时间裕量。
  • 某些 ADC、DAC 的时钟输入对占空比敏感,非 50% 的占空比会引入偶次谐波和时钟泄漏。
  • 分频输出后面再接倍频器或者 DCC(占空比校正电路)时,输入占空比误差会被放大,校正负担变大。

所以 N.5 分频器设计里,“占空比尽量接近 0.5”不是一个可有可无的要求,而是实际工程里的硬指标。下面这个实现就是围绕这一点设计的。

2. 核心设计:半周期计数 + 相位轮换

2.1 把“半个周期”变成可计数的“事件”

要实现 N.5 分频,第一步是获得“半周期事件”。所谓半周期事件,就是输入时钟的每一次翻转,包括上升沿和下降沿。时钟每翻转一次,相当于经过了 0.5 个输入周期。如果能在每个半周期事件到来时给一个计数器加 1,那这个计数器记的就是“半周期个数”。

这样做的好处是,输出相位长度不再只能以整数个输入周期为单位,而是可以以半个输入周期为单位。2.5 分频的完整输出周期是 5 个半周期,高电平和低电平加起来必须是 5,这在高电平 2 个半周期、低电平 3 个半周期的组合下就能实现。

不过“半周期事件”在硬件上不是白给的。仿真里面可以直接写always @(posedge clk or negedge clk),但在 FPGA 综合时会遇到麻烦,这个问题留到第 4 节专门讲。这里先把原理层面的事情说透。

2.2 轮换模式 [N, N+1, N+1, N] 的由来

对于分频比 N.5,输出周期一共是 2N+1 个半周期。高电平和低电平各自的目标时长只能是整数个半周期,所以最接近的分配是:一个状态持续 N 个半周期,另一个状态持续 N+1 个半周期。

关键问题来了:怎么轮换。最容易想到的是每个输出相位都交替使用 N 和 N+1,也就是 N、N+1、N、N+1……这样高电平每次都是 N,低电平每次都是 N+1,平均下来高电平时长是 N,低电平时长是 N+1,占空比偏掉了。以 2.5 分频为例,这样写会得到高电平 10ns、低电平 15ns 的稳定输出,占空比 40%,永远回不到 50%。

正确做法是采用 4 个相位一组的轮换模式:N、N+1、N+1、N。高电平在两个输出周期里分别拿到一次 N 和一次 N+1,低电平也分别拿到一次 N+1 和一次 N,两边平均都是 N+0.5 个半周期。这样任意连续两个输出周期合并统计,占空比严格等于 50%。

写成 Verilog 表达式就是:

target = N + (phase_no[0] ^ phase_no[1]);

phase_no 是一个 2 位计数器,在每个输出相位结束时加 1,按 00、01、10、11、00……循环。取 phase_no[0] 和 phase_no[1] 的异或,得到的序列就是 0、1、1、0,对应 target 序列 N、N+1、N+1、N。这个小技巧比写 case 分支简单,也更容易被综合工具优化。

2.3 2.5分频的完整时序表

用 100MHz 输入、2.5 分频举例,输入周期 10ns,半周期 5ns。按上面的轮换模式,输出相位序列如下:

输出相位等级持续半周期数持续时间
0高210ns
1低315ns
2高315ns
3低210ns
4高210ns
5低315ns

从这个表可以看出来两个重要特性。

第一,输出上升沿到上升沿的间隔始终是 25ns,也就是输出周期恒定。单个输出周期由高电平和低电平相加得到,10ns 加 15ns、15ns 加 10ns,都是 25ns,频率一直是精准的 40MHz,不会出现周期抖动的累积误差。

第二,下降沿的位置在 10ns 和 15ns 之间来回摆。理想 50% 占空比的下降沿应该出现在 12.5ns,实际要么早 2.5ns、要么晚 2.5ns。这是确定性抖动,源于边沿量化粒度,丢掉这 2.5ns 的误差没有任何纯数字手段能做到更好。

2.4 占空比到底能多准

很多人看到 2.5 分频的单个周期占空比在 40% 和 60% 之间来回跳,会怀疑标题里“非常接近 0.5”的说法。这里要分清两个概念:瞬时占空比和平均占空比。

瞬时占空比,也就是单独看某一个输出周期,偏差是固定的。单个周期的理想占空比偏差可以通过公式算出来:

单周期占空比偏差 = 0.5 / (2N+1) = 1 / (4N+2)

N=2 时,偏差 10%;N=4 时,偏差约 5.6%;N=49 时,偏差已经小于 0.5%。所以当 N 比较大时,哪怕只看单个周期,占空比也确实非常接近 50%。

平均占空比,也就是把连续两个输出周期合并统计,这个更关键。由于轮换模式是 [N, N+1, N+1, N],高电平两个周期合计 2N+1 个半周期,低电平两个周期合计也是 2N+1 个半周期,平均占空比严格等于 50%。在纯数字分频方案里,这已经是最优解了。

不同分频比的占空比表现整理如下:

分频比输出周期(半周期)短相位(半周期)长相位(半周期)平均占空比
2.552350%
3.573450%
4.594550%
10.521101150%

平均占空比做到严格 50%,对很多应用来说已经足够了。如果下游真的要求每一个输出周期的占空比都逼近 50%,那就要在分频器后面再接模拟的 Duty Cycle Correction 电路,或者干脆换用 PLL/DLL 方案,这已经不属于纯数字分频的讨论范围。

3. Verilog实现与仿真验证

3.1 可参数化的RTL代码

下面这个模块把上面的思路直接实现出来。N 是参数,例化时传入 2,就是 2.5 分频;传入 3,就是 3.5 分频。

// N.5 分频器,分频比 = N + 0.5 module n5_divider #( parameter integer N = 2 )( input wire clk, input wire rst_n, output reg clk_out ); // 半周期事件检测:clk 每翻转一次,hp_evt 拉高一个脉冲 reg clk_d; wire hp_evt; always @(posedge clk or negedge clk) begin if (!rst_n) clk_d <= 1'b0; else clk_d <= clk; end assign hp_evt = clk ^ clk_d; // 半周期计数器与相位模式 reg [7:0] hp_cnt; reg [1:0] phase_no; wire [7:0] target = N + (phase_no[0] ^ phase_no[1]); always @(posedge clk or negedge clk) begin if (!rst_n) begin hp_cnt <= 0; clk_out <= 1'b0; phase_no <= 2'b00; end else if (hp_evt) begin if (hp_cnt == target - 1) begin hp_cnt <= 0; clk_out <= ~clk_out; phase_no <= phase_no + 1'b1; end else begin hp_cnt <= hp_cnt + 1'b1; end end end endmodule

代码逻辑分两层。第一层是半周期事件检测,clk_d保存上一次时钟电平,clk ^ clk_d在每次时钟翻转时输出 1。第二层是主状态机,hp_cnt在每次半周期事件时加 1,加到target-1就翻转输出、清零计数器、推进相位模式。

有一个细节要注意:target表示当前输出相位要持续的半周期个数,而hp_cnt是从 0 开始计数的,所以判断条件是hp_cnt == target - 1,不是hp_cnt == target。我见过不少初学的人在这里差一个数,结果分频比变成 N+1.5 或者 N-0.5,整个波形全错。

另外hp_cnt的位宽我给了 8 位,覆盖到 255 个半周期,也就是分频比最大能到 127.5。如果需要的 N 更大,记得用$clog2(N+1)计算位宽,不要照抄。

3.2 测试平台怎么写

写个简单的 testbench,验证频率和占空比:

module tb_n5_divider; reg clk = 0; reg rst_n = 0; wire clk_out; always #5 clk = ~clk; // 100MHz n5_divider #(.N(2)) u_div ( .clk (clk), .rst_n (rst_n), .clk_out(clk_out) ); integer rise_cnt = 0; integer fall_cnt = 0; always @(posedge clk_out) rise_cnt = rise_cnt + 1; always @(negedge clk_out) fall_cnt = fall_cnt + 1; // 统计一段时间内高电平总时间和低电平总时间 real high_time, low_time, duty; real last_edge_time; reg last_level; always @(posedge clk_out or negedge clk_out) begin if ($realtime != 0) begin if (last_level == 1'b0) high_time = high_time + ($realtime - last_edge_time); else low_time = low_time + ($realtime - last_edge_time); end last_edge_time = $realtime; last_level = clk_out; end initial begin repeat(3) @(posedge clk); rst_n = 1; repeat(2000) @(posedge clk); duty = high_time / (high_time + low_time); $display("rise_cnt=%0d fall_cnt=%0d", rise_cnt, fall_cnt); $display("duty=%.6f", duty); $finish; end endmodule

测试平台里有几个测量点。

第一是边沿计数。2000 个输入时钟周期是 20us,2.5 分频输出 40MHz,理想情况下 rise_cnt 应该是 800,fall_cnt 也应该是 800。如果 rise_cnt 和 fall_cnt 不相等,说明输出周期的总数和相位结构有问题。

第二是高低电平总时间统计。在每一个输出边沿处,把上一段电平的持续时间累加到对应变量里。仿真跑完 20us,high_time 和 low_time 应该严格相等,duty 打印出来是 0.500000。如果 duty 不是 0.5,就要回头检查 phase_no 的轮换逻辑。

3.3 仿真波形怎么读

打开波形,按下面几个顺序检查。

先看输出周期。用光标量两个相邻上升沿的间隔,2.5 分频输入 100MHz 时应该是 25ns,而且连续量几个都要是 25ns。这个验证的是分频比本身。

再看占空比。量上升沿到紧跟的下降沿,会轮流得到 10ns 和 15ns。这不是 bug,是设计预期。然后量下降沿到下一个上升沿,会反过来得到 15ns 和 10ns。高电平和低电平的总时间通过长时间平均才能看出 50%,单看一个周期看不出来。

最后看抖动。以输出上升沿为参考,下降沿的绝对位置在理想 12.5ns 的左右各偏移 2.5ns,这个偏移量是固定的确定性抖动。如果偏移量超过了 2.5ns,说明计数器有问题或者相位轮换不是按 4 状态跑的。

4. 从仿真到上板:双沿计数怎么落进FPGA

4.1 为什么不能直接综合

前面给的 RTL 里有两个always @(posedge clk or negedge clk)块。仿真器跑这种代码完全没有问题,但 FPGA 综合工具通常会直接报错或者给你一个用不了的电路。原因是 FPGA 的寄存器本质上是单沿触发的,一个寄存器不可能同时被上升沿和下降沿触发。虽然少数器件上有 DDR 输出寄存器可以近似实现双沿采样,但那不是通用的寄存器资源。

有些朋友图省事,把两个 always 块拆开,一个写上升沿、一个写下降沿,然后想通过组合逻辑合并结果。这样做很容易造成多驱动竞争,或者出现毛刺和亚稳态问题。我的建议是不要在双沿触发这条路上死磕,换一种工程上更干净的方案。

4.2 最省事:PLL产生2倍时钟后再做同款计数

既然半周期事件不好直接检测,那就干脆把输入时钟倍频一下,用 PLL/MMCM 生成一个 2 倍频时钟clk_x2。原来的一个输入周期等于两个clk_x2周期,原来的一次上升沿和一次下降沿,在clk_x2里都是上升沿。这样“半周期计数”就变成了普普通通的整数周期计数,状态机不变,但整个模块变成了单沿综合,任何 FPGA 都能直接跑。

// 用 2 倍频时钟实现 N.5 分频 module n5_divider_pll #( parameter integer N = 2 )( input wire clk_x2, // 2 x 原始时钟 input wire rst_n, output reg clk_out ); reg [7:0] hp_cnt; reg [1:0] phase_no; wire [7:0] target = N + (phase_no[0] ^ phase_no[1]); always @(posedge clk_x2 or negedge rst_n) begin if (!rst_n) begin hp_cnt <= 0; clk_out <= 1'b0; phase_no <= 2'b00; end else begin if (hp_cnt == target - 1) begin hp_cnt <= 0; clk_out <= ~clk_out; phase_no <= phase_no + 1'b1; end else begin hp_cnt <= hp_cnt + 1'b1; end end end endmodule

这段代码和原理版本的唯一区别就是没有clk ^ clk_d的边沿检测,直接以clk_x2的上升沿作为半周期事件。2.5 分频的 N=2,module 里所有行为一模一样,输出频率和占空比表现也完全一致。

工程上这个方案最稳。只要系统里本来就有 PLL 资源,倍频加分频的成本很低,而且综合、时序收敛都好做。

4.3 没有PLL可用:ODDR/DDR原语

如果系统里没有 PLL,或者约束不允许额外倍频,最后一招是用厂商的 DDR 输出原语(比如 Xilinx 的 ODDR、Altera 的 ALTDDIO_OUT)来构建一个双沿触发的翻转寄存器。原理是把输出分成上下两路,各自在上升沿和下降沿更新,再用原语拼出双沿输出。这块代码和器件绑定,不同厂商写法不同,而且需要读原语的时序模型。除非确实没有 PLL,否则我不推荐普通项目里这么干,调试成本不低。

5. 常见问题与调试实录

下面这些坑,大部分我在调 N.5 分频器时都亲自踩过,整理出来给各位当速查表。

现象可能原因解决办法
输出频率正好是期望值的一半把输出周期 2N+1 个半周期当成了单个相位的目标值phase target 应该是 N 或 N+1,不是 2N+1
占空比固定在 40%,不是 50%phase_no 轮换写成了 N、N+1、N、N+1用 4 状态轮换 [N, N+1, N+1, N]
复位后的前几个周期波形异常phase_no 初始状态没有对齐复位时把 phase_no 清零,并且不计复位后的第一个周期
上板后输出有毛刺输出 q 用了组合逻辑直接赋值输出信号必须用寄存器驱动,不要用 assign 从比较器取
综合报 dual-edge 或 multi-driven 错误直接在 always 块里写 posedge/negedge换成 PLL 2 倍频方案,或者用 DDR 原语
分频比 N 较大时计数溢出hp_cnt 位宽不够用 $clog2(N+1) 计算位宽
统计占空比不是 50%测量窗口只覆盖了奇数个输出周期的前半段至少统计连续两个完整输出周期,或者用长时间平均

这里重点说两个容易忽略的细节。

一个是复位后的初始相位。复位时clk_out强制为 0,phase_no为 00,所以复位后的第一个高电平相位是 2 个半周期。如果你在系统上电后立刻测量第一个输出周期,看到的占空比可能是 40%。这不是设计错误,是初始相位选择导致的瞬态表现。正常使用时,让系统先稳定几个输出周期再启用采样或锁存逻辑。

另一个是hp_cnt == target - 1这个比较条件里的位宽问题。target是 wire 类型,如果你把 N 声明成 integer,而 hp_cnt 声明成 8 位 reg,综合工具在比较时会自动扩展位宽,一般没问题。但我建议把 N 也显式写成常量参数,并且把 target 定义成和 hp_cnt 同宽的有符号或无符号向量,避免位宽不匹配带来的仿真警告。

6. 把N.5推广到任意小数分频

N.5 分频其实是更一般的小数分频的特例。任意小数分频比 D = I + F,其中 I 是整数部分,F 是小数部分,经典做法是双模预分频加相位累加器:大部分输出周期按 I 分频,每隔一段时间按 I+1 分频,I+1 出现的频率由 F 控制。

具体做法是维护一个累加器acc,每个输出周期执行:

acc = acc + F; if (acc >= 1) begin 当前周期分频比为 I+1; acc = acc - 1; end else begin 当前周期分频比为 I; end

当 F=0.5 时,acc 的序列是 0.5、1、0.5、1……也就是说每隔一个输出周期切一次模,对应到分频比就是 I、I+1、I、I+1。如果这时候再结合半周期的边沿处理,就回到了我们前面讲的 N.5 分频。F 是 0.3、0.7 这类非 0.5 的值时,切换序列不再这么简单,通常需要 sigma-delta 调制器来打散误差,避免能量集中在特定频点上,那已经是小数分频锁相环的领域了。

我这里讲的半周期计数方案,本质上也是一种“边沿域”的相位累加:把余数 0.5 个半周期放在 4 个相位里均摊,只不过因为 N.5 的余数恰好是 0.5,轮换序列非常规律,用一个小状态机就能搞定。

结尾再分享一点个人经验。我最早做这个分频器时,死磕双沿触发,觉得 PLL 倍频是偷懒。后来在 FPGA 上综合失败好几次,才明白工程方案和原理方案本来就是两回事:原理用双沿计数讲得最清楚,工程用 PLL 倍频实现最稳。如果让我给建议,我会说先花半小时把轮换模式的时序表画明白,再决定用哪种方案落地,别上来就写代码。状态机的 target 序列一旦画错,后面所有调试都是在浪费时间;画对了,仿真一遍过,上板也是一遍过的概率很高。

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

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

立即咨询