☰
Verilog时钟分频器设计:偶数、奇数与半整数分频的工程实践
2026/10/17 18:09:56 网站建设 项目流程

简介:一份面向FPGA工程师与数字系统学习者的基于Verilog的时钟分频器工程资源,采用同步计数器机制,通过计数达到预设分频系数后翻转输出,将高频时钟转换为所需低频时钟,覆盖参数化分频、UCF管脚约束与时钟约束等关键环节,适用于入门教学与实际项目复用。资源包共155个文件,以.v源码、.ucf管脚约束、.bit配置文件、综合报告及工程配置文件为主,整体仅971KB,便于快速下载与移植。目前已有1727人学习下载。压缩包内不仅包含可综合的Verilog分频器实现,还附带Xilinx ISE工程完整过程文件,包括综合、映射、布局布线报告与仿真相关文件,读者可对照源码理解分频系数参数化设置、计数器状态更新与输出翻转逻辑,并借鉴工程目录结构与排错思路,直接用于自身FPGA时钟管理设计。 最近有个刚学FPGA的朋友发来一段分频器代码,问我为什么仿真波形乱七八糟。代码本身不长,就是一个计数器加一个翻转,但问题出在复位信号没接好、位宽少写了一位,比较条件用的是大于等于而不是等于。这些细节单看都不难,堆在一起却能把人绕晕。于是我把verilog时钟分频器这个看似基础、实际坑位密集的模块按工程实践重新梳理了一遍。这篇文章不讲教科书里那种“抄完就忘”的例程,而是从参数化模块设计、奇数分频的占空比问题、半整数分频的可行性到testbench自动验证,把这些要点过一遍,适合正在学FPGA、准备数字IC笔试、或者已经在做逻辑设计想回头补基础的同学参考。

1. 分频器在FPGA工程里解决什么问题:UART波特率与PLL之外的逻辑分频

1.1 三个绕不开的实际场景:串口、PWM、按键消抖

分频器不是学术玩具,它在板子上天天被用到。最典型的是UART波特率生成。通常的做法是先由系统时钟分频得到一个16倍过采样时钟,再在这个时钟节拍下找数据位的中心采样点。假设系统时钟是50MHz,目标波特率115200,那分频系数就是50_000_000 / (115200 * 16) = 27.1267,取整后要么27、要么28。取27时,实际波特率是50_000_000 / (27 * 16) = 115740.7,误差约0.47%,对UART来说完全没问题;取28时,实际波特率变成111607,误差到了-3.12%。UART的位误差容忍范围一般在±2%左右,加上噪声和时钟抖动,用28分频就会出现偶发误码。同一个分频器,参数只差一位,通讯稳定性就能差出一个量级。

第二个场景是PWM、呼吸灯、按键消抖这类周期性逻辑。PWM调光和直流电机调速对频率精度要求不高,但需要周期稳定、占空比可调;按键消抖则经常需要产生一个几毫秒的节拍信号,用计数器分频把快时钟降到慢节拍。这类需求用逻辑分频器最合适,省PLL资源,代码也简单。

第三个场景是片内低速时钟域。一个SOC式设计里,有些外设模块不需要跑在几百兆赫兹的全局时钟上,给它一个更低频率的节拍就能工作,同时还能降低动态功耗。这里用分频器生成独立的低速节拍,比单独再配一个PLL输出要经济得多。

1.2 为什么不能所有分频都丢给PLL

很多人会问:FPGA里不是有PLL或者MMCM吗,干嘛还要用Verilog写分频器?答案很现实:PLL资源有限。常见中端FPGA的PLL数量在4到8个左右,而一个复杂设计的时钟域可能超过十几个。另外,PLL对输入频率、输出频率有范围限制,锁定需要时间,某些低频输出直接做不出来。相比之下,Verilog分频器消耗的只是寄存器和LUT,适合生成低频整数倍分频信号,缺点是没法直接产生非整数倍频,输出抖动也偏大。

工程上的通用做法是:能整除的用逻辑分频器,不能整除或对抖动要求高的交给PLL,两者配合。一张表说明差异:

维度PLL/MMCMVerilog逻辑分频器
分频能力整数、小数、倍频主要整数,半整数/分数需特殊设计
输出抖动低相对较高
全局时钟驱动自带BUFG路径需要手动加BUFG或改用时钟使能
资源占用专用模拟/时钟资源寄存器和查找表
典型场景DDR、高速SerDes、低抖动时钟UART波特率、PWM、指示灯闪烁

这张表回答了一个关键问题:分频器设计的本质不是“怎么把计数器写出来”,而是“这个时钟的用途是什么、精度要求到什么程度、用什么方式生成最合理”。先想清楚需求,再写代码,顺序不能反。

2. 偶数分频的代码与边界:N/2-1、位宽和复位相位

2.1 参数化偶数分频模块:为什么比较条件是N/2-1

先给一个最常用的参数化偶数分频模块。对N偶数分频,计数器计数范围是0到N/2-1,每计数满一轮输出翻转一次。以4分频为例,输入时钟周期T,计数器在0、1内输出保持低,在2、3内输出保持高,输出周期就是4T。关键在比较条件:当计数值等于N/2-1时翻转并清0,不要写成N/2,否则周期会多出一拍。

module clk_div_even #( parameter integer DIV = 4 // 分频系数,必须为不小于2的偶数 )( input wire clk_in, input wire rst_n, output reg clk_out ); localparam integer CNT_WIDTH = $clog2(DIV); reg [CNT_WIDTH-1:0] cnt; always @(posedge clk_in or negedge rst_n) begin if (!rst_n) begin cnt <= 'd0; clk_out <= 1'b0; end else if (cnt == (DIV/2 - 1)) begin cnt <= 'd0; clk_out <= ~clk_out; end else begin cnt <= cnt + 1'b1; clk_out <= clk_out; end end endmodule

DIV=2时,DIV/2-1等于0,计数器一直停在0状态翻转,输出正好是二分频,行为正确。DIV=4时,计数器在计到1时翻转清0,输出占空比50%。DIV=6时,计数器在0、1、2内保持低,3、4、5内保持高,周期6T。这里有个容易犯错的地方:有人会把计数器写成0到DIV-1,在cnt==DIV/2时翻转,这样也能得到6分频,但计数器范围变宽,代码多了冗余,还容易在DIV为奇数时把模块改坏。统一用0到N/2-1的范围,逻辑更紧凑。

2.2 位宽陷阱与复位相位

参数化模块最容易踩的坑是位宽。$clog2(DIV)在DIV=2时得到1,刚好;DIV=4时得到2,刚好;但如果DIV=1,$clog2(1)=0,声明一个0位宽的向量,综合直接报错。因此参数化分频器一定要在注释里明确DIV的取值范围,或者加一句initial assert(DIV >= 2 && DIV % 2 == 0),让非法参数在仿真阶段就暴露,而不是等到综合或上板才出问题。

复位相位也常被忽略。复位释放后clk_out初始值最好固定为0,同时cnt清0,保证第一个输入时钟边沿到来后分频器才真正开始计数。如果初始值是1,下游模块可能在复位释放瞬间就收到一个上升沿,而此时其他模块还在复位状态,启动时序就乱了。另外,分频器的复位建议接入经过“异步复位、同步释放”处理的复位信号,不要直接用按键异步信号去复位。原因是分频器对启动相位敏感,如果复位释放落在时钟边沿附近,触发器和计数器可能进入亚稳态,最终表现是上电后输出相位偶发异常,这种问题在仿真里看不到,只能在硬件上抓,非常难排查。

3. 奇数分频的50%占空比:双沿计数、相或与毛刺处理

3.1 为什么5分频不能像4分频那样直接翻转

偶数分频简单是因为半周期是整数倍的T,计数器在上升沿就能表达。奇数分频麻烦在周期是奇数倍T,半周期是2.5T、3.5T这种带0.5的值,而计数器只在时钟上升沿动作,能数出来的时间粒度只有T,表达不了0.5T。所以只用上升沿,无论怎么选翻转位置,占空比只能是2/5=40%或3/5=60%,不可能正好50%。

要解决0.5T的问题,思路是借助下降沿。既然上升沿只能给出整周期精度,那就用下降沿再产生一路带半拍偏移的分频信号,最后把两路信号做逻辑合并,高电平时间就能凑出0.5T的差值。这个思路叫双沿计数法,也是奇数分频50%占空比的经典解法。

3.2 双沿计数加相或:一个参数化奇数分频模块

实现方式如下:产生clk_p和clk_n两路信号,clk_p在上升沿计数,计数器从0到DIV-1循环,当计数到(DIV-1)/2时clk_p拉高,计数到DIV-1时clk_p拉低;clk_n用同样的逻辑,只是整个always块由下降沿触发。最后assign clk_out = clk_p | clk_n把两路合并。

module clk_div_odd #( parameter integer DIV = 5 // 分频系数,必须为不小于3的奇数 )( input wire clk_in, input wire rst_n, output wire clk_out ); localparam integer CNT_WIDTH = $clog2(DIV); reg [CNT_WIDTH-1:0] cnt_p, cnt_n; reg clk_p, clk_n; always @(posedge clk_in or negedge rst_n) begin if (!rst_n) begin cnt_p <= 'd0; clk_p <= 1'b0; end else if (cnt_p == (DIV - 1)) begin cnt_p <= 'd0; clk_p <= 1'b0; end else begin cnt_p <= cnt_p + 1'b1; if (cnt_p == (DIV - 1)/2) clk_p <= 1'b1; end end always @(negedge clk_in or negedge rst_n) begin if (!rst_n) begin cnt_n <= 'd0; clk_n <= 1'b0; end else if (cnt_n == (DIV - 1)) begin cnt_n <= 'd0; clk_n <= 1'b0; end else begin cnt_n <= cnt_n + 1'b1; if (cnt_n == (DIV - 1)/2) clk_n <= 1'b1; end end assign clk_out = clk_p | clk_n; endmodule

以5分频为例走一遍时序:clk_p在计数值等于2时拉高,计数值等于4时拉低,高电平持续2个输入周期T;clk_n同样在下降沿侧拉高、拉低,但相位比clk_p晚半个T。两路相或后,输出高电平从clk_p拉高的上升沿开始,一直到clk_n拉低的下降沿才结束,正好2.5T,低电平也是2.5T,占空比50%。DIV=3时,clk_p高电平1T、clk_n延迟半拍,合并后1.5T高电平,同样50%。

3.3 相或输出的毛刺问题与工程替代

上面的代码在仿真里没问题,拿到实际工程里却要小心。assign clk_out = clk_p | clk_n是一个组合逻辑,clk_p和clk_n经过LUT输出,存在组合延迟。clk_p和clk_n本身又是由不同边沿触发的寄存器输出,它们翻转时刻不同,合并时可能产生局部毛刺。更重要的问题是,这种分频时钟如果被当作真正的时钟信号去触发后续逻辑,综合工具可能不会自动把它放到BUFG上,布局布线后时钟偏斜会很明显,时序收敛变难。

工程上有两种替代做法。第一种是改成时钟使能模式:不再生成真正的clk_out,而是生成一个周期性的en信号,比如计数器计数到某个值时en拉高一个周期,下游逻辑改成always @(posedge clk_in)配合if (en)来驱动。整个设计只有一个真实时钟clk_in,不存在门控时钟和BUFG问题,时序分析和跨时钟域处理都省心很多。第二种是用PLL/MMCM直接输出奇数分频时钟,PLL生成的时钟经过专用时钟网络,占空比和抖动都有保证。我的习惯是:板级需要真正时钟给外部器件时优先PLL;PLL紧张时,偶数分频可以用寄存器直接翻转,奇数分频尽量改成时钟使能模式;纯实验和教学场景下才保留clk_p | clk_n这种写法。

生成方式占空比毛刺风险时序分析适用范围
偶数分频直接翻转50%低可综合,建议加BUFG整数偶数分频
奇数分频双沿相或50%中高组合延迟明显教学、简单工程
计数器时钟使能周期可预测低推荐,全同步设计内部低速节拍
PLL/MMCM可配置低最优时钟质量要求高的场合

4. 2.5分频这类半整数分频:脉冲删除能做什么、不能做什么

4.1 半整数分频不是面试题,是协议时序里的真实需求

半整数分频经常被当成数字IC笔试的“偏题”,但它在实际接口时序里绕不开。比如某些音频Codec的MCLK与LRCK不是整数倍关系,或者某个传感器需要非整数倍的触发时钟。当PLL资源不够或者输入频率范围不合适时,就得用逻辑去凑平均周期。这里一定要先建立一个概念:逻辑实现的半整数分频输出不是等间隔时钟,而是“平均频率正确、瞬时周期不均匀”的信号。它不能直接当高质量时钟用,但作为同步使能信号是完全可靠的。

另一种更常见的解读是:半整数分频解决的不是“产生一个2.5T周期的时钟”,而是“在每5个输入时钟周期里产生2个触发脉冲”。平均下来每个脉冲间隔2.5T,但实际间隔可能是2T和3T交替。这种模式在内部定时、数据采样、某些低速协议中都可以用。

4.2 一个最简单的2.5分频脉冲示例

思路是用模5计数器,固定其中两个相位输出高电平。下面的模块在cnt等于0和3时输出高,其余时间输出低,等效于每5个输入时钟周期输出2个1T宽的高脉冲,平均频率是输入的2/5,即2.5分频。

module clk_div_2p5 ( input wire clk_in, input wire rst_n, output reg pulse_out ); reg [2:0] cnt; always @(posedge clk_in or negedge rst_n) begin if (!rst_n) begin cnt <= 3'd0; pulse_out <= 1'b0; end else begin cnt <= (cnt == 3'd4) ? 3'd0 : cnt + 1'b1; case (cnt) 3'd0, 3'd3: pulse_out <= 1'b1; default: pulse_out <= 1'b0; endcase end end endmodule

这段代码输出的是脉冲序列而不是标准方波。把它接到外部器件的时钟引脚,很可能因为最小高电平时间不足或周期抖动大而触发失败;但作为内部采样使能,比如在cnt等于0和3时各采样一次,它就是完全同步、可靠的。用的时候要清楚自己到底需要的是哪种信号。

4.3 什么时候能用、什么时候必须绕道

我判断半整数分频能不能用的标准只有一条:输出是给内部逻辑当使能,放心用;输出是要给外部芯片当时钟,除非对方协议明确说能容忍非等间隔时钟,否则不要用,老老实实加PLL或者选一个接近的整数分频。

另外,半整数分频的输出占空比通常不是50%,如果外部电路只关心上升沿,可能无所谓;如果依赖占空比,方案就要改。需要更一般的小数分频时,比如1.5分频、3.3分频,基本原理类似:在一个大周期内分布M个脉冲,但脉冲分布越不均匀,瞬时误差越大,应用场景越窄。真正值得记住的不是某段2.5分频代码,而是“平均频率”和“瞬时周期”的差别,以及同步使能相比独立时钟的优势。

5. Testbench自动验证与工程落地的常见坑

5.1 用边沿间隔检测替代“肉眼看波形”

很多人的分频器testbench只会拉出波形看一眼就结束,换参数后还得重新数。更可靠的做法是用边沿间隔检测自动检查:在testbench里统计clk_out相邻上升沿之间的输入时钟周期数,判断是否等于配置的分频系数。以100MHz输入为例,配置6分频时输出周期应为60ns,5分频时输出周期应为50ns。示例代码如下:

module tb_clk_divider; reg clk_in, rst_n; wire clk_even, clk_odd; initial begin clk_in = 0; forever #5 clk_in = ~clk_in; // 100MHz end clk_div_even #(.DIV(6)) u_even ( .clk_in(clk_in), .rst_n(rst_n), .clk_out(clk_even) ); clk_div_odd #(.DIV(5)) u_odd ( .clk_in(clk_in), .rst_n(rst_n), .clk_out(clk_odd) ); integer last_time_even; integer last_time_odd; integer even_cnt, odd_cnt; always @(posedge clk_even) begin even_cnt = even_cnt + 1; if (even_cnt > 1) $display("even period = %0d ns", $time - last_time_even); last_time_even = $time; end always @(posedge clk_odd) begin odd_cnt = odd_cnt + 1; if (odd_cnt > 1) $display("odd period = %0d ns", $time - last_time_odd); last_time_odd = $time; end initial begin rst_n = 0; #30; rst_n = 1; #2000; $finish; end endmodule

除了周期,还要检查占空比。偶数分频直接看高电平宽度是否等于输入周期的DIV/2倍;奇数分频看高电平宽度是否等于(DIV/2)个输入周期再加半个输入周期。可以在testbench里加统计高电平宽度的逻辑,并用$error在数值不符时打印错误信息。这样在Vivado和ModelSim里跑完一遍,所有参数组合都能自动核对。

5.2 坑1:比较条件写错导致计数边界异常

分频器代码最常见的毛病是把比较条件写成cnt >= DIV/2 - 1。仿真时这个写法可能碰巧也能翻转,但综合后会产生额外逻辑,而且在某些边界组合下会出现一个周期内多次翻转的情况。正确写法是用精确的等号比较,并且在同一个always块里明确“达到边界时清零并翻转”的优先级。另一个常见错误是计数器范围设计不完整,比如从0数到DIV-2就提前清零,导致输出频率偏离预期。检查计数器范围的方式很简单:看最终计数值能否覆盖到DIV/2-1或者DIV-1这个边界。

5.3 坑2:门控时钟与时钟使能的选择

如果用分频时钟直接去做always @(posedge clk_out),工具会把它识别为门控时钟,布局布线时时钟树综合和约束都很麻烦。改成时钟使能模式之后,所有逻辑都挂在clk_in上,只有使能信号控制数据更新,设计变成单时钟域同步逻辑。这个改动对功能没有损失,但让代码的可维护性和后端处理难度都大幅改善。给外部接口提供时钟的场合另当别论,那种情况要确保时钟通过BUFG进入全局时钟网络,不要在子模块里到处生成分频时钟再东拉西扯。

5.4 坑3:跨时钟域采样和复位释放的隐患

分频时钟用作外部接口时钟时,如果另一端用同一个时钟采样数据,低速接口问题不大;但在FPGA内部用clk_out去采样另一个异步时钟域的数据,必须打两拍做同步,否则分频时钟相位和异步信号组合可能造成亚稳态。分频器复位信号也应经过同步处理,避免复位释放时刻落在输入时钟边沿附近导致启动相位不确定。这些基础问题在仿真里都很难复现,但上板后偶发异常往往就是它们引起的。

最后再说一个跟分频器有关的经验。分频器代码量不大,但它影响的是整个系统的时序和稳定性。我在项目里见过很多“分频器看起来没问题但系统偶发异常”的case,最终定位到复位相位不一致、门控时钟过多、计数器边界写错这类基础问题。每次接手新项目,我第一件事就是检查所有分频模块的parameter定义、复位方式和输出时钟路径。这些基础模块稳了,后面的逻辑才敢放心往上堆。

本文还有配套的精品资源,点击获取

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

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

立即咨询