Verilog时钟分频实战:从偶数、奇数到小数分频的设计与实现
1. 项目概述从零开始掌握Verilog时钟分频在数字电路和FPGA设计中时钟信号是驱动整个系统同步运行的“心跳”。然而一个系统往往需要多种不同频率的时钟来驱动不同的模块比如高速的处理器核心和低速的外设接口。直接使用多个外部晶振既不经济也不现实这时时钟分频技术就成了我们手中的利器。它允许我们从单一的高频源时钟通过数字逻辑电路产生出我们所需的任意低频时钟。对于每一位硬件描述语言HDL的实践者尤其是使用Verilog的工程师来说深刻理解并熟练实现各种分频电路是一项至关重要的基本功。这次我们不谈空泛的理论直接切入实战。我将结合自己多年的FPGA开发经验系统性地梳理和总结Verilog实现时钟分频的各类方法。从最简单的偶数分频到需要巧思的奇数分频再到更具挑战性的半整数分频和小数分频我会逐一拆解其背后的设计思路、电路原理、Verilog实现代码并分享那些在教科书和官方文档里找不到的设计陷阱、时序考量和调试技巧。无论你是正在学习Verilog的学生还是需要快速回顾的工程师这篇文章都将为你提供一个清晰、可直接复现的参考指南。2. 时钟分频的核心原理与设计思路在深入代码之前我们必须先建立正确的认知在FPGA中设计分频电路本质上是在设计一个同步时序逻辑。它的核心是一个计数器通过计数源时钟的周期数在特定的计数值点对输出时钟信号进行翻转或生成脉冲。设计的目标不仅仅是得到一个频率正确的信号更要追求稳定的时序性能、可控的占空比以及纯净的时钟质量避免毛刺。2.1 分频类型与设计目标解析根据分频系数N即输出时钟频率 输入时钟频率 / N的特性我们将分频分为几类每类的设计目标和挑战都不同偶数分频 (N为偶数)这是最理想的情况。目标是产生占空比为50%的方波时钟。因为N是偶数我们可以轻松地在计数到N/2时翻转时钟实现完美的对称。奇数分频 (N为奇数)挑战在于如何得到50%的占空比。如果对占空比没有要求例如仅用作使能信号实现方式与偶数分频类似。若要求50%占空比则必须利用源时钟的双边沿上升沿和下降沿来构造两个相位差半个周期的中间信号再进行逻辑组合。半整数分频 (N为X.5如3.5, 4.5)分频系数带0.5。目标是在长时间尺度上实现平均频率的精确分频但单个周期的占空比无法达到50%。设计核心同样是双边沿逻辑的巧妙运用。小数分频 (N为小数如7.6, 5.76)这是最复杂的情况。由于无法对时钟进行小数计数我们采用“平均频率”的概念。例如7.6分频并非每个输出周期都是7.6个输入周期而是在76个输入周期内产生10个输出周期76/107.6。这需要通过动态切换两种不同的整数分频模式如7分频和8分频并按特定算法“均匀插入”来实现以最小化输出时钟的相位抖动。2.2 关键设计考量与选型依据在设计分频模块时以下几个问题必须在编码前就想清楚全局时钟网络 vs. 普通逻辑信号在FPGA中通过逻辑产生的时钟称为“门控时钟”或“衍生时钟”通常无法接入专用的全局时钟网络。这会导致时钟偏斜大、驱动能力弱。因此分频产生的时钟应尽量避免直接用作其他时序模块的时钟输入端更好的做法是将其作为“时钟使能信号”原模块仍使用全局时钟在使能有效时才更新。如果必须作为时钟使用需在约束文件中将其定义为生成时钟。同步复位与异步复位示例代码中常使用异步复位negedge rstn。在实际工程中推荐优先使用同步复位因为它更利于静态时序分析STA能避免复位信号上的毛刺引起触发器误动作。除非有明确需求如上电初始化否则应养成同步复位的习惯。毛刺问题这是分频电路尤其是采用组合逻辑输出时的大敌。当分频逻辑的多个输入信号变化时刻略有差异时输出端可能产生极窄的尖峰脉冲毛刺。毛刺时钟会引发后续电路功能错误。解决方案包括使用寄存器输出、在组合逻辑后插入寄存器、或使用时钟专用缓冲器。注意在FPGA设计中一个非常重要的原则是“时钟宜少不宜多时钟域宜简不宜繁”。每增加一个分频时钟就意味着增加了一个新的时钟域随之而来的是复杂的跨时钟域同步问题。因此在架构设计初期应优先考虑使用时钟使能Clock Enable方案来替代生成多个低频时钟。3. 偶数分频的经典实现与优化偶数分频是基础理解它有助于构建更复杂分频的思维模型。其核心思想是一个N位计数器N为偶数循环计数0到N-1在计数值为0和N/2时对输出时钟进行翻转。3.1 基础2/4/8分频触发器级联法对于2、4、8等2的幂次方分频有一种非常简洁且面积优化的实现方法D触发器的反向输出端Qn连接到其数据输入端D。这样每个时钟上升沿触发器都会取反一次自然产生一个周期是原时钟两倍的信号即2分频。module div2( input wire clk, input wire rst_n, output reg clk_div2 ); always (posedge clk or negedge rst_n) begin if (!rst_n) clk_div2 1‘b0; else clk_div2 ~clk_div2; // 关键输出取反后反馈给自己 end endmodule将两个这样的div2模块级联第一个的输出作为第二个的时钟输入就得到了4分频。再级联一个得到8分频。这种方法硬件开销极小。实操心得这种方法产生的分频时钟其时钟沿与原时钟的上升沿对齐。但在级联时要注意后级触发器的时钟是前级的输出这属于“行波时钟”可能会带来时序上的挑战。在低速或对时钟质量要求不高的场景下可以使用但在高速或要求时钟抖动小的系统中更推荐下面统一的计数器法。3.2 通用偶数分频器计数器法对于任意偶数N例如10我们使用一个计数器在0到N-1之间循环。当计数器小于N/2时输出高电平大于等于N/2时输出低电平或反之。这样就能得到占空比50%的时钟。module even_divisor #( parameter DIV_COEF 10 // 分频系数必须为偶数 )( input wire clk, input wire rst_n, output reg clk_out ); // 计算计数器位宽能覆盖0到DIV_COEF-1 localparam CNT_WIDTH $clog2(DIV_COEF); reg [CNT_WIDTH-1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; clk_out 1b0; end else begin if (cnt DIV_COEF - 1) begin cnt 0; end else begin cnt cnt 1; end // 在计数值到达中间点和零点时翻转时钟 if (cnt (DIV_COEF/2) - 1) begin clk_out 1b1; end else if (cnt DIV_COEF - 1) begin clk_out 1b0; end end end endmodule代码细节解析$clog2(DIV_COEF)是Verilog系统函数用于计算存储DIV_COEF所需的最小位宽这样写代码更通用、更专业。时钟翻转的判断条件cnt (DIV_COEF/2) - 1和cnt DIV_COEF - 1是精确实现50%占空比的关键。注意因为计数器从0开始所以中间点是N/2 - 1结束点是N-1。复位时将clk_out置为0这是一个确定的初始状态。你也可以根据系统需求置为1。仿真与调试提示编写Testbench时除了检查分频频率是否正确一定要用波形工具测量高电平和低电平的持续时间是否严格相等。一个常见的错误是计数器判断条件写错导致占空比不是50%比如误写成cnt DIV_COEF/2这会使得高电平多持续一个周期。4. 奇数分频实现50%占空比的两种路径奇数分频的难点在于如果只在时钟上升沿计数无法找到一个整数点能将周期N平分为两个相等的整数部分。因此要获得50%占空比必须借助时钟的下降沿。4.1 非50%占空比的简单实现如果对占空比无要求例如这个信号只是作为一个周期性的脉冲使能那么实现方式和偶数分频的计数器法几乎一样计数器循环0到N-1在计数值为0时拉高在计数值为某个值比如N-1时拉低。这样产生的是一个窄脉冲时钟。module odd_divisor_simple #( parameter DIV_COEF 9 )( input wire clk, input wire rst_n, output reg clk_out ); localparam CNT_WIDTH $clog2(DIV_COEF); reg [CNT_WIDTH-1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; clk_out 1b0; end else begin if (cnt DIV_COEF - 1) begin cnt 0; end else begin cnt cnt 1; end // 产生一个周期的高脉冲 clk_out (cnt 0) ? 1b1 : 1b0; end end endmodule4.2 50%占空比实现双边沿采样与逻辑组合这是奇数分频的精华所在。以3分频为例我们的目标是产生一个周期为3T高电平持续1.5T的信号。思路如下分别在源时钟的上升沿和下降沿驱动两个计数器实际上是同一个计数器但用双边沿采样其值。根据计数器值在上升沿生成一个信号clk_p其高电平持续(N1)/2个周期不对于3分频我们在上升沿产生一个高电平为1T、低电平为2T的信号。在下降沿根据相同的计数器值产生另一个信号clk_n其波形与clk_p相似但相位相差半个源时钟周期(T/2)。将clk_p和clk_n进行或运算(OR)或与运算(AND)即可得到一个占空比为50%的时钟。为什么或运算和与运算都可以这取决于你在步骤2和3中如何定义clk_p和clk_n的波形。或运算方案让clk_p和clk_n都是“窄高电平”波形高1T低2T。这两个窄脉冲在时间轴上错开半个周期将它们“或”起来两个窄脉冲就会连成一个宽脉冲恰好是1.5T。与运算方案让clk_p和clk_n都是“宽高电平”波形高2T低1T。这两个宽脉冲有半个周期的重叠部分将它们“与”起来得到的重叠部分恰好是1.5T。下面以或运算实现9分频为例module odd_divisor_or #( parameter DIV_COEF 9 // 必须为奇数 )( input wire clk, input wire rst_n, output wire clk_out ); localparam CNT_WIDTH $clog2(DIV_COEF); reg [CNT_WIDTH-1:0] cnt; reg clk_p, clk_n; // 公共计数器在上升沿计数 always (posedge clk or negedge rst_n) begin if (!rst_n) cnt 0; else if (cnt DIV_COEF - 1) cnt 0; else cnt cnt 1; end // 上升沿生成窄高电平信号 // 假设高电平在计数值0~(DIV_COEF-1)/2 -1 期间有效 // 例如9分频高电平在cnt0~3时有效4T低电平在cnt4~8时有效5T always (posedge clk or negedge rst_n) begin if (!rst_n) clk_p 1b0; else if (cnt ((DIV_COEF-1)1) - 1) // cnt 3 clk_p 1b1; else clk_p 1b0; end // 下降沿生成同样的窄高电平信号但相位差半拍 always (negedge clk or negedge rst_n) begin if (!rst_n) clk_n 1b0; else if (cnt ((DIV_COEF-1)1) - 1) // 使用相同的计数器值判断 clk_n 1b1; else clk_n 1b0; end // 或操作合并得到50%占空比时钟 assign clk_out clk_p | clk_n; endmodule关键点与避坑指南计数器共用clk_p和clk_n的逻辑判断基于同一个计数器cnt。cnt仅在上升沿变化但clk_n在下降沿采样这个“稳定”的cnt值。这保证了两个信号的同步性。避免使用组合逻辑选择器最终输出clk_out是通过组合逻辑assign语句产生的。虽然在这个特定设计中由于clk_p和clk_n是寄存器输出且变化时刻错开产生毛刺的风险较低但最佳实践是再使用一级寄存器对clk_out进行打拍以彻底消除任何潜在的毛刺。reg clk_out_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) clk_out_reg 1b0; else clk_out_reg clk_p | clk_n; // 用寄存器输出 end assign clk_out clk_out_reg;注意这里用上升沿寄存器采样会引入一个时钟周期的延迟并可能轻微改变占空比取决于clk_p|clk_n的变化相对于时钟沿的位置。在要求极精确的应用中需要仔细分析。更稳妥但面积稍大的方法是用一个触发器在下降沿采样这个组合逻辑。时序约束由于使用了时钟的下降沿必须在约束文件中声明。在Vivado中需要创建create_generated_clock并指定-edges选项。否则静态时序分析工具会无法正确分析这条路径。5. 半整数分频如3.5分频的实现策略半整数分频如3.5分频意味着输出时钟的周期是输入时钟的3.5倍。你无法用整数个时钟周期来实现一个周期但可以在多个周期内实现平均频率的正确。例如3.5分频等价于每7个输入时钟周期产生2个输出时钟周期7/23.5。5.1 核心思想双边沿调整与脉冲合并我们以3.5分频为例阐述一种经典且易于理解的实现方法目标在7个clk周期内产生2个clk_div周期。第一步产生非均匀脉冲。设计一个计数器从0计数到6。我们可以在cnt0时产生一个持续4T的高脉冲在cnt4时产生一个持续3T的高脉冲cnt4到cnt6再加回到0。这样两个脉冲的周期分别是4T和3T它们的“平均”周期是3.5T但本身不均匀。我们称这个信号为clk_ave。第二步产生调整脉冲。为了将不均匀的脉冲“均匀化”我们需要另一个信号clk_adj。它在clk的下降沿被产生其脉冲的位置相对于clk_ave的脉冲有半个周期的偏移。具体来说clk_adj的一个脉冲对应clk_ave的第一个脉冲但起始点延迟半个周期另一个脉冲对应clk_ave的第二个脉冲但起始点提前半个周期。第三步逻辑合并。将clk_ave和clk_adj进行**或(OR)**运算。两个信号在时间轴上的脉冲部分重叠、部分衔接最终合并出的波形其高电平持续时间就变得均匀了实现了3.5倍分频。module half_divisor #( parameter MULT2_COEF 7 // 对于3.5分频此为2*3.57 )( input wire clk, input wire rst_n, output wire clk_div ); // 计数器计数值0到6 localparam CNT_WIDTH $clog2(MULT2_COEF); reg [CNT_WIDTH-1:0] cnt; reg clk_ave_r, clk_adjust_r; // 计数器逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) cnt 0; else if (cnt MULT2_COEF - 1) cnt 0; else cnt cnt 1; end // 在上升沿产生非均匀脉冲 clk_ave_r // 第一个脉冲cnt0时开始持续到cnt3? 我们需要一个持续4T的脉冲。 // 更准确在cnt0时拉高在cnt4时拉低0,1,2,3 共4个周期 // 第二个脉冲cnt4时开始持续到cnt64,5,6 共3个周期 always (posedge clk or negedge rst_n) begin if (!rst_n) clk_ave_r 1b0; else if (cnt 0) clk_ave_r 1b1; else if (cnt (MULT2_COEF/2) 1) // 对于7 (7/2)14 (整数除法) clk_ave_r 1b1; else if (cnt (MULT2_COEF/2) ) // cnt 3 clk_ave_r 1b0; else if (cnt MULT2_COEF - 1) // cnt 6 clk_ave_r 1b0; end // 在下降沿产生调整脉冲 clk_adjust_r // 它的作用是“填补” clk_ave_r 的空白使其波形均匀化。 // 通常它的脉冲位置与 clk_ave_r 错开半个周期。 always (negedge clk or negedge rst_n) begin if (!rst_n) clk_adjust_r 1b0; else if (cnt 1) // 相对于 clk_ave_r 的第一个脉冲延迟半拍开始 clk_adjust_r 1b1; else if (cnt (MULT2_COEF/2) 1) // cnt 4 相对于第二个脉冲提前半拍开始 clk_adjust_r 1b1; else if (cnt (MULT2_COEF/2) 1) // 何时拉低需要根据波形仔细设计 clk_adjust_r 1b0; // ... 这里需要根据精确的时序图来完善拉低条件 end // 合并输出 assign clk_div clk_ave_r | clk_adjust_r; endmodule重要提示上面的代码中clk_adjust_r的拉低条件我故意留空了因为这是设计中最精细的部分。在实际动手前必须用纸笔画出一个完整的时序图标出cnt、clk_ave_r、clk_adjust_r在每个clk上升沿和下降沿的变化才能准确编写代码。这是硬件设计的基本功依赖感觉或猜测一定会出错。5.2 设计验证与常见问题半整数分频的设计验证至关重要。在仿真中你需要测量输出时钟clk_div的周期是否稳定为3.5倍的clk周期。可以测量多个周期的平均时间。观察clk_div的占空比。对于3.5分频占空比不可能是50%但应该是稳定的。重点检查毛刺。由于最终输出是组合逻辑在clk_ave_r和clk_adjust_r变化时刻如果存在细微的时间差clk_div上就可能产生毛刺。必须放大波形仔细观察每一个跳变沿。常见问题输出有毛刺这是最可能的问题。解决方案永远是寄存器输出。不要直接使用组合逻辑赋值给clk_div而是用clk的上升沿或下降沿去采样clk_ave_r | clk_adjust_r的结果再用这个寄存器的值作为输出时钟。占空比不稳定检查计数器的判断条件是否精确尤其是边界条件如cnt X。确保clk_ave_r和clk_adjust_r的脉冲宽度计算正确。时序违规因为使用了双边沿如果不加约束建立/保持时间检查可能会报错。务必在约束文件中正确定义生成的时钟。6. 小数分频的算法实现与均匀插入技巧小数分频是时钟管理中的高阶课题其核心思想是“长时间平均频率”准确。例如实现7.6分频我们无法让每个输出周期都是7.6个输入周期但可以做到在76个输入周期内恰好产生10个输出周期76/107.6。6.1 算法基础双模分频与差值累加实现小数分频通常采用双模分频器即在两种整数分频模式比如7分频和8分频之间动态切换。问题的关键在于如何安排这两种模式的切换顺序才能使输出时钟的相位抖动Phase Jitter最小答案就是“均匀插入”。我们以7.6分频为例总输入周期数 M 76总输出脉冲数 N 10基础分频系数 K floor(M/N) floor(7.6) 7需要多计数的周期总数 F M - KN 76 - 710 6这意味着在76个clk周期内我们需要进行10次分频。其中大部分10-64次是7分频有6次需要“额外多吃掉”一个周期变成8分频。如果我们把这6次8分频均匀地穿插在4次7分频之中输出时钟的抖动就会最小。差值累加算法是实现均匀插入的经典方法设置一个累加器acc初始值为0。每次需要输出一个脉冲即完成一次分频时执行acc acc F。判断acc是否大于等于N本例为10如果acc N则本次分频采用K1模式8分频并令acc acc - N。如果acc N则本次分频采用K模式7分频。重复步骤2-3直到完成所有分频。这个算法的妙处在于它自动实现了“均匀插入”。acc累加F一旦超过N就进位并采用更长的分频模式同时扣除N。这个过程类似于一个分数累加器。6.2 Verilog实现与代码详解下面是一个参数化的小数分频模块实现module frac_divisor #( parameter SOURCE_CYCLES 76, // 总输入时钟周期数 M parameter DEST_CYCLES 10 // 总输出时钟周期数 N )( input wire clk, input wire rst_n, output reg clk_frac ); // 计算基础分频系数和差值 localparam K SOURCE_CYCLES / DEST_CYCLES; // 基础分频比 (7) localparam F SOURCE_CYCLES - K * DEST_CYCLES; // 差值 (6) localparam KP1 K 1; // 增一分频比 (8) // 主计数器用于当前分频模式的计数 reg [$clog2(KP1)-1:0] main_cnt; // 当前分频周期阈值K 或 KP1 reg [$clog2(KP1)-1:0] cnt_threshold; // 差值累加器 reg [$clog2(2*DEST_CYCLES)-1:0] acc; // 位宽需能存下 accF // 状态机或逻辑控制 always (posedge clk or negedge rst_n) begin if (!rst_n) begin main_cnt 0; clk_frac 1b0; acc 0; cnt_threshold K - 1; // 计数器从0到K-1共K个周期 end else begin // 主计数器逻辑 if (main_cnt cnt_threshold) begin main_cnt 0; clk_frac 1b1; // 计数满产生一个高电平脉冲单周期 // *** 关键更新下一次的分频模式 *** // 计算临时累加值 if (acc F DEST_CYCLES) begin // 本次应采用增一分频模式 (KP1) cnt_threshold KP1 - 1; acc acc F - DEST_CYCLES; end else begin // 本次应采用基础分频模式 (K) cnt_threshold K - 1; acc acc F; end end else begin main_cnt main_cnt 1; clk_frac 1b0; // 非计数结束点输出为低 end end end endmodule代码深度解析与避坑参数计算K、F、KP1使用localparam在编译时计算保证了逻辑的通用性。你可以通过修改SOURCE_CYCLES和DEST_CYCLES来实现任意小数分频如5.76分频则 M576, N100。位宽设计main_cnt的位宽由KP1决定因为要能计到KP1-1。acc的位宽是关键。它需要能存储accF的最大值。最坏情况是连续多次采用基础模式acc会一直累加F直到接近N才被扣除。因此其位宽应能表示N F的量级。这里保守地设为$clog2(2*DEST_CYCLES)。输出波形这个模块产生的是单周期高脉冲的时钟。clk_frac在每个分频周期结束时拉高一个clk周期。如果你需要50%占空比对于小数分频这不可能或更宽的脉冲可以修改clk_frac的生成逻辑例如在main_cnt (cnt_threshold1)时输出高电平。但请注意小数分频的占空比本身是不均匀的。均匀性验证为了验证算法是否真的均匀插入了8分频你可以在仿真中监视cnt_threshold的变化。一个7.6分频的典型序列可能是7, 7, 8, 7, 8, 7, 8, 7, 8, 8。你会发现8分频的出现大致是均匀的而不是连续出现。性能与优化上面的代码将累加判断和模式更新放在了main_cnt cnt_threshold的时刻这是串行逻辑。在高速时钟下这可能会成为关键路径。一个优化技巧是使用“提前计算”的思路在本次分频周期开始时就根据当前的acc值计算出本次应该用哪个阈值并预判下一次的acc值。这可以将关键路径拆分开。6.3 仿真与调试实录编写Testbench时除了常规的时钟和复位关键是要验证平均频率。module tb_frac_divisor; reg clk; reg rst_n; wire clk_out; frac_divisor #(.SOURCE_CYCLES(76), .DEST_CYCLES(10)) uut (.*); initial begin clk 0; forever #5 clk ~clk; // 100MHz时钟周期10ns end initial begin rst_n 0; #100 rst_n 1; #10000; // 仿真足够长时间覆盖多个76周期循环 $finish; end // 自动验证平均频率 integer clk_cycle_count 0; integer frac_cycle_count 0; real last_rise_time; real current_period; real total_time; real average_period; always (posedge clk) clk_cycle_count clk_cycle_count 1; always (posedge clk_out) begin if (rst_n frac_cycle_count 0) begin current_period $realtime - last_rise_time; total_time total_time current_period; average_period total_time / frac_cycle_count; $display(Cycle %0d: Period %0.3f ns, Running Avg %0.3f ns (Expected 76.0 ns), frac_cycle_count, current_period, average_period); end last_rise_time $realtime; frac_cycle_count frac_cycle_count 1; end endmodule在仿真波形中你会看到clk_out的周期在7T和8T之间跳动但打印出的Running Avg会逐渐收敛到76.0ns即7.6 * 10ns。这就是小数分频成功的标志。最终总结与个人体会 时钟分频看似是数字逻辑中的基础课题但深入下去涉及到了同步设计思想、时序分析、时钟域处理以及算法硬件化等核心概念。从我多年的项目经验来看对于偶数分频和占空比无要求的奇数分频可以放心使用。但对于需要50%占空比的奇数分频以及小数分频在工程中引入必须慎之又慎。它们产生的时钟质量通常不如专用的时钟管理单元如FPGA内的PLL或MMCM会带来额外的时序收敛压力和抖动。我的建议是能用PLL/MMCM就不要用逻辑分频。逻辑分频更适合产生低频的使能信号或用于时钟门控。如果必须使用一定要做好充分的仿真、严格的时序约束并且将分频逻辑放在一个独立的、层次清晰的模块中方便后续的维护和调试。记住在硬件设计中稳定性和可维护性永远比炫技更重要。