3. 分频器设计(一):整数分频器的RTL实现(计数器法、双模预分频)
各位同学,咱们今天聊聊整数分频器。这东西在PLL里太常见了,说白了就是让时钟频率按整数倍降下来。我刚开始做RTL设计那会儿,觉得分频器不就是数数嘛,有啥难的?后来踩了几个坑才明白,这里面的门道还真不少。
3.1 整数分频器的基本原理
整数分频,就是输出频率是输入频率的1/N。N是整数,比如2、3、4、5……你想想看,如果输入是100MHz,N=5,输出就是20MHz。这个逻辑其实很简单:计数器从0数到N-1,每数完一轮,输出翻转一次。
但这里有个关键点——占空比。偶数分频好办,50%占空比轻松搞定。奇数分频呢?嗯,这就有点意思了。
核心公式:
- 偶数分频:N = 2M,计数器范围 0 ~ M-1
- 奇数分频:N = 2M+1,需要组合两个边沿
3.2 计数器法实现分频器
计数器法是最直观的方式。我个人习惯用这种方法做原型验证,因为它逻辑简单,不容易出错。
3.2.1 偶数分频器
先看个2分频的例子。这应该是入门级了,但咱们还是走一遍流程。
module div_even #(
parameter DIV_N = 2 // 分频系数,必须是偶数
)(
input wire clk_in,
input wire rst_n,
output reg clk_out
);
localparam CNT_WIDTH = $clog2(DIV_N/2);
reg [CNT_WIDTH-1:0] cnt;
always @(posedge clk_in or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
clk_out <= 1'b0;
end else begin
if (cnt == (DIV_N/2 - 1)) begin
cnt <= 0;
clk_out <= ~clk_out;
end else begin
cnt <= cnt + 1'b1;
end
end
end
endmodule
这段代码里,我用了$clog2函数自动计算计数器位宽。这个技巧是我从前辈那儿学来的,能避免手动算位宽时出错。计数器从0跑到DIV_N/2-1,然后翻转输出。注意看,这里只用了时钟的上升沿。
小提示:偶数分频时,计数器范围是0到N/2-1,不是0到N-1。很多新手会搞混这个边界条件。
3.2.2 奇数分频器
奇数分频就有点绕了。为什么?因为只用上升沿,你没法得到50%占空比。我记得第一次做3分频时,仿真波形出来占空比是33.3%,被项目经理骂了一顿。
解决方案是:用上升沿和下降沿分别生成两个信号,然后组合。来看代码:
module div_odd #(
parameter DIV_N = 3 // 分频系数,必须是奇数
)(
input wire clk_in,
input wire rst_n,
output wire clk_out
);
localparam CNT_WIDTH = $clog2(DIV_N);
reg [CNT_WIDTH-1:0] cnt_pos, cnt_neg;
reg clk_pos, clk_neg;
// 上升沿计数
always @(posedge clk_in or negedge rst_n) begin
if (!rst_n) begin
cnt_pos <= 0;
clk_pos <= 1'b0;
end else begin
if (cnt_pos == DIV_N-1) begin
cnt_pos <= 0;
end else begin
cnt_pos <= cnt_pos + 1'b1;
end
// 在 (N-1)/2 和 N-1 处翻转
if (cnt_pos == (DIV_N-1)/2 - 1) begin
clk_pos <= 1'b1;
end else if (cnt_pos == DIV_N-2) begin
clk_pos <= 1'b0;
end
end
end
// 下降沿计数
always @(negedge clk_in or negedge rst_n) begin
if (!rst_n) begin
cnt_neg <= 0;
clk_neg <= 1'b0;
end else begin
if (cnt_neg == DIV_N-1) begin
cnt_neg <= 0;
end else begin
cnt_neg <= cnt_neg + 1'b1;
end
if (cnt_neg == (DIV_N-1)/2 - 1) begin
clk_neg <= 1'b1;
end else if (cnt_neg == DIV_N-2) begin
clk_neg <= 1'b0;
end
end
end
// 组合输出
assign clk_out = clk_pos | clk_neg;
endmodule
这里的关键是:上升沿和下降沿各生成一个信号,它们的占空比都是 (N+1)/(2N) 和 (N-1)/(2N)。用或门组合后,就能得到50%占空比。我在项目中遇到过一个问题——如果两个信号的翻转点没对齐,输出会有毛刺。所以一定要确保时序收敛。
注意:奇数分频器的组合输出可能会引入毛刺。建议在输出端加一级寄存器打一拍,或者用门控时钟技术处理。
3.3 双模预分频器
双模预分频器(Dual-Modulus Prescaler)是PLL里的核心模块。它能在两个分频比之间切换,比如除以N或者除以N+1。为什么要这么设计?因为PLL的反馈分频器需要实现小数分频,而双模预分频是实现这个功能的基础。
说白了,双模预分频就是一个可以配置的分频器。你给它一个控制信号,它就能在两种模式间切换。我当年做PLL设计时,这个模块让我折腾了好几个通宵。
3.3.1 工作原理
双模预分频的核心思想是:用两个计数器级联。第一个计数器(吞脉冲计数器)控制第二个计数器(主计数器)的模值。
举个例子,假设我们要实现一个 8/9 双模预分频器:
- 模式0:每8个输入时钟输出1个脉冲
- 模式1:每9个输入时钟输出1个脉冲
控制信号MC(Modulus Control)决定当前是哪种模式。
3.3.2 RTL实现
来看一个简单的 4/5 双模预分频器实现:
module dual_mod_prescaler #(
parameter N = 4 // 基础分频比
)(
input wire clk_in,
input wire rst_n,
input wire mc, // 模控制:0=÷N, 1=÷(N+1)
output reg clk_out
);
reg [1:0] cnt;
always @(posedge clk_in or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
clk_out <= 1'b0;
end else begin
if (mc) begin
// ÷(N+1) 模式
if (cnt == N) begin
cnt <= 0;
clk_out <= 1'b1;
end else begin
cnt <= cnt + 1'b1;
clk_out <= 1'b0;
end
end else begin
// ÷N 模式
if (cnt == N-1) begin
cnt <= 0;
clk_out <= 1'b1;
end else begin
cnt <= cnt + 1'b1;
clk_out <= 1'b0;
end
end
end
end
endmodule
这段代码里,我用了参数化设计,方便调整分频比。注意看,输出脉冲只持续一个时钟周期,不是50%占空比。这是因为双模预分频的输出通常要送给后面的计数器,不需要占空比约束。
设计要点:
- 双模预分频的切换必须无毛刺
- MC信号需要与时钟同步,避免亚稳态
- 输出脉冲宽度为一个输入时钟周期
3.4 知识体系图
下面这张图展示了整数分频器的设计脉络。我习惯用这种图来梳理思路,一目了然。
3.5 避坑指南
做分频器设计,有几个坑我踩过,跟大家分享一下:
- 计数器溢出:我曾经在项目中用了3位计数器做8分频,结果计数器跑到7就归零了,输出频率完全不对。后来养成了用$clog2的习惯。
- 异步复位问题:复位释放时,计数器可能从任意值开始。我建议在复位后先让计数器同步几个周期再使能输出。
- 毛刺处理:奇数分频的组合输出很容易出毛刺。我的经验是:要么在输出端加寄存器,要么用门控时钟。
- 双模切换时序:MC信号必须在时钟稳定后才能切换。我曾经因为MC切换太快,导致预分频器输出了一串乱脉冲。
个人建议:写分频器代码时,先画时序图。把每个信号的翻转点标清楚,再动手写RTL。这样能避免80%的逻辑错误。
好了,整数分频器的内容就讲到这里。计数器法适合小规模分频,双模预分频则是PLL中的关键模块。下节课我们会聊分数分频器的设计,那才是真正考验功底的地方。