FPGA实战:N.5分频器的Verilog实现与占空比优化

发布时间:2026/10/15 17:59:09

FPGA实战:N.5分频器的Verilog实现与占空比优化 做数字电路或者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 分频为例。分频比 D2.5意味着输出周期是输入周期的 2.5 倍。输入 100MHz、周期 T10ns 时输出周期应该是 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, N1, N1, N] 的由来对于分频比 N.5输出周期一共是 2N1 个半周期。高电平和低电平各自的目标时长只能是整数个半周期所以最接近的分配是一个状态持续 N 个半周期另一个状态持续 N1 个半周期。关键问题来了怎么轮换。最容易想到的是每个输出相位都交替使用 N 和 N1也就是 N、N1、N、N1……这样高电平每次都是 N低电平每次都是 N1平均下来高电平时长是 N低电平时长是 N1占空比偏掉了。以 2.5 分频为例这样写会得到高电平 10ns、低电平 15ns 的稳定输出占空比 40%永远回不到 50%。正确做法是采用 4 个相位一组的轮换模式N、N1、N1、N。高电平在两个输出周期里分别拿到一次 N 和一次 N1低电平也分别拿到一次 N1 和一次 N两边平均都是 N0.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、N1、N1、N。这个小技巧比写 case 分支简单也更容易被综合工具优化。2.3 2.5分频的完整时序表用 100MHz 输入、2.5 分频举例输入周期 10ns半周期 5ns。按上面的轮换模式输出相位序列如下输出相位等级持续半周期数持续时间0高210ns1低315ns2高315ns3低210ns4高210ns5低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 / (2N1) 1 / (4N2)N2 时偏差 10%N4 时偏差约 5.6%N49 时偏差已经小于 0.5%。所以当 N 比较大时哪怕只看单个周期占空比也确实非常接近 50%。平均占空比也就是把连续两个输出周期合并统计这个更关键。由于轮换模式是 [N, N1, N1, N]高电平两个周期合计 2N1 个半周期低电平两个周期合计也是 2N1 个半周期平均占空比严格等于 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 1b0; 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 1b0; phase_no 2b00; end else if (hp_evt) begin if (hp_cnt target - 1) begin hp_cnt 0; clk_out ~clk_out; phase_no phase_no 1b1; end else begin hp_cnt hp_cnt 1b1; 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。我见过不少初学的人在这里差一个数结果分频比变成 N1.5 或者 N-0.5整个波形全错。另外hp_cnt的位宽我给了 8 位覆盖到 255 个半周期也就是分频比最大能到 127.5。如果需要的 N 更大记得用$clog2(N1)计算位宽不要照抄。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 1b0) 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 个输入时钟周期是 20us2.5 分频输出 40MHz理想情况下 rise_cnt 应该是 800fall_cnt 也应该是 800。如果 rise_cnt 和 fall_cnt 不相等说明输出周期的总数和相位结构有问题。第二是高低电平总时间统计。在每一个输出边沿处把上一段电平的持续时间累加到对应变量里。仿真跑完 20ushigh_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. 从仿真到上板双沿计数怎么落进FPGA4.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 1b0; phase_no 2b00; end else begin if (hp_cnt target - 1) begin hp_cnt 0; clk_out ~clk_out; phase_no phase_no 1b1; end else begin hp_cnt hp_cnt 1b1; end end end endmodule这段代码和原理版本的唯一区别就是没有clk ^ clk_d的边沿检测直接以clk_x2的上升沿作为半周期事件。2.5 分频的 N2module 里所有行为一模一样输出频率和占空比表现也完全一致。工程上这个方案最稳。只要系统里本来就有 PLL 资源倍频加分频的成本很低而且综合、时序收敛都好做。4.3 没有PLL可用ODDR/DDR原语如果系统里没有 PLL或者约束不允许额外倍频最后一招是用厂商的 DDR 输出原语比如 Xilinx 的 ODDR、Altera 的 ALTDDIO_OUT来构建一个双沿触发的翻转寄存器。原理是把输出分成上下两路各自在上升沿和下降沿更新再用原语拼出双沿输出。这块代码和器件绑定不同厂商写法不同而且需要读原语的时序模型。除非确实没有 PLL否则我不推荐普通项目里这么干调试成本不低。5. 常见问题与调试实录下面这些坑大部分我在调 N.5 分频器时都亲自踩过整理出来给各位当速查表。现象可能原因解决办法输出频率正好是期望值的一半把输出周期 2N1 个半周期当成了单个相位的目标值phase target 应该是 N 或 N1不是 2N1占空比固定在 40%不是 50%phase_no 轮换写成了 N、N1、N、N1用 4 状态轮换 [N, N1, N1, N]复位后的前几个周期波形异常phase_no 初始状态没有对齐复位时把 phase_no 清零并且不计复位后的第一个周期上板后输出有毛刺输出 q 用了组合逻辑直接赋值输出信号必须用寄存器驱动不要用 assign 从比较器取综合报 dual-edge 或 multi-driven 错误直接在 always 块里写 posedge/negedge换成 PLL 2 倍频方案或者用 DDR 原语分频比 N 较大时计数溢出hp_cnt 位宽不够用 $clog2(N1) 计算位宽统计占空比不是 50%测量窗口只覆盖了奇数个输出周期的前半段至少统计连续两个完整输出周期或者用长时间平均这里重点说两个容易忽略的细节。一个是复位后的初始相位。复位时clk_out强制为 0phase_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 分频每隔一段时间按 I1 分频I1 出现的频率由 F 控制。具体做法是维护一个累加器acc每个输出周期执行acc acc F; if (acc 1) begin 当前周期分频比为 I1; acc acc - 1; end else begin 当前周期分频比为 I; end当 F0.5 时acc 的序列是 0.5、1、0.5、1……也就是说每隔一个输出周期切一次模对应到分频比就是 I、I1、I、I1。如果这时候再结合半周期的边沿处理就回到了我们前面讲的 N.5 分频。F 是 0.3、0.7 这类非 0.5 的值时切换序列不再这么简单通常需要 sigma-delta 调制器来打散误差避免能量集中在特定频点上那已经是小数分频锁相环的领域了。我这里讲的半周期计数方案本质上也是一种“边沿域”的相位累加把余数 0.5 个半周期放在 4 个相位里均摊只不过因为 N.5 的余数恰好是 0.5轮换序列非常规律用一个小状态机就能搞定。结尾再分享一点个人经验。我最早做这个分频器时死磕双沿触发觉得 PLL 倍频是偷懒。后来在 FPGA 上综合失败好几次才明白工程方案和原理方案本来就是两回事原理用双沿计数讲得最清楚工程用 PLL 倍频实现最稳。如果让我给建议我会说先花半小时把轮换模式的时序表画明白再决定用哪种方案落地别上来就写代码。状态机的 target 序列一旦画错后面所有调试都是在浪费时间画对了仿真一遍过上板也是一遍过的概率很高。
延伸阅读

更多相关文章

2026/10/15 17:59:09

MFC实时绘图不卡顿方案:TeeChart与HighSpeedChart工业级优化

简介:本资源是一套面向MFC开发者的动态曲线绘图实战代码包,聚焦TeeChart与CChartCtrl(HightSpeedChart)两大主流控件在实时数据可视化场景中的应用,适用于工业监控、仪器仪表、数据采集等需高频刷新波形的中高级C项目开…

2026/10/15 17:59:09

嵌入式Linux GPIO按键驱动开发:中断消抖与input子系统实战

简介:这份资源面向Linux内核驱动初学者与嵌入式开发人员,提供按键驱动从内核实现到用户空间验证的完整示例,帮助理解字符设备注册、中断处理与输入子系统的基本流程。压缩包共2个文件,均为C语言源码,整体约2KB&#xf…

2026/10/15 17:59:09

JIRA零基础入门:从核心概念到团队配置实战指南

1. 从一个真实困境说起:为什么我要写这份零基教程团队从十几人扩张到几十人的过程中,我经历过最混乱的一段时期。需求靠聊天记录追,任务靠口头分配,进度靠站会同步,上线靠人肉记忆。结果就是:同一个需求三个…

2026/10/15 19:04:23

Vue 3组合式API精讲:从响应式基础到组件通信实战

如果你是从 JavaScript 语法课一路跟过来的,到这一节基本算是站在了“能写页面”和“会写应用”的分水岭上。前面二十节把变量、函数、闭包、原型链、异步这些地基夯得差不多了,现在要接触的是真正的前端工程化主角——Vue 3 组合式 API。这篇内容就是帮…

2026/10/15 18:59:22

Arduino OLED驱动终极方案:U8g2库工业级实战指南

简介:Arduino_u8g2库是面向Arduino初学者与嵌入式开发者的高性能图形显示解决方案,专为OLED、LCD及ePaper等低资源显示屏设计,显著降低复杂界面开发门槛,适用于物联网终端、智能硬件原型、教育实验及艺术交互装置等场景。资源包共…

2026/10/14 11:31:03

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/14 17:41:07

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/15 0:02:22

防关联浏览器哪个好?三款产品实测差异解析

新手做多店铺,最纠结的就是防关联浏览器选哪个。飞跨、网易、站斧这三款被问得最多,先给结论:要一体化省心、长期便宜,选站斧;要网易招牌和稳,选网易;预算紧做东南亚 Lazada,选飞跨。…

2026/10/15 0:02:22

影刀RPA深度实战:大模型Fine-tuning定制RPA专用Prompt与指令生成

影刀RPA深度实战:大模型Fine-tuning定制RPA专用Prompt与指令生成 一、技术背景 大模型(LLM)在通用场景下表现强大,但当我们将 GPT-4 或 Qwen 直接用于生成影刀RPA操作指令和 XPath 定位时,准确率通常只有 40%-60%。原因…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑