实现无毛刺时钟切换
无毛刺时钟切换的目标是在切换时钟源时,确保输出时钟不会产生小于一个周期的窄脉冲(毛刺),从而避免下游电路发生功能错误。这是一个非常经典的数字电路设计问题。在DFS/DVS/DVFS (Dynamic Frequency/Voltage Scaling)等对功耗要求比较高的系统中,无毛刺时钟切换尤为重要。
1. 问题的根源:为什么会有毛刺?
假设有两个不同频率或相位的时钟源 CLK1 和 CLK2 ,通过一个选择信号 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 -------|__________| |___________|
- 关键信号 :
sel: 原始的、可能异步的时钟选择信号。en1,en2: 分别控制 CLK1 和 CLK2 的门控使能信号。en1和en2绝对不能同时为高 ,否则两个时钟会短接竞争。clk_out: 最终的无毛刺输出时钟。
- 核心步骤(以从 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. 关键设计要点与注意事项
- 同步是关键 :所有跨时钟域的控制信号(
req1,req2)必须经过目标时钟域的同步器处理(通常打两拍),防止亚稳态。 - 握手机制 :
req和ack构成一个简单的握手协议,确保一个时钟完全关闭后,另一个才开启。这是无毛刺的核心。 - 避免使能重叠 : 代码逻辑必须保证
en1和en2在任何时候都 不能同时为高 。上面的逻辑通过先关后开来保证。 - 门控单元的实现 : 实际芯片中,
clk & en这样的与门实现时钟门控可能在en变化时产生毛刺。必须使用 集成锁存器的标准时钟门控单元 (ICG) 。
- 库中的 ICG 电路结构保证了使能信号在时钟低电平时被锁存,只在时钟上升沿采样变化,从而从根源上消除毛刺。
- 综合时需要添加
set_clock_gating_style等约束,让工具自动插入或识别这些单元。
- 从关断到开启的延迟 : 由于握手过程,时钟切换会引入几个周期的延迟,下游电路需要容忍这个"空白期"。
- 复位策略 : 复位必须将逻辑初始化到一个确定的安全状态(如选择默认时钟)。
5. 更复杂场景的扩展
- 两个以上时钟的切换 : 原理相同,但需要更复杂的仲裁逻辑,确保任何时刻只有一个时钟使能有效。
- 频率相近或异步时钟 : 上述方案对完全异步的时钟源都有效,因为握手协议不关心频率关系。
- 动态频率/电压缩放 (DFS/DVS) : 无毛刺时钟切换是DFS的基础技术之一。
总结 :无毛刺时钟切换的标准做法是 采用同步的握手协议,结合专用的时钟门控单元,遵循"先关后开"的原则 ,在硬件上彻底杜绝毛刺的产生。