1. 从“分频”说起为什么FPGA里这个功能如此重要刚接触FPGA的朋友可能会觉得“分频”不就是把一个高频时钟变慢一点吗写个计数器不就行了这有什么好讲的。但当你真正开始做一个项目比如需要驱动一个VGA显示器需要25.175MHz的像素时钟或者与一个串口设备通信需要115200Hz的波特率时钟又或者生成一个特定频率的PWM信号来控制电机转速时你就会发现事情没那么简单。你手头的主时钟晶振可能只有50MHz、100MHz这些固定的频率如何精准、稳定地“变”出你需要的那个频率就是分频器的核心任务。在FPGA设计中时钟信号是数字电路的“心跳”。一个稳定、纯净、相位关系明确的时钟是整个系统可靠工作的基石。而分频器正是我们根据这颗“主心跳”去生成其他“辅助心跳”的关键模块。它不仅仅是计数器那么简单它涉及到时钟的占空比、相位抖动、毛刺抑制等一系列工程问题。特别是当分频系数不是简单的2的N次幂时比如分频比是3、5、7甚至是3.5这样的小数如何用纯数字逻辑实现就成了考验设计者基本功的试金石。今天我们就抛开那些花哨的高级IP核从最底层的寄存器Register和门电路Gate出发手把手拆解任意分频器偶数、奇数、小数的设计思路、Verilog实现以及那些仿真波形图里不会告诉你的实战坑点。无论你是想驱动一个外设还是为后续学习PLL锁相环打下坚实基础这篇内容都会让你对FPGA的时钟域管理有一个透彻的理解。2. 偶数分频最直观的对称计数器偶数分频是最基础也最符合直觉的一种。所谓偶数分频就是分频系数N为偶数比如2分频、4分频、10分频。它的目标是产生一个占空比为50%的新时钟。为什么强调50%因为在很多同步数字电路中时钟的上升沿和下降沿都可能被用来采样数据一个对称的时钟能提供更均衡的时序裕量。2.1 核心原理与经典实现偶数分频的本质就是一个模N计数器。当N6时意味着我们需要对原时钟CLK每6个周期计数一次并在计数值达到特定值时翻转输出时钟CLK_OUT的电平从而产生一个周期为原时钟6倍的新时钟。最直接的做法是使用一个从0计数到(N/2 - 1)的计数器。以6分频为例N6N/23。我们让计数器在0, 1, 2这三个状态循环。当计数器达到最大值2时将输出时钟翻转同时计数器归零。这样输出时钟会在每3个原时钟周期翻转一次从而形成一个完整的周期高电平3个周期低电平3个周期占空比正好是50%。让我们用Verilog来描述这个逻辑。首先我们需要一个寄存器cnt来做计数器另一个寄存器clk_out_reg来寄存分频后的时钟输出。module even_divider #( parameter N 6 // 分频系数必须为偶数 )( input wire clk, input wire rst_n, // 低电平复位 output reg clk_out ); reg [31:0] cnt; // 计数器位宽根据N的大小调整 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; clk_out 0; end else begin // 计数器逻辑 if (cnt (N/2 - 1)) begin cnt 0; clk_out ~clk_out; // 达到半周期时翻转时钟 end else begin cnt cnt 1; end end end endmodule这段代码非常清晰。在复位状态下计数器和输出都清零。每个原时钟上升沿计数器加1。当计数器计到(N/2 - 1)对于N6就是2时在下一个时钟沿计数器归零同时输出时钟取反。这样就实现了6分频。2.2 仿真验证与波形解读理论很完美但我们还是要用仿真说话。我们用ModelSim或Vivado自带的仿真器给模块一个50MHz周期20ns的clk看clk_out的输出。你会看到clk_out的一个完整周期从上升沿到下一个上升沿正好是6个clk周期即120ns。其高电平和低电平的持续时间都是3个clk周期60ns占空比50%。这完全符合预期。注意这里有一个初学者极易忽略的细节——输出时钟clk_out的翻转发生在cnt判断为最大值之后的那个clk上升沿。这意味着clk_out的边沿与原clk的边沿是对齐的更准确地说是延迟了一个clk的传播延迟。这在单一时钟域内没有问题但如果你需要将这个clk_out作为一个新的时钟域去驱动其他模块就必须考虑这个延迟带来的时序问题。通常我们会用PLL来生成真正全局、低抖动的时钟而分频器产生的信号更多是作为“使能信号”来使用这一点我们后面会详细讨论。2.3 参数化设计与边界思考上面的模块使用了parameter N这很好使得模块可以灵活配置为任意偶数分频。但这里有几个工程上的思考点计数器位宽代码中我用了reg [31:0] cnt32位对于任何实际的分频系数都绰绰有余。但在资源受限的FPGA中这是一种浪费。更专业的做法是根据参数N动态计算所需的位宽。例如可以添加一个localparam CNT_WIDTH $clog2(N)然后用reg [CNT_WIDTH-1:0] cnt来声明。$clog2是Verilog系统函数用于计算以2为底的对数并向上取整。奇数N的防范虽然模块名叫even_divider但万一用户不小心将N设为奇数比如5我们的代码(N/2 - 1)在整数除法下会得到2-11导致实际分频比变成4因为计数器从0到1每2个周期翻转一次这显然不是我们想要的。一种健壮的做法是在模块内部加入断言Assertion或生成错误提示或者在综合前用if (N % 2 ! 0)的generate语句来报错。输出寄存我们的clk_out是直接由寄存器驱动的这很好避免了组合逻辑产生的毛刺时钟。这是数字时钟设计的一条铁律时钟信号必须由寄存器输出绝不允许经过组合逻辑。任何组合逻辑的延迟和竞争都会导致时钟抖动Jitter甚至毛刺Glitch进而引发灾难性的时序违例。3. 奇数分频挑战50%占空比的实现艺术当分频系数N为奇数时想要得到50%的占空比情况就变得有趣了。你无法像偶数分频那样在计数到N/2时进行翻转因为N/2不是整数。例如5分频一个周期是5个原时钟周期50%占空比要求高电平持续2.5个周期这在数字电路中是无法直接实现的因为我们只能以整个时钟周期为单位。那么我们是不是只能放弃50%占空比呢并不是。数字电路是离散的但我们可以通过一些“技巧”从宏观上逼近50%的占空比或者更准确地说产生一个在时钟沿上完全对称的波形。核心思路是产生两个相位差180度的N分频信号然后将它们进行逻辑操作。3.1 双计数器相位合成法这是最经典且可靠的奇数分频方法。我们以5分频N5为例详细拆解步骤生成两个中间信号clk_p在clk上升沿触发的计数器控制下产生一个占空比为(N-1)/2N : (N1)/2N的N分频信号。对于N5即高电平占2/5低电平占3/5。clk_n在clk下降沿触发的计数器控制下产生一个与clk_p波形相同但相位延迟半个原时钟周期的信号。合成最终时钟将clk_p和clk_n进行“或”OR或者“与”AND操作就能得到一个占空比为50%的N分频时钟。如果两个中间信号的高电平部分不重叠采用“或”运算可以将它们的高电平部分拼接起来。更常见的是让两个信号都是低电平时有一段重叠然后用“与”运算取出它们共同为高的部分。对于5分频经过精心设计计数器翻转点可以使clk_p和clk_n的高电平部分在中间区域重叠取“与”后恰好得到一个高电平宽度为2.5个原时钟周期宏观上的脉冲。听起来有点绕我们来看代码和波形这是理解的关键。module odd_divider #( parameter N 5 // 分频系数必须为奇数 )( input wire clk, input wire rst_n, output wire clk_out ); reg [31:0] cnt_p, cnt_n; // 上升沿和下降沿计数器 reg clk_p_reg, clk_n_reg; // 两个中间寄存器时钟 // 上升沿计数器与 clk_p 生成 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_p 0; clk_p_reg 0; end else begin if (cnt_p N-1) begin cnt_p 0; end else begin cnt_p cnt_p 1; end // 控制 clk_p 的翻转点在0和 (N-1)/2 处翻转 if (cnt_p (N-1)/2) begin clk_p_reg 1; end else if (cnt_p N-1) begin clk_p_reg 0; end end end // 下降沿计数器与 clk_n 生成 always (negedge clk or negedge rst_n) begin if (!rst_n) begin cnt_n 0; clk_n_reg 0; end else begin if (cnt_n N-1) begin cnt_n 0; end else begin cnt_n cnt_n 1; end // 控制 clk_n 的翻转点逻辑与上升沿部分一致 if (cnt_n (N-1)/2) begin clk_n_reg 1; end else if (cnt_n N-1) begin clk_n_reg 0; end end end // 合成最终时钟使用AND逻辑 assign clk_out clk_p_reg clk_n_reg; endmodule我们来解读一下这段代码的关键点。对于N5(N-1)/2 2。在上升沿进程里cnt_p从0计数到4。当cnt_p 2时clk_p_reg拉高当cnt_p 4时clk_p_reg拉低。这样clk_p_reg的高电平持续时间为cnt_p从2到4不包括4即2个原时钟周期。同理clk_n_reg在下降沿做同样的事情但它的边沿相对于clk_p_reg延迟了半个原时钟周期。现在看assign clk_out clk_p_reg clk_n_reg;。只有clk_p_reg和clk_n_reg同时为高时clk_out才为高。通过仿真波形可以清晰看到这两个信号的高电平部分会在中间区域重叠重叠的宽度恰好是clk_p_reg高电平宽度2周期减去半个周期即1.5个周期等等这里需要仔细计算。实际上由于clk_n_reg滞后半个周期它们的重叠部分是从clk_p_reg上升沿之后半个周期开始到clk_n_reg下降沿之前半个周期结束。对于5分频这个重叠的高电平持续时间正好是2.5个原时钟周期而低电平持续时间也是2.5个周期宏观上实现了50%占空比。3.2 仿真分析与注意事项用仿真工具跑一下你会看到一个完美的5分频时钟周期是原时钟的5倍高电平和低电平“看起来”各占一半。但如果你用光标仔细测量会发现clk_out的上升沿和下降沿并不是对齐在原时钟的上升沿或下降沿而是发生在clk_p_reg和clk_n_reg变化的时刻这些时刻可能位于原时钟的上升沿和下降沿之间。这是奇数分频器一个非常重要的特性它的边沿与原时钟边沿不再同步。这带来了两个后果优点输出时钟的抖动相邻周期之间的偏差理论上可以小于一个原时钟周期因为它的边沿由两个沿上升沿和下降沿的事件共同决定相当于利用了双倍的时间分辨率。缺点如果直接将这个clk_out作为另一个时钟域的主时钟其与源时钟clk的相位关系是不固定的取决于具体的翻转逻辑这会给跨时钟域设计带来很大麻烦。因此奇数分频器产生的信号强烈建议仅用作“时钟使能”信号而不是真正的全局时钟网络。例如你可以用原时钟clk来驱动所有寄存器然后生成一个clk_en信号每5个周期有效一次用这个clk_en去使能那些需要低频操作的逻辑。这样整个系统仍然同步于同一个主时钟避免了棘手的跨时钟域问题。4. 小数分频用整数逼近分数的数字魔法小数分频比如3.5分频、4.2分频听起来有点反直觉——时钟周期怎么能是半个周期呢在数字电路中我们确实无法直接生成一个周期为3.5个clk的时钟。但是我们可以通过一种“平均”的思想来实现在一段较长时间内让输出时钟的周期数与原时钟周期数之比精确地等于所需的小数分频比。例如要实现3.5分频即输出频率是输入频率的1/3.5 2/7。换句话说每7个输入时钟周期应该产生2个输出时钟周期。这等价于一个分数分频比 M/N 2/7。我们常用的方法是双模分频法也称为吞脉冲技术。4.1 双模分频法原理剖析我们以3.5分频为例目标是在长时间统计中每7个clk周期对应2个clk_out周期。设分频系数K3.5。我们可以将其拆分为一个整数部分X和一个分数部分。整数部分 X floor(K) 3分数部分 F K - X 0.5双模分频法的精髓在于我们准备两个整数分频器一个按X分频另一个按X1分频。然后通过一个精密的控制器决定在哪些周期使用X分频哪些周期使用X1分频使得长期平均分频系数等于K。如何决定呢我们需要引入一个累加器Accumulator的概念。每个输出周期我们在累加器中加上分数部分F。当累加器累加值超过1时我们就让下一个分频周期使用(X1)分频同时从累加器中减去1相当于进位否则就使用X分频。对于K3.5F0.5初始累加器A0。第一个输出周期A 0 0.5 0.5 1 所以采用3分频。第二个输出周期A 0.5 0.5 1.0 1 所以采用4分频同时A 1.0 - 1 0。第三个输出周期A 0 0.5 0.5 1 采用3分频。第四个输出周期A 0.5 0.5 1.0 1 采用4分频同时A 0。 ...如此循环。你会发现分频模式以[3, 4, 3, 4...]循环。计算平均分频系数(34)/2 3.5完美。4.2 Verilog实现与精度控制下面我们用Verilog实现一个参数化的小数分频器支持定点小数表示。为了便于理解我们假设分数部分用Q格式表示例如对于分频系数K3.5我们输入整数部分INT_PART 3分数部分FRAC_PART 0.5。但在数字电路中我们通常用整数来表示分数比如用8位二进制数表示0.5即8‘b1000_0000因为0.5 128/256。module frac_divider #( parameter INT_PART 3, // 分频系数整数部分 parameter FRAC_BITS 8, // 分数部分位宽 parameter FRAC_VAL 128 // 分数值0.5用 2^(FRAC_BITS-1)表示即128 )( input wire clk, input wire rst_n, output reg clk_out ); localparam ACC_WIDTH FRAC_BITS; // 累加器位宽通常等于分数位宽 reg [ACC_WIDTH-1:0] acc; // 累加器 reg [31:0] cnt; // 分频计数器 reg [31:0] div_num; // 当前周期的分频数INT_PART 或 INT_PART1 wire carry; // 累加器进位标志 // 累加器逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin acc 0; div_num INT_PART; // 初始分频数 end else if (cnt div_num - 1) begin // 当一个分频周期结束时 // 累加分数值 acc acc FRAC_VAL; // 判断是否进位 if (carry) begin div_num INT_PART 1; acc acc FRAC_VAL - (1 FRAC_BITS); // 等价于 acc - 256 end else begin div_num INT_PART; end end end // 进位标志累加器最高位或溢出判断 assign carry (acc FRAC_VAL) (1 FRAC_BITS); // 分频计数器与时钟生成逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; clk_out 0; end else begin if (cnt div_num - 1) begin cnt 0; clk_out ~clk_out; // 翻转输出时钟 end else begin cnt cnt 1; end end end endmodule这段代码的核心是acc累加器和carry标志。FRAC_VAL是分数部分F的定点数表示。每个输出周期结束时cnt div_num - 1acc加上FRAC_VAL。如果相加结果超过了累加器的表示范围即(1 FRAC_BITS)对于8位就是256carry为1表示需要进位下一个分频周期就使用INT_PART1分频并从累加器中减去进制acc acc FRAC_VAL - 256否则使用INT_PART分频。4.3 性能权衡与工程实践小数分频器非常强大但它有两个显著的缺点周期性抖动由于分频系数在X和X1之间切换输出时钟的周期并不是恒定的。例如在3.5分频中输出时钟周期会在3个clk周期和4个clk周期之间交替。这种周期之间的变化称为周期抖动。对于一些对时钟稳定性要求极高的应用如高速ADC采样、精密通信这种抖动是不可接受的。频谱杂散周期性的抖动会在输出时钟的频谱上产生杂散Spur即除了主频之外还会在边带出现一些不需要的频率成分。因此在工程中小数分频器通常用于对时钟纯度要求不高的场合或者作为更高性能频率合成技术如基于Δ-Σ调制的小数分频的基础概念。Δ-Σ调制通过将累加器的进位操作随机化或进行噪声整形将周期性的抖动转化为类似白噪声的随机抖动从而将杂散能量推高到频带之外显著改善了输出时钟的频谱纯度。但这属于更高级的课题通常由专用的频率合成IP核如FPGA内的PLL或DCM来实现。对于我们FPGA开发者而言理解双模分频的原理足以应对大多数需要非整数倍时钟的场景例如生成一个接近标准视频像素时钟的频率。但在最终产品中如果条件允许应优先使用芯片厂商提供的、经过硅片验证的时钟管理单元Clock Management Tile, CMT它们内部集成了高性能的PLL能够生成低抖动、高精度的任意频率时钟远比我们自己写的分频器稳定可靠。5. 从分频器到时钟使能一个更优的设计范式在讲解了三种分频器的实现后我必须强调一个在现代FPGA设计中越来越重要的最佳实践尽量避免使用分频产生的信号作为全局时钟而是使用“时钟使能”信号。为什么原因主要有以下几点时钟偏移Skew管理FPGA内部的全局时钟网络Global Clock Network是经过特殊布线的能够保证时钟信号到达各个寄存器的时间差Skew非常小。如果你用普通逻辑资源查找表LUT和寄存器生成一个clk_out并用它去驱动其他寄存器这个clk_out走的是普通布线资源其延迟大、偏移控制差极易导致建立时间和保持时间违例。静态时序分析STA困难综合和布局布线工具擅长分析单一的、已知的时钟网络。当你使用多个由逻辑产生的时钟时就形成了多个时钟域。工具需要分析这些时钟域之间的路径这会使时序约束变得复杂分析难度增加甚至掩盖真正的时序问题。资源消耗虽然一个分频器本身不大但clk_out作为时钟信号会扇出Fanout到很多寄存器。高扇出的网络需要工具花费大量精力去优化布线可能会占用更多的布线资源影响整体性能。那么“时钟使能”范式如何工作呢其核心思想是整个系统只使用一个主时钟例如50MHz的clk所有寄存器都用这个时钟的上升沿触发。对于需要低频操作的部分我们生成一个周期性的“使能脉冲”clk_en只有当这个脉冲有效时相应的逻辑才进行更新。让我们以生成一个5分频的使能信号为例module clk_en_generator #( parameter DIV 5 )( input wire clk, input wire rst_n, output reg clk_en ); reg [31:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; clk_en 0; end else begin clk_en 0; // 默认使能信号为0 if (cnt DIV - 1) begin cnt 0; clk_en 1; // 在计数到末尾时产生一个周期的高电平脉冲 end else begin cnt cnt 1; end end end endmodule然后在你的功能模块中这样使用module low_speed_module ( input wire clk, input wire rst_n, input wire clk_en_5, // 5分频使能信号 // ... 其他端口 ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin // 复位逻辑 end else if (clk_en_5) begin // 仅当使能有效时才更新 // 你的低频业务逻辑 end end endmodule这样做的好处是巨大的单一时钟域整个设计只有一个主时钟STA非常简单。无时钟偏移问题clk_en是数据信号即使有延迟也只会影响数据路径不会像时钟偏移那样直接影响寄存器的采样窗口。工具友好综合和布局布线工具可以全力优化单一时钟网络。灵活你可以很容易地生成多个不同频率的使能信号clk_en_5,clk_en_7,clk_en_3p5去控制不同的模块而不会引入复杂的时钟交互。因此当你下次需要“分频”时先问问自己我真的需要一个新的时钟吗还是一个使能信号就足够了在绝大多数情况下答案都是后者。这种设计模式是构建稳健、可维护的FPGA系统的基石之一。