FPGA 跨时钟域(CDC)处理:3 种最实用的工程方案
1. 什么是跨时钟域 CDC?
简单说清楚 3 个关键点,就完全够用:
- 核心场景:信号从一个时钟域(比如 clk_a)传到另一个时钟域(比如 clk_b);
- 触发条件:两个时钟的频率不同,或者相位无关(没有固定的时间关系);
- 直接后果:如果不做处理,直接打拍会出现亚稳态,进而导致数据错误,严重的还会让整个系统死机。
注意:只要是多时钟系统,就必须做 CDC 处理,这不是可选操作,是企业级 FPGA 开发的基本要求。
2. 方案 1:单比特信号 —— 两级寄存器同步
适用场景:按键输入、使能信号、标志位、单 bit 控制信号(比如中断请求、数据有效标志),这类场景在工程里最常见,用这个方案准没错。
代码示例:
module sync_2d(
input wire clk_dst, // 目标时钟(信号要传到的时钟域)
input wire rst_n, // 全局复位,低电平有效
input wire din, // 异步输入(来自另一个时钟域的单 bit 信号)
output wire dout // 同步后输出(已经适配目标时钟域,无亚稳态风险)
);
// 两级同步寄存器,核心就是这两个 reg,用于消除亚稳态
reg q1, q2;
// 时序逻辑,目标时钟上升沿触发,复位清零(工程标准写法)
always @(posedge clk_dst or negedge rst_n) begin
if(!rst_n) begin // 复位有效时,两级寄存器均置 0,确保初始状态稳定
q1 <= 1'b0;
q2 <= 1'b0;
end else begin
q1 <= din; // 第一级同步:采集异步输入信号,初步稳定
q2 <= q1; // 第二级同步:进一步稳定信号,彻底抵御亚稳态
end
end
// 同步后的数据输出,取第二级寄存器的值(确保输出无亚稳态)
assign dout = q2;
endmodule
关键要点:
- 两级寄存器足够抵御大部分亚稳态,工程里单 bit 信号统一用这个方案,不用多打拍(多打拍浪费资源,没必要);
- 绝对不要只打一拍!只打一拍风险极大,亚稳态还没稳定就输出,很容易出 bug;
- 这个模板可以直接复用,不管目标时钟是 50MHz 还是 100MHz,替换 clk_dst 即可。
3. 方案 2:多比特信号 —— 握手机制
适用场景:数据总线、地址信号、多 bit 控制信号(比如 8bit 数据、16bit 配置信号),这类信号不能用方案 1 直接打拍。
核心思路(简单好懂,记住这个流程):
- 发送方(原时钟域):先把数据准备好,然后发送一个 valid 有效信号(告诉接收方'数据准备好了');
- 同步 valid:把发送方的 valid 信号,用方案 1 的两级同步器,同步到接收方的时钟域;
- 接收方(目标时钟域):检测到同步后的 valid 信号,立刻锁存发送方的数据(确保数据稳定采集);
- 应答同步:接收方锁存数据后,发送一个 ack 应答信号,再把 ack 同步回发送方,告诉发送方'数据已接收,可以发下一组'。
重点提醒:多 bit 信号禁止直接打拍!直接打拍会导致不同 bit 的信号同步延迟不一样,出现数据错乱(比如原本是 16'b10101010_10101010,同步后变成 16'b10101010_10001010),工程里绝对禁止这种写法。
代码示例(16bit 数据):
// 多比特跨时钟域握手机制,发送方 clk_a,接收方 clk_b,16bit 数据(工程常用)
module cdc_handshake
(
// 发送方(原时钟域 clk_a)
input wire clk_a, // 发送方时钟
input wire rst_n, // 全局复位,低电平有效
input wire [15:0] data_a, // 发送方多 bit 数据(16bit,可修改位宽)
input wire data_vld_a, // 发送方数据有效信号
// 接收方(目标时钟域 clk_b)
input wire clk_b, // 接收方时钟
output reg [15:0] data_b, // 接收方同步后的数据
output reg data_vld_b // 接收方数据有效信号(可选)
);
// 第一步:声明信号(同步信号、握手信号)
reg valid_a_sync1; // valid_a 同步到 clk_b 域 第一级
reg valid_a_sync2; // valid_a 同步到 clk_b 域 第二级(稳定有效)
reg ack_b; // 接收方应答信号(clk_b 域)
reg ack_b_sync1; // ack_b 同步到 clk_a 域 第一级
reg ack_b_sync2; // ack_b 同步到 clk_a 域 第二级(稳定有效)
reg data_lock; // 数据锁存标志(避免数据被覆盖)
// 第二步:发送方 valid_a 同步到接收方 clk_b 域(用方案 1 的两级同步)
always @(posedge clk_b or negedge rst_n) begin
if(!rst_n) begin
valid_a_sync1 <= 1'b0;
valid_a_sync2 <= 1'b0;
end else begin
valid_a_sync1 <= data_vld_a;
valid_a_sync2 <= valid_a_sync1;
end
end
// 第三步:接收方逻辑(锁存数据 + 产生应答 ack_b)
always @(posedge clk_b or negedge rst_n) begin
if(!rst_n) begin
data_b <= 16'd0; // 对应 16bit 数据,复位置 0
data_vld_b <= 1'b0;
ack_b <= 1'b0;
data_lock <= 1'b0;
end else begin
case(valid_a_sync2)
1'b1: begin
if(!data_lock) begin // 第一次检测到 valid,锁存数据
data_b <= data_a;
data_vld_b <= 1'b1;
data_lock <= 1'b1;
ack_b <= 1'b1; // 发送应答,告诉发送方已接收
end else begin
data_vld_b <= 1'b0; // 避免多次触发有效信号
end
end
1'b0: begin // valid 无效,复位锁存标志和应答
data_vld_b <= 1'b0;
ack_b <= 1'b0;
data_lock <= 1'b0;
end
endcase
end
end
// 第四步:接收方 ack_b 同步到发送方 clk_a 域(两级同步,确认应答)
always @(posedge clk_a or negedge rst_n) begin
if(!rst_n) begin
ack_b_sync1 <= 1'b0;
ack_b_sync2 <= 1'b0;
end else begin
ack_b_sync1 <= ack_b;
ack_b_sync2 <= ack_b_sync1;
end
end
// (可选)发送方应答检测:收到 ack 后,可禁止新数据输入(避免数据冲突)
// 此处可根据实际需求添加,比如:assign data_a_en = !ack_b_sync2;
endmodule

