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 知识体系图

下面这张图展示了整数分频器的设计脉络。我习惯用这种图来梳理思路,一目了然。

整数分频器设计知识体系 整数分频器 计数器法 双模预分频 偶数分频 奇数分频 ÷N 模式 ÷(N+1) 模式 50% 占空比 双沿组合 MC控制切换 吞脉冲技术 核心目标:实现精确的整数倍时钟分频

3.5 避坑指南

做分频器设计,有几个坑我踩过,跟大家分享一下:

  • 计数器溢出:我曾经在项目中用了3位计数器做8分频,结果计数器跑到7就归零了,输出频率完全不对。后来养成了用$clog2的习惯。
  • 异步复位问题:复位释放时,计数器可能从任意值开始。我建议在复位后先让计数器同步几个周期再使能输出。
  • 毛刺处理:奇数分频的组合输出很容易出毛刺。我的经验是:要么在输出端加寄存器,要么用门控时钟。
  • 双模切换时序:MC信号必须在时钟稳定后才能切换。我曾经因为MC切换太快,导致预分频器输出了一串乱脉冲。

个人建议:写分频器代码时,先画时序图。把每个信号的翻转点标清楚,再动手写RTL。这样能避免80%的逻辑错误。

好了,整数分频器的内容就讲到这里。计数器法适合小规模分频,双模预分频则是PLL中的关键模块。下节课我们会聊分数分频器的设计,那才是真正考验功底的地方。

可可精品资料库