跨时钟域同步(CDC)握手协议
FPGA中的多bit数据跨时钟域同步(CDC)握手协议是高质量设计的核心,使用场景非常广泛。让我用一个完整的工程视角来解析:
1. 核心问题:为什么多bit不能简单打两拍?
对于 单bit控制信号 :打两拍(两级同步器)是标准做法,主要解决亚稳态问题。
但对于 多bit数据总线 :
- 每bit的延迟可能不同(布线差异、时序差异)
- 如果简单地对每bit独立打两拍,数据可能被错位采样
- 例如:32位数据
0x0000FFFF→0x00010000,可能出现中间态0x0000FFFE(毛刺状态)
这就是所谓的"数据歪斜"(data skew)问题。
2. 握手协议的基本原理
握手协议通过请求(req) 和应答(ack) 两个控制信号,确保接收方在数据稳定后才采样。
text
发送域(clk_src) 接收域(clk_dst)
数据[7:0] ──────────┐
▲ │
│ ▼
数据准备好 → req ────┐
│ │
│ ├─ 同步req到clk_dst
│ │
│ ▼
ack ←─────────────┐ │
▲ │ │
│ ▼ ▼
等待ack有效 采样数据
关键特征 :
- 数据总线 不需要同步器 (它只是物理连线)
- 只有
req和ack需要做CDC同步 - 数据在
req有效期间必须保持稳定
3. 经典握手协议的四种状态
状态转移(以发送方视角)
IDLE → SEND_DATA → ASSERT_REQ → WAIT_ACK → DEASSERT_REQ → IDLE
(数据准备) (发出请求) (等待应答) (撤销请求)
Verilog实现示例
module cdc_handshake_sender #(
parameter DATA_WIDTH = 32
)(
input wire clk_src,
input wire rst_n_src,
input wire [DATA_WIDTH-1:0] data_in, // 要发送的数据
input wire valid_in, // 发送请求
output reg ready_out, // 发送方准备好接收新数据
// CDC接口
output reg req_out, // 到接收域的请求
input wire ack_sync, // 从接收域同步回来的应答
output reg [DATA_WIDTH-1:0] data_out // 跨域数据(直接连线)
);
// 发送方状态机
typedef enum logic [1:0] {
S_IDLE,
S_ASSERT_REQ,
S_WAIT_ACK,
S_DEASSERT
} state_t;
state_t state, next_state;
// 状态寄存器
always @(posedge clk_src or negedge rst_n_src) begin
if (!rst_n_src) begin
state <= S_IDLE;
req_out <= 1'b0;
ready_out <= 1'b1;
end else begin
state <= next_state;
case (next_state)
S_IDLE: begin
req_out <= 1'b0;
ready_out <= 1'b1;
end
S_ASSERT_REQ: begin
req_out <= 1'b1;
ready_out <= 1'b0; // 发送中,不能接收新数据
end
S_WAIT_ACK: begin
req_out <= 1'b1;
ready_out <= 1'b0;
end
S_DEASSERT: begin
req_out <= 1'b0;
ready_out <= 1'b0;
end
endcase
end
end
// 状态转移逻辑
always_comb begin
next_state = state;
case (state)
S_IDLE: begin
if (valid_in && ready_out)
next_state = S_ASSERT_REQ;
end
S_ASSERT_REQ: begin
// 数据已稳定,进入等待应答状态
next_state = S_WAIT_ACK;
end
S_WAIT_ACK: begin
if (ack_sync) // 收到接收域的应答
next_state = S_DEASSERT;
end
S_DEASSERT: begin
// 等待ack变低,确保接收方看到req撤销
if (!ack_sync)
next_state = S_IDLE;
end
endcase
end
// 数据寄存器:在valid_in有效时锁存数据
always @(posedge clk_src or negedge rst_n_src) begin
if (!rst_n_src) begin
data_out <= '0;
end else if (valid_in && ready_out) begin
data_out <= data_in;
end
end
endmodule
verilog
module cdc_handshake_receiver #(
parameter DATA_WIDTH = 32
)(
input wire clk_dst,
input wire rst_n_dst,
// CDC接口
input wire [DATA_WIDTH-1:0] data_in, // 来自发送域的数据
input wire req_in, // 来自发送域的请求(需要同步)
output reg ack_out, // 到发送域的应答
// 接收方接口
output reg [DATA_WIDTH-1:0] data_out,
output reg valid_out,
input wire ready_in // 下游模块准备好
);
// 同步链:将req同步到接收时钟域
reg [2:0] req_sync;
wire req_synced = req_sync[2];
always @(posedge clk_dst or negedge rst_n_dst) begin
if (!rst_n_dst)
req_sync <= 3'b000;
else
req_sync <= {req_sync[1:0], req_in};
end
// 检测req上升沿(表示新数据到达)
reg req_synced_prev;
wire req_pos_edge = req_synced && !req_synced_prev;
always @(posedge clk_dst or negedge rst_n_dst) begin
if (!rst_n_dst)
req_synced_prev <= 1'b0;
else
req_synced_prev <= req_synced;
end
// 接收方状态机
typedef enum logic {
R_IDLE,
R_ACK
} r_state_t;
r_state_t r_state, r_next_state;
always @(posedge clk_dst or negedge rst_n_dst) begin
if (!rst_n_dst) begin
r_state <= R_IDLE;
ack_out <= 1'b0;
valid_out <= 1'b0;
data_out <= '0;
end else begin
r_state <= r_next_state;
case (r_next_state)
R_IDLE: begin
ack_out <= 1'b0;
if (req_pos_edge) begin
// 采样数据
data_out <= data_in;
valid_out <= 1'b1;
end else if (valid_out && ready_in) begin
valid_out <= 1'b0;
end
end
R_ACK: begin
ack_out <= 1'b1;
valid_out <= 1'b0; // 数据已给出
end
endcase
end
end
always_comb begin
r_next_state = r_state;
case (r_state)
R_IDLE: begin
if (req_pos_edge)
r_next_state = R_ACK;
end
R_ACK: begin
if (!req_synced) // 发送方已撤销req
r_next_state = R_IDLE;
end
endcase
end
endmodule
4. 握手协议的时序图
text
clk_src: __/ \__/ \__/ \__/ \__/ \__/ \__
clk_dst: __/ \__/ \__/ \__/ \__/ \__/ \__/ \__
data: XXXX< D1 >XXXXXXXXXXXXXXXXXXXX< D2 >XXXXXX
valid_in: ______/ \__________
ready_out: \______________________________/ \__
req_out: ______________/ \________
ack_sync: XXXXXXXXXXXXXXX\_____________/ \______
req_synced: ______________/ \________
valid_out: _______________/ \__________________
ack_out: ______________/ \________
关键点:
1. D1稳定后,req拉高
2. req被同步到clk_dst域(req_synced)
3. 接收方检测到req_synced上升沿,采样D1,拉高ack
4. ack被同步回clk_src域(ack_sync)
5. 发送方看到ack_sync,撤销req
6. 接收方看到req_synced变低,撤销ack
5. 握手协议的优缺点
优点:
- 绝对安全 :确保多bit数据被完整正确地传输
- 适应性强 :适用于任意频率比、任意相位关系的时钟域
- 吞吐量可预测 :完成一次传输需要固定周期数
缺点:
- 带宽较低 :完成一次握手需要多个时钟周期
- 最佳情况:4个src周期 + 4个dst周期
- 实际:通常6-10个周期
- 控制逻辑复杂 :需要两个状态机
- req/ack本身需要CDC :增加同步延迟
6. 工程实践中的变体
流水线握手(增加吞吐量)
verilog
// 允许最多N个传输同时在管道中
module cdc_handshake_pipelined #(
parameter DEPTH = 2 // 流水线深度
)(
// 接口类似,但内部有多个并行的握手单元
);
带缓冲的握手(解耦吞吐)
verilog
// 发送方和接收方各加一个FIFO
// 握手发生在FIFO的push/pop之间
Valid-Ready握手变体
verilog
// 类似AXI的valid/ready握手
// 但valid需要同步,ready需要反向同步
// 更高效但实现更复杂
7. 选择指南:何时用握手?
| 场景 | 推荐方案 |
|---|---|
| 低频数据传输(如配置寄存器) | 握手协议 |
| 高频数据流(如视频流) | 异步FIFO |
| 控制信号(单bit或格雷码) | 打两拍 |
| 计数器同步 | 格雷码+打两拍 |
| 地址/数据总线 | 握手或异步FIFO |
经验法则 :
- 如果数据传输间隔 > 10个时钟周期 → 握手协议
- 如果数据连续流 → 异步FIFO
- 如果只是状态/控制标志 → 格雷码同步
8. 关键设计检查点
-
数据稳定窗口 :req有效期间,数据必须绝对稳定
-
脉冲展宽 :req脉冲必须足够宽,确保能被慢时钟采样到
-
撤销顺序 :必须先撤销req,再等待ack撤销
-
复位同步 :确保两个域的复位释放时间差不会导致错误握手
-
时序约束 :
tcl# 握手信号需要设set_false_path set_false_path -from [get_clocks clk_src] -to [get_pins req_sync_reg*/D] set_false_path -from [get_clocks clk_dst] -to [get_pins ack_sync_reg*/D]
握手协议是FPGA/ASIC设计中 最可靠、最通用的多bit CDC方法 。虽然效率不是最高,但它的确定性和安全性使其成为许多关键接口的首选方案。