分频器实战:从秒脉冲到任意分频的Verilog实现与仿真

📅 2026/6/28 21:27:10
分频器实战:从秒脉冲到任意分频的Verilog实现与仿真
1. 分频器基础概念与秒脉冲生成在数字电路设计中分频器是最基础也最常用的模块之一。简单来说分频器的作用就是将输入时钟信号的频率降低到我们需要的频率。比如把24MHz的时钟变成1Hz的秒脉冲这就是典型的秒分频应用。我刚开始接触FPGA时第一个实验就是实现秒脉冲生成。当时用的是经典的50MHz开发板需要从50MHz分频到1Hz。这个过程中踩过不少坑比如计数器位数不够导致分频错误或者复位信号处理不当导致计数器无法正常工作。下面是一个典型的秒脉冲生成模块的Verilog实现以24MHz时钟为例module second_pulse( input clk, // 24MHz系统时钟 input rst_n, // 低电平复位 output reg pulse // 秒脉冲输出 ); reg [24:0] counter; // 24MHz时钟需要25位计数器 always (posedge clk or negedge rst_n) begin if(!rst_n) begin counter 0; pulse 0; end else begin if(counter 24_000_000 - 1) begin // 计数到24M-1 counter 0; pulse 1; // 产生1个时钟周期的高电平 end else begin counter counter 1; pulse 0; end end end endmodule这个模块的工作原理很简单每当计数器计满24,000,000个时钟周期正好1秒就输出一个时钟周期的高电平脉冲。我在实际项目中发现这种设计有个小问题如果时钟频率不是整数MHz计算起来会比较麻烦。后来我改进成了参数化的设计可以根据不同的输入频率自动计算计数值。2. 偶数分频的实现技巧偶数分频是最简单的分频方式比如2分频、4分频、6分频等。它的特点是实现简单而且可以保证输出时钟的占空比为50%。这对于很多同步电路设计来说非常重要。我常用的偶数分频实现方法是这样的对于N分频N为偶数我们只需要在计数器计到N/2-1时翻转输出时钟信号即可。比如要实现6分频module even_divider #( parameter DIV 6 // 分频系数必须为偶数 )( input clk, input rst_n, output reg clk_out ); reg [3:0] counter; // 计数器 always (posedge clk or negedge rst_n) begin if(!rst_n) begin counter 0; clk_out 0; end else if(counter (DIV/2 - 1)) begin counter 0; clk_out ~clk_out; // 翻转时钟 end else begin counter counter 1; end end endmodule在实际工程中我遇到过一个问题当分频系数很大时比如1000分频直接用这种方法会导致计数器位数很大。后来我发现可以用两级分频来解决比如先10分频再100分频这样总共就是1000分频但每级计数器只需要很少的位数。3. 奇数分频的两种实现方案奇数分频如3分频、5分频比偶数分频复杂一些主要难点在于如何保持50%的占空比。经过多次实践我总结出两种可靠的实现方法。3.1 双计数器法第一种方法使用两个计数器分别在时钟的上升沿和下降沿工作module odd_divider #( parameter DIV 5 // 分频系数奇数 )( input clk, input rst_n, output clk_out ); reg [3:0] cnt_p, cnt_n; // 上升沿和下降沿计数器 reg clk_p, clk_n; // 两个中间时钟 // 上升沿计数器 always (posedge clk or negedge rst_n) begin if(!rst_n) begin cnt_p 0; clk_p 0; end else if(cnt_p DIV - 1) begin cnt_p 0; clk_p ~clk_p; end else begin cnt_p cnt_p 1; end end // 下降沿计数器 always (negedge clk or negedge rst_n) begin if(!rst_n) begin cnt_n 0; clk_n 0; end else if(cnt_n DIV - 1) begin cnt_n 0; clk_n ~clk_n; end else begin cnt_n cnt_n 1; end end assign clk_out clk_p | clk_n; // 组合输出 endmodule这种方法通过将上升沿和下降沿生成的时钟信号进行或运算可以得到完美的50%占空比。我在一个SPI接口项目中就使用了这种5分频设计效果很好。3.2 半整数分频法第二种方法更巧妙先进行(N-1)/2 0.5分频再进行2分频module odd_divider_alt #( parameter DIV 7 )( input clk, input rst_n, output reg clk_out ); reg [3:0] counter; reg clk_half; // 生成 (DIV-1)/2 0.5 分频 always (posedge clk or negedge rst_n) begin if(!rst_n) begin counter 0; clk_half 0; end else if(counter DIV - 1) begin counter 0; clk_half ~clk_half; end else begin counter counter 1; if(counter (DIV-1)/2) clk_half ~clk_half; end end // 再进行2分频 always (posedge clk_half or negedge rst_n) begin if(!rst_n) clk_out 0; else clk_out ~clk_out; end endmodule这种方法代码更简洁但理解起来需要一些技巧。我在一个音频处理项目中对比过两种方法发现第二种方法在资源利用上更高效。4. 任意整数分频的通用实现在实际项目中我们经常需要实现任意整数分频包括奇数和偶数。下面分享一个我经过多次优化后的通用分频器设计module universal_divider #( parameter DIV 10 // 任意正整数分频系数 )( input clk, input rst_n, output reg clk_out ); reg [31:0] counter; reg clk_p, clk_n; generate if(DIV 1) begin // 1分频特殊情况 always (posedge clk or negedge rst_n) begin if(!rst_n) clk_out 0; else clk_out clk; end end else if(DIV[0] 0) begin // 偶数分频 always (posedge clk or negedge rst_n) begin if(!rst_n) begin counter 0; clk_out 0; end else if(counter DIV/2 - 1) begin counter 0; clk_out ~clk_out; end else begin counter counter 1; end end end else begin // 奇数分频 // 上升沿计数器 always (posedge clk or negedge rst_n) begin if(!rst_n) begin counter 0; clk_p 0; end else if(counter DIV - 1) begin counter 0; clk_p ~clk_p; end else begin counter counter 1; if(counter (DIV-1)/2) clk_p ~clk_p; end end // 下降沿计数器 always (negedge clk or negedge rst_n) begin if(!rst_n) begin clk_n 0; end else begin if(counter (DIV-1)/2) clk_n ~clk_n; end end assign clk_out clk_p | clk_n; end endgenerate endmodule这个设计有几个关键点使用generate语句根据分频系数的奇偶性选择不同的实现方式对于偶数分频采用简单的计数器翻转法对于奇数分频结合了双沿计数和半整数分频的优点特殊处理了1分频的情况在最近的一个多时钟域项目中这个通用分频器帮了大忙可以灵活地生成各种需要的时钟频率。不过要注意这种数分频产生的时钟可能会有较大的抖动对于高精度时序要求的场合建议使用PLL等模拟方式。