FPGA 实现无毛刺时钟切换

实现无毛刺时钟切换

无毛刺时钟切换的目标是在切换时钟源时,确保输出时钟不会产生小于一个周期的窄脉冲(毛刺),从而避免下游电路发生功能错误。这是一个非常经典的数字电路设计问题。在DFS/DVS/DVFS (Dynamic Frequency/Voltage Scaling)等对功耗要求比较高的系统中,无毛刺时钟切换尤为重要。


1. 问题的根源:为什么会有毛刺?

假设有两个不同频率或相位的时钟源 CLK1CLK2 ,通过一个选择信号 sel 控制多路器(MUX)输出:

verilog

assign clk_out = (sel == 1'b0) ? CLK1 : CLK2; // 这是“有毛刺”的写法

毛刺产生原因
sel 是异步信号,可能与当前或目标时钟都不同步。如果在时钟高电平时切换,MUX的输出可能在切换瞬间产生一个“断开-连接”的毛刺。例如,当 CLK1=1, CLK2=0, sel 从 0 变为 1 时,输出可能瞬间从 1 变到 0 再变到 ?(取决于路径延迟),产生一个下降毛刺。


2. 核心解决方案:时钟门控同步技术

无毛刺切换的核心思想是: 在目标时钟关闭(低电平)时,才允许切换选择信号,并在切换完成后,再打开目标时钟 。这需要一套同步和握手逻辑。

最经典、最可靠的电路结构如下:

电路框图与工作原理

text

                    ___________        ___________
        CLK1 -----|           |      |           |
                  | 时钟门控   |------|           |
                  | 单元 CG1   |      |           |      _________
        en1 -------|__________|      |           |     |         |
                                   | 逻辑   |-----| 2-input |---> clk_out
                    ___________    |  OR    |     |   OR    |
        CLK2 -----|           |    |        |     |_________|
                  | 时钟门控   |------|           |
                  | 单元 CG2   |      |           |
        en2 -------|__________|      |___________|
  1. 关键信号
  • sel: 原始的、可能异步的时钟选择信号。
  • en1, en2: 分别控制 CLK1 和 CLK2 的门控使能信号。 en1en2 绝对不能同时为高 ,否则两个时钟会短接竞争。
  • clk_out: 最终的无毛刺输出时钟。
  1. 核心步骤(以从 CLK1 切换到 CLK2 为例)
  • 步骤 1:关闭当前时钟 (CLK1) 。系统检测到 sel 的变化(想选 CLK2),它不会立刻行动。它首先 在 CLK1 的同步域下 ,将 en1 拉低。由于 en1 连接到 CLK1 的门控单元,这个“关闭”命令会确保在 CLK1 变为低电平时才生效,并在 CLK1 保持低电平时,clk_out 来自 CLK1 的部分被安全关闭。此时 clk_out 输出为低(因为两个使能都低,OR 门输出低)。
  • 步骤 2:同步与握手sel 变化和 en1 变低的信息,被同步到 CLK2 的时钟域。
  • 步骤 3:开启目标时钟 (CLK2) 。在 CLK2 的时钟域,确认 en1 已无效(即 CLK1 已关闭)且 sel 选择 CLK2 后,才将 en2 拉高。en2 连接到 CLK2 的门控单元,这个“开启”命令会确保在 CLK2 为低电平时生效,然后在 CLK2 的下一个上升沿,clk_out 干净地开始输出 CLK2 的波形。

最终效果clk_out 在切换过程中,会有一个完整的低电平“空白期”,之后由新的时钟源从上升沿开始干净地启动。 完全避免了中间产生毛刺的可能


3. 可综合的 Verilog 代码示例

以下是基于上述原理的一个经典实现。注意:实际时钟门控单元通常由综合工具或标准单元库提供(如 CLKGATE),这里用逻辑描述其行为。

verilog

module glitch_free_clk_switch (
    input  wire clk1,
    input  wire clk2,
    input  wire rst_n,
    input  wire sel,         // async select, 1=clk2, 0=clk1
    output wire clk_out
);

    // ========== 信号声明 ==========
    // CLK1域信号
    reg  req1;                // 请求关闭CLK1
    reg  [2:0] sync_req2_to_clk1;  // 同步链:将CLK2的req2同步到CLK1域
    reg  ack1;                // 确认CLK2已准备好开启(在CLK1域生成)
    reg  en1;                 // CLK1使能
  
    // CLK2域信号  
    reg  req2;                // 请求开启CLK2
    reg  [2:0] sync_req1_to_clk2;  // 同步链:将CLK1的req1同步到CLK2域
    reg  ack2;                // 确认CLK1已关闭(在CLK2域生成)
    reg  en2;                 // CLK2使能
  
    // 选择信号同步
    reg  [1:0] sync_sel_clk1, sync_sel_clk2;

    // ========== 选择信号同步 ==========
    // 同步sel到CLK1域(用于生成req1)
    always @(posedge clk1 or negedge rst_n) begin
        if (!rst_n)
            sync_sel_clk1 <= 2'b00;
        else
            sync_sel_clk1 <= {sync_sel_clk1[0], sel};
    end
  
    // 同步sel到CLK2域(用于生成req2)
    always @(posedge clk2 or negedge rst_n) begin
        if (!rst_n)
            sync_sel_clk2 <= 2'b00;
        else
            sync_sel_clk2 <= {sync_sel_clk2[0], sel};
    end

    // ========== CLK1域逻辑 ==========
    always @(posedge clk1 or negedge rst_n) begin
        if (!rst_n) begin
            req1  <= 1'b0;
            sync_req2_to_clk1 <= 3'b000;
            ack1  <= 1'b0;
            en1   <= 1'b1;          // 复位后默认选择CLK1
        end else begin
            // --- Step 1: 同步req2到CLK1域(2级同步)---
            sync_req2_to_clk1 <= {sync_req2_to_clk1[1:0], req2};
        
            // --- Step 2: 生成ack1 ---
            // ack1表示"CLK2已准备好开启"
            // 当检测到req2有效(CLK2请求开启)时,确认应答
            ack1 <= sync_req2_to_clk1[2];
        
            // --- Step 3: 生成req1 ---
            // 条件:当前正在使用CLK1(en1=1) 且 需要切换到CLK2(sel_sync=1)
            // 注意:只有ack1=0(对方还没准备好)时才发出请求
            if (en1 && sync_sel_clk1[1] && !ack1)
                req1 <= 1'b1;
            else if (!en1 && !sync_sel_clk1[1])
                req1 <= 1'b0;
        
            // --- Step 4: 生成en1 ---
            // 关闭条件:已发出关闭请求(req1=1) 且 收到对方确认(ack1=1)
            if (req1 && ack1)
                en1 <= 1'b0;
            // 重新开启条件:需要切回CLK1(sel_sync=0) 且 当前未使能
            else if (!sync_sel_clk1[1] && !en1)
                en1 <= 1'b1;
        end
    end

    // ========== CLK2域逻辑(对称)==========
    always @(posedge clk2 or negedge rst_n) begin
        if (!rst_n) begin
            req2  <= 1'b0;
            sync_req1_to_clk2 <= 3'b000;
            ack2  <= 1'b0;
            en2   <= 1'b0;          // 复位后不选择CLK2
        end else begin
            // --- Step 1: 同步req1到CLK2域 ---
            sync_req1_to_clk2 <= {sync_req1_to_clk2[1:0], req1};
        
            // --- Step 2: 生成ack2 ---
            // ack2表示"CLK1已关闭"
            // 当检测到req1有效(CLK1请求关闭)时,确认应答
            ack2 <= sync_req1_to_clk2[2];
        
            // --- Step 3: 生成req2 ---
            // 条件:当前未使用CLK2(en2=0) 且 需要切换到CLK2(sel_sync=1)
            // 注意:只有ack2=1(对方已关闭)时才发出请求
            if (!en2 && sync_sel_clk2[1] && ack2)
                req2 <= 1'b1;
            else if (en2 && !sync_sel_clk2[1])
                req2 <= 1'b0;
        
            // --- Step 4: 生成en2 ---
            // 开启条件:已发出开启请求(req2=1) 且 对方已关闭(ack2=1)
            if (req2 && ack2)
                en2 <= 1'b1;
            // 关闭条件:需要切回CLK1(sel_sync=0) 且 当前使能
            else if (!sync_sel_clk2[1] && en2)
                en2 <= 1'b0;
        end
    end

    // ========== ack1/ack2 生成逻辑总结 ==========
    /*
    ack1 生成规则(在CLK1域):
    1. 检测从CLK2域同步过来的 req2 信号
    2. 当 req2_sync 为高,表示CLK2请求开启
    3. ack1 拉高,表示"我知道你想开启CLK2,我这边CLK1正准备关闭"
    4. 实际上 ack1 就是 req2_sync 延迟一拍(去亚稳态后)
  
    ack2 生成规则(在CLK2域):
    1. 检测从CLK1域同步过来的 req1 信号
    2. 当 req1_sync 为高,表示CLK1请求关闭
    3. ack2 拉高,表示"我知道你想关闭CLK1,我这边CLK2正准备开启"
    4. 实际上 ack2 就是 req1_sync 延迟一拍(去亚稳态后)
    */

    // ========== 时钟门控单元(示意)==========
    // 实际使用标准单元库中的ICG
    wire clk1_gated = clk1 & en1;
    wire clk2_gated = clk2 & en2;
  
    assign clk_out = clk1_gated | clk2_gated;

endmodule

握手协议状态转移表(以切换到CLK2为例)

步骤CLK1域状态CLK2域状态说明
初始en1=1, req1=0en2=0, req2=0使用CLK1
1sel同步到CLK1域sel同步到CLK2域检测到切换请求
2req1=1(请求关闭)检测到req1_syncCLK1请求关闭
3等待ack1ack2=1(确认CLK1想关闭)CLK2确认
4ack1=1(收到req2_sync)req2=1(请求开启)CLK2请求开启
5en1=0(关闭CLK1)检测到ack1_syncCLK1关闭完成
6req1=0en2=1(开启CLK2)切换完成

4. 关键设计要点与注意事项

  1. 同步是关键 :所有跨时钟域的控制信号(req1, req2)必须经过目标时钟域的同步器处理(通常打两拍),防止亚稳态。
  2. 握手机制reqack 构成一个简单的握手协议,确保一个时钟完全关闭后,另一个才开启。这是无毛刺的核心。
  3. 避免使能重叠 : 代码逻辑必须保证 en1en2 在任何时候都 不能同时为高 。上面的逻辑通过先关后开来保证。
  4. 门控单元的实现 : 实际芯片中,clk & en 这样的与门实现时钟门控可能在 en变化时产生毛刺。必须使用 集成锁存器的标准时钟门控单元 (ICG)
  • 库中的 ICG 电路结构保证了使能信号在时钟低电平时被锁存,只在时钟上升沿采样变化,从而从根源上消除毛刺。
  • 综合时需要添加 set_clock_gating_style 等约束,让工具自动插入或识别这些单元。
  1. 从关断到开启的延迟 : 由于握手过程,时钟切换会引入几个周期的延迟,下游电路需要容忍这个“空白期”。
  2. 复位策略 : 复位必须将逻辑初始化到一个确定的安全状态(如选择默认时钟)。

5. 更复杂场景的扩展

  • 两个以上时钟的切换 : 原理相同,但需要更复杂的仲裁逻辑,确保任何时刻只有一个时钟使能有效。
  • 频率相近或异步时钟 : 上述方案对完全异步的时钟源都有效,因为握手协议不关心频率关系。
  • 动态频率/电压缩放 (DFS/DVS) : 无毛刺时钟切换是DFS的基础技术之一。

总结 :无毛刺时钟切换的标准做法是 采用同步的握手协议,结合专用的时钟门控单元,遵循“先关后开”的原则 ,在硬件上彻底杜绝毛刺的产生。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值