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=0 en2=0, req2=0 使用CLK1
1 sel同步到CLK1域 sel同步到CLK2域 检测到切换请求
2 req1=1(请求关闭) 检测到req1_sync CLK1请求关闭
3 等待ack1 ack2=1(确认CLK1想关闭) CLK2确认
4 ack1=1(收到req2_sync) req2=1(请求开启) CLK2请求开启
5 en1=0(关闭CLK1) 检测到ack1_sync CLK1关闭完成
6 req1=0 en2=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的基础技术之一。

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

相关推荐
仲南音9 小时前
Xilinx FPGA——时钟IP核MMCM、PLL
fpga开发
I'm a winner1 天前
基于Xilinx FPGA的LVDS高速串行通信系统(三)--数据训练及握手机制【文末源码】
fpga开发
upper20201 天前
coremark移植坑之-nostartfiles -nostdlib参数
学习·fpga开发·coremark移植·riscv设计
Eloudy1 天前
Vivado 纯命令行构建 FPGA 项目教程
fpga开发
Rambo.xia1 天前
AXI跨时钟域与数据一致性——CDC FIFO深度不够、写后读冒险、多Master仲裁、乱序返回,出了Bug根本查不出来
fpga开发·bug
upper20201 天前
FPGA部署卷积神经网络识别MINST图片
fpga开发·卷积神经网络·minst·minst识别·部署神经网络·神经网络识别图片
你是我的解忧王子2 天前
嵌入式 ADC采样 原始数据分析上层(平均值 波动率 正态分布 线性补偿 系统误差)
fpga开发·数据挖掘·数据分析
9527华安2 天前
FPGA纯verilog代码ISP图像处理培训教程,基于IMX214,提供工程源码+视频教程+FPGA开发板
图像处理·fpga开发·isp·imx214·mipi
千寻xun3 天前
二、实战篇-NVME SSD控制之ZYNQ实现(六)-读写NVME SSD硬盘数据
fpga开发·nvme·nvme ssd
千寻xun3 天前
二、实战篇-NVME SSD控制之ZYNQ实现(三)
fpga开发·nvme ssd·nvme协议