
1. 奇数分频一个看似简单却暗藏玄机的设计在数字电路设计尤其是FPGA和ASIC开发中时钟分频是最基础也是最频繁遇到的操作之一。偶数分频很简单一个计数器在时钟上升沿计数计满翻转输出即可。但当你需要得到一个占空比为50%的奇数分频时钟时比如5分频、7分频问题就变得有趣起来了。直接使用单边沿计数器你只能得到占空比非50%的波形这在很多对时钟质量要求严格的场景下是无法接受的。今天我们就来深入拆解一种经典且高效的奇数分频实现方法——基于双边沿触发的“或逻辑”法并以5分频为例从原理、代码到仿真把每一个细节和可能踩的坑都讲清楚。这个方法的核心思想很巧妙既然一个触发器只能在时钟的上升沿或下降沿动作那我们何不用两个触发器一个用上升沿一个用下降沿分别产生两个相位有特定关系的信号再把它们组合起来呢最终我们将得到一个占空比严格为50%的奇数分频时钟。这个方法不仅适用于FPGA在CPLD乃至ASIC设计中都是通用的思路。无论你是正在学习Verilog的在校学生还是需要在实际项目中实现特定时钟需求的工程师理解这个设计的来龙去脉都大有裨益。2. 核心原理为什么双边沿触发是奇数分频的关键要理解这个设计我们得先抛开代码从时钟波形的本质入手。一个理想的50%占空比方波时钟其高电平和低电平的时间是相等的。对于N分频N为奇数输出时钟的一个完整周期需要覆盖输入时钟的N个周期。我们的目标是让这个输出周期的高电平时间精确等于(N/2)*Tclk这里N/2不是整除例如5分频就是2.5个输入时钟周期低电平时间也同样如此。单靠一个上升沿触发的计数器是做不到这一点的。因为计数器的翻转动作只能发生在时钟边沿的离散时刻。例如要实现5分频你可能会让计数器在计到0和2时翻转但这样得到的输出信号其高电平持续2个Tclk低电平持续3个Tclk或反之占空比是40%/60%而不是50%。双边沿触发法的精妙之处在于“相位合成”。我们分别用输入时钟的上升沿和下降沿驱动两个独立的计数器。这两个计数器都产生一个占空比非50%的中间信号通常称为DIV0和DIV1但关键是这两个中间信号的相位是错开的错开的时间正好是半个输入时钟周期因为一个用上升沿一个用下降沿。当我们把这两个相位错开的信号进行“或”操作时它们的高电平部分就会在时间上拼接起来最终形成一个高、低电平持续时间完全对称的波形。以5分频N5为例(N-1)/2 2。我们让每个计数器在计数值为0和1时输出高电平在计数值为2、3、4时输出低电平。这样DIV0和DIV1各自都是占空比40%的波形。但由于DIV1由下降沿触发它的波形相对于DIV0延迟了半个MCLK周期。将DIV0和DIV1做逻辑或它们的高电平区域就会部分重叠、部分衔接最终形成的DIV5_CLK波形其高电平持续时间恰好是2.5个MCLK周期低电平也是2.5个完美实现50%占空比。注意这里(N-1)/2必须是一个整数这也是为什么这个方法天然适用于奇数分频。对于偶数分频(N-1)/2不是整数这个计数策略就不适用了。3. 代码实现与关键细节解析理解了原理我们来看Verilog代码实现。代码是设计思想的直接体现每一行都有其目的。3.1 模块定义与参数化设计首先一个良好的设计应该是参数化的便于复用。我们使用parameter来定义分频系数N和中间值M。timescale 1ns / 1ps module div_odd #( parameter N 5, // 分频系数必须为奇数 parameter CNT_WIDTH 3 // 计数器位宽根据N大小调整需满足 2^CNT_WIDTH N ) ( input wire MCLK, // 主时钟输入 output wire DIV_CLK, // N分频时钟输出 output wire DIV0, // 上升沿路径中间信号调试用 output wire DIV1 // 下降沿路径中间信号调试用 ); localparam M (N-1)/2; // 高电平计数阈值这里我做了几点优化和说明参数化N和计数器位宽CNT_WIDTH都作为参数使得模块可以轻松改为3分频、7分频等。M通过localparam自动计算避免手动修改出错。位宽定义显式定义了计数器位宽。对于5分频计数器需计到43位宽足够。如果N很大需要相应增加CNT_WIDTH。输出信号DIV0和DIV1作为输出端口在调试时非常有用可以直观看到两个中间信号的波形便于验证设计。在实际产品中如果不需要观察可以去掉。3.2 核心计数器与信号生成逻辑这是整个设计的核心部分包含了两个分别由上升沿和下降沿触发的always块。reg [CNT_WIDTH-1:0] count0, count1; reg div0_reg, div1_reg; // 上升沿计数器与DIV0生成 always (posedge MCLK) begin if (count0 M-1) begin // 计数到M-1时下一个时钟沿DIV0变低 div0_reg 1b0; count0 count0 1; end else if (count0 N-1) begin // 计数到N-1一个分频周期结束时计数器归零DIV0变高 div0_reg 1b1; count0 0; end else begin // 其他情况计数器递增 count0 count0 1; end end // 下降沿计数器与DIV1生成 always (negedge MCLK) begin if (count1 M-1) begin div1_reg 1b0; count1 count1 1; end else if (count1 N-1) begin div1_reg 1b1; count1 0; end else begin count1 count1 1; end end逻辑要点解析计数范围计数器从0计数到N-1对于5分频是0~4这是一个完整的输入时钟周期数。翻转点信号在计数到M-1对于5分频是1时拉低在计数到N-1对于5分频是4时拉高并复位计数器。这意味着DIV0/DIV1的高电平持续了M个计数周期0和1低电平持续了N-M个计数周期2,3,4。这正是产生占空比为M/N即2/5中间波形的关键。阻塞与非阻塞赋值这里全部使用了非阻塞赋值()。这是描述时序逻辑的标准做法能正确模拟寄存器行为避免仿真与综合结果不一致的“陷阱”。关于这个“坑”后面会详细展开。3.3 最终输出组合逻辑两个中间信号生成后通过一个简单的组合逻辑“或门”产生最终的分频时钟。// 将寄存器输出连接到端口 assign DIV0 div0_reg; assign DIV1 div1_reg; // 关键步骤通过或操作合成最终50%占空比时钟 assign DIV_CLK div0_reg | div1_reg; endmodule这一行assign DIV_CLK div0_reg | div1_reg;是整个设计的“画龙点睛”之笔。它利用数字逻辑中最基本的或门将两个相位错开半个周期的波形“缝合”起来生成了我们需要的完美时钟。在FPGA中这个操作会由查找表LUT实现速度极快。4. 深入仿真波形分析与设计验证理论分析和代码编写完成后必须通过仿真来验证。我们使用ModelSim、Vivado Simulator或任何支持Verilog的仿真工具进行测试。4.1 正确的仿真波形我们为测试平台提供一個100MHz周期10ns的MCLK观察5分频后的DIV_CLK波形。timescale 1ns/1ps module tb_div_odd(); reg clk; wire div_clk, div0, div1; div_odd #(.N(5)) uut ( .MCLK(clk), .DIV_CLK(div_clk), .DIV0(div0), .DIV1(div1) ); initial begin clk 0; forever #5 clk ~clk; // 生成100MHz时钟 end initial begin #200; // 仿真运行一段时间 $finish; end endmodule仿真得到的波形应该是这样的MCLK: 周期10ns的方波。DIV0: 以MCLK上升沿为基准。在count0为0和1时高电平持续20ns在count0为2、3、4时低电平持续30ns。周期为50ns5个MCLK周期。DIV1: 波形与DIV0完全相同但每个边沿都相对于DIV0延迟了5ns半个MCLK周期。DIV_CLK: 是DIV0和DIV1的“或”。你会发现它的高电平期和低电平期都精确地持续了25ns周期为50ns实现了50%占空比的5分频20MHz。通过测量DIV_CLK的周期和占空比可以定量验证设计是否正确。这是硬件设计中最有说服力的一环。4.2 一个致命的陷阱阻塞与非阻塞赋值在提供的原始资料中作者提到了一个关键错误错误地使用了非阻塞赋值。让我们重现这个错误并理解为什么它会导致失败。错误代码示例always(posedge MCLK) begin if(COUNT0M) // M2 begin DIV00; end else if(COUNT0N) // N5 begin COUNT00; DIV01; end COUNT0COUNT01; // 问题在这里 end错误分析在同一个always块中所有非阻塞赋值语句在时钟沿到来时是“并发”执行的。仿真器会先读取所有等式右边的值旧值然后在时间步结束时统一更新左边的寄存器。当COUNT0旧值等于4时下一个时钟上升沿到来。仿真器评估COUNT05吗不成立旧值是4。COUNT02吗不成立。所以两个if都不执行。仿真器评估COUNT0COUNT01;右边是415。时间步结束COUNT0被更新为5DIV0保持不变。关键点来了COUNT0是在这个时钟沿之后才变成5的所以COUNT05这个条件在这个时钟沿永远不会被触发。计数器会一直累加下去永远不会清零DIV0也永远不会被置1整个状态机就“跑飞”了。正确的理解是在描述时序逻辑always (posedge clk)时如果你想根据计数器当前值即本次时钟沿到来时的值来决定其下一个值那么对计数器的判断和更新必须在逻辑上连贯。通常有两种正确写法使用阻塞赋值的顺序执行较少见需谨慎如原始资料中第一个正确示例COUNT0 COUNT0 1;是阻塞赋值它会立刻更新COUNT0的值使得后续的if(COUNT05)能判断到。使用非阻塞赋值的标准写法推荐如我在3.2节给出的代码。将计数器加1和条件判断基于同一个count0的旧值。在count0 N-1时我们将其下一个值设为0否则设为count0 1。这样写清晰、标准综合工具也能很好地处理。实操心得在Verilog中牢记“时序逻辑用非阻塞组合逻辑用阻塞”这条黄金法则可以避免90%以上的仿真与综合不一致问题。对于计数器在同一个always块内其“下一个状态”应完全由“当前状态”决定所有对它的赋值都应使用非阻塞并且赋值来源是它的旧值。5. 工程设计考量与优化技巧将代码成功仿真只是第一步要将其变成可靠硬件还需要考虑更多实际问题。5.1 时钟约束与时序分析这是该方法最重要的一点。DIV_CLK DIV0 | DIV1是一个组合逻辑路径。DIV0和DIV1由不同的时钟边沿产生到达或门的时间可能有微小差异。问题如果DIV0或DIV1到DIV_CLK的路径延迟过大会导致DIV_CLK输出出现毛刺glitch。一个短暂的毛刺对于将DIV_CLK用作时钟信号是灾难性的可能触发错误的逻辑。解决方案添加约束在综合和布局布线工具如Vivado、Quartus中需要对DIV0和DIV1到DIV_CLK的路径设置最大延迟约束。告诉工具这条路径必须非常快通常要小于目标时钟周期的几分之一例如对于100MHz的DIV_CLK这条路径延迟要小于1-2ns。寄存器输出最稳健的方法是使用一个额外的寄存器来采样DIV_CLK。虽然这会引入一个时钟周期的延迟但能消除所有毛刺得到干净稳定的时钟输出。这对于高速系统至关重要。// 增加一级寄存器输出以消除毛刺 reg div_clk_reg; always (posedge MCLK or negedge MCLK) begin // 注意这里用了双沿触发器有些FPGA原生支持 div_clk_reg div0_reg | div1_reg; end assign DIV_CLK div_clk_reg;实际上更常见的做法是选择MCLK或DIV_CLK的一个边沿来寄存。这需要仔细分析时序关系。5.2 高频应用下的注意事项原始资料提到“MCLK频率要求较高尽量不要出现窄脉冲”。这里指的是什么 当DIV0和DIV1的边沿非常接近时它们通过或门产生的DIV_CLK脉冲宽度可能会非常窄。如果这个窄脉冲的宽度小于后续电路如其他触发器的建立/保持时间要求就会导致功能异常。根源DIV0和DIV1的跳变沿本身是由MCLK的上升沿和下降沿产生的理论上间隔Tclk/2。但如果两条路径的延迟不匹配由于布局布线导致这个间隔可能缩小产生窄脉冲。应对策略平衡布局在综合约束中可以将产生DIV0和DIV1的触发器放在彼此靠近的位置并使用相同的布线资源以最小化路径延迟差异。使用全局时钟网络如果DIV_CLK需要驱动较大负载或长距离线路应将其通过FPGA的全局时钟缓冲器BUFG后再输出。全局时钟网络具有低偏移、高驱动能力的特性。降低源时钟频率如果可能这是最直接的方法。或者考虑使用其他奇数分频架构例如基于状态机的设计。5.3 资源与性能权衡COUNT1的必要性原始资料提到“COUNT1可有可无”。在低速情况下DIV1的计数器可以简化甚至可以用DIV0反相后延迟得到。但在高速或高可靠性设计中使用独立的下降沿计数器是最规范的做法它能保证两个路径的对称性和可约束性。通用性修改本文的代码是参数化的要改为7分频只需例化时修改参数div_odd #(.N(7), .CNT_WIDTH(3)) uut (...);。注意CNT_WIDTH要能覆盖计数值7分频需计到63位宽足够。6. 常见问题与调试技巧实录在实际实现过程中你可能会遇到以下问题问题1仿真波形正确但下载到FPGA后功能不正常。可能原因最常见的元凶就是时序违例特别是DIV_CLK组合路径上的毛刺。排查步骤检查综合和布局布线报告中的“时序总结”Timing Summary看是否有建立时间Setup Time或保持时间Hold Time违例。使用集成逻辑分析仪如Vivado的ILA、Quartus的SignalTap抓取实际FPGA内部的DIV0、DIV1和DIV_CLK信号。观察DIV_CLK上是否有毛刺。如果发现毛刺按照5.1节的方法添加输出寄存器或加强时序约束。问题2分频比不是预期的5而是3或其他值。可能原因计数器判断条件写错。例如把if (count0 N-1)误写为if (count0 N)会导致计数器多计一个数。排查步骤仔细检查always块中的判断条件。牢记计数器是从0开始计数。在仿真中观察count0和count1的数值变化看它们是否在正确的数值复位。问题3占空比不是精确的50%。可能原因DIV0和DIV1的高电平计数周期M计算错误。M必须等于(N-1)/2。排查步骤确认参数N是奇数。确认M的计算是正确的整数。例如7分频时(7-1)/23高电平应持续3个MCLK周期。仿真测量DIV0和DIV1的高电平时间是否正确。调试技巧充分利用中间信号在设计时像本例一样输出DIV0和DIV1是调试此类分频电路的利器。在仿真和ILA中同时观察这三个信号可以迅速定位问题是出在计数器部分看DIV0/DIV1波形是否正确还是出在组合逻辑部分看DIV_CLK是否由DIV0/DIV1正确合成。7. 方法对比与扩展思考除了这种双边沿“或逻辑”法实现奇数分频还有其他方法各有优劣状态机法使用一个状态机状态数为N每个状态对应一个时钟周期直接控制输出时钟的高低。这种方法逻辑清晰占空比精确且没有组合逻辑毛刺问题。但资源消耗相对较大特别是N较大时。小数分频与DDS对于更复杂的分频需求如小数分频通常会采用基于累加器的直接数字频率合成DDS技术通过控制相位累加器的溢出和输出波形表来产生任意频率的时钟精度非常高但设计也更复杂。如何选择对于简单的、固定的低阶奇数分频如3、5、7本文介绍的双边沿“或逻辑”法非常简洁高效。对于需要动态改变分频比或者分频系数较大的情况状态机法可能更易于管理和验证。对于需要高精度、任意频率包括小数时钟生成的场景DDS是标准选择。最后我想强调的是硬件设计不仅仅是写出能仿真的代码更要考虑它在硅上的真实行为。时序约束、时钟质量、资源利用这些都是在项目实践中需要反复权衡的。这个奇数分频的设计虽然小巧但它完美地体现了数字逻辑设计中“用巧劲”的思想。理解并掌握它对你构建更复杂、更可靠的数字系统大有帮助。在实际项目中如果对时钟质量要求极高我通常会倾向于在FPGA内部使用专用的时钟管理模块如PLL或MMCM来产生所需时钟它们能提供更优的抖动和占空比性能。但当资源紧张或需要动态控制时这种纯逻辑的分频方法依然是不可或缺的利器。