FPGA基础 -- CDC(Clock Domain Crossing)实战教程
CDC(Clock Domain Crossing)实战教程。
CDC 教程(工程版)
1. 为什么需要 CDC
- 不同时钟域的采样沿相对相位不确定,跨域信号在建立/保持窗口内被采到会进入亚稳态(介于 0/1 的中间电平,随机解析到 0 或 1,并可能延迟)。
- 多比特总线跨域时,各位的到达时间不同步 → 在目标域同一拍被采到不一致的位组合(“撕裂”)。
- 直接跨域短脉冲:如果脉宽 < 目标域周期,有几率完全错过。
2. 典型错误
- 把多比特总线“每位接 2 级触发器”跨域。
- 用帧信号去门控复位(两个域释放相位不一致)。
- 用
count = wr_ptr - rd_ptr
直接相减未同步指针。 - 未对 CDC 路径做时序例外(STA 试图为异步路径“收敛”)。
3. 方案选型速查(牢记这张表)
场景 | 推荐方案 | 可靠性 | 吞吐/延迟 | 备注 |
---|---|---|---|---|
单比特电平 | 2 级触发器同步 | 很高 | 1–2 拍 | 最常见 |
单次事件/脉冲 | Toggle → 2FF → XOR | 很高* | 2–3 拍 | *前提:任意两采样沿间最多 1 次事件;可升级握手 |
事件绝不丢 | Req/Ack(toggle 握手) | 极高 | 往返 2T_dst+ | 有背压 busy |
数据流/多比特 | 异步 FIFO(灰码指针) | 极高 | 高吞吐 | Lattice EBR FIFO 或自研 |
事件突发(可多发) | 灰码事件计数 | 高 | 1 拍/周期结算 | 有模数上限 |
复位 | 异步置位、同步释放 | 必须 | – | 各域各自同步 |
4. 原理要点(简明数学)
4.1 2 级同步器与 MTBF
- 第一拍可能亚稳,第二拍提供解析时间 TsettleT_\text{settle}Tsettle。失效率 ~ e−Tsettle/τe^{-T_\text{settle}/\tau}e−Tsettle/τ。
- 常用 MTBF 近似:MTBF≈KFclk⋅FdataeTsettle/τ\mathrm{MTBF} \approx \frac{K}{F_\text{clk} \cdot F_\text{data}} e^{T_\text{settle}/\tau}MTBF≈Fclk⋅FdataKeTsettle/τ。布置相邻、走线短 → MTBF 指数提高。
4.2 Toggle 脉冲同步
- 源域将“一拍脉冲”编码为“翻转并保持的电平”;目域 2FF 同步后做 XOR(
s1^s2
)得 1 拍脉冲。 - 硬条件:相邻两次目标采样沿之间最多 1 次事件(工程上建议留到 ≥2 个 T_dst)。
4.3 Req/Ack 握手
req_t
(toggle)到达 → 目域产生 1 拍pulse_dst
并翻转ack_t
回源域;- 源域只有
req_t == ack_sync
(不 busy)时才允许下一个事件 → 零丢失,代价是吞吐由往返决定。
4.4 异步 FIFO(灰码指针)
- 写/读指针用 ADDR_BITS+1 位二进制;跨域传 灰码(单比特变化),2FF 同步后转回二进制,在本域环形减法。
Empty = (rd_gray == wr_gray_sync)
;
Full = (wr_gray_next == {~rd_gray_sync[MSB:MSB-1], rd_gray_sync[LSB..]})
。
5. 复位策略
- Reset:异步置位、各域同步释放。不要把复位与帧信号相与。
- 帧末软清空:停写 → 读端排空至
Empty
稳定 → 下一帧。若必须“指针归零”,用 WPReset+RPReset 对拍复位并用握手协调。
6. 约束(以 Lattice LPF 为例)
# 基本时钟
FREQUENCY PORT "pixclk_o" 72.0 MHz;
FREQUENCY PORT "tx_byte_clk" 125.0 MHz;# 切断异步时钟域之间的时序分析(双向)
CUT PATH FROM CLOCKNET "pixclk_o" TO CLOCKNET "tx_byte_clk";
CUT PATH FROM CLOCKNET "tx_byte_clk" TO CLOCKNET "pixclk_o";
同时给 2 级同步器寄存器打属性(综合器识别并物理相邻放置)。Synplify 可用:
(* syn_async_reg = "true" *) reg s1, s2;
若用其他工具,使用等效ASYNC_REG/DONT_TOUCH
属性。
7. 验证建议
-
随机相位仿真:两个时钟不整数相关;随机插入事件,统计“事件数==接收脉冲数”。
-
SVA:
- toggle:约束“事件间隔 ≥ N 个 dst 周期”;
- 握手:
req
触发到ack
返回之间禁止第二次req
; - FIFO:禁止
rd_en && empty
/wr_en && full
。
模块模板(可直接用)
7.1 单比特电平同步(2FF)
module cdc_sync_bit(input clk_dst, input rstn_dst,input in_async, output reg out_sync
);(* syn_async_reg = "true" *) reg s1;always @(posedge clk_dst or negedge rstn_dst)if(!rstn_dst) begin s1<=0; out_sync<=0; endelse begin s1<=in_async; out_sync<=s1; end
endmodule
7.2 事件(脉冲)同步(Toggle)
module cdc_pulse_sync(input clk_src, input rstn_src, input pulse_src,input clk_dst, input rstn_dst, output wire pulse_dst
);reg t_src;always @(posedge clk_src or negedge rstn_src)if(!rstn_src) t_src<=0; else if(pulse_src) t_src<=~t_src;(* syn_async_reg = "true" *) reg s1, s2;always @(posedge clk_dst or negedge rstn_dst)if(!rstn_dst) begin s1<=0; s2<=0; end else begin s1<=t_src; s2<=s1; endassign pulse_dst = s1 ^ s2; // 严格一拍
endmodule
7.3 事件握手(Req/Ack)
module cdc_event_req_ack(input clk_src, input rstn_src, input event_src, output wire busy_src,input clk_dst, input rstn_dst, output wire pulse_dst
);reg req_t; wire ack_sync_s; assign busy_src = (req_t != ack_sync_s);always @(posedge clk_src or negedge rstn_src)if(!rstn_src) req_t<=0; else if(event_src && !busy_src) req_t<=~req_t;(* syn_async_reg = "true" *) reg r1,r2; reg ack_t;assign pulse_dst = r1 ^ r2;always @(posedge clk_dst or negedge rstn_dst)if(!rstn_dst) begin r1<=0; r2<=0; ack_t<=0; endelse begin r1<=req_t; r2<=r1; if(pulse_dst) ack_t<=~ack_t; end(* syn_async_reg = "true" *) reg a1,a2; assign ack_sync_s = a2;always @(posedge clk_src or negedge rstn_src)if(!rstn_src) begin a1<=0; a2<=0; end else begin a1<=ack_t; a2<=a1; end
endmodule
7.4 异步 FIFO(80b 核心片段,灰码指针)
你前面已经用过完整版本;这里强调关键点。
// 指针宽度 ADDR_BITS+1;Empty/Full 用灰码公式;计数在本地域算
// 写域:wr_bin_n = wr_bin + (wr_en && !full);
// 读域:rd_bin_n = rd_bin + (rd_en && !empty);
// 跨域:wr_gray -> rd 同步;rd_gray -> wr 同步;再 gray2bin 环形减法
使用案例 1:像素域“行开始/行结束”事件 → 发送域 FSM
场景:pixclk_o
下由 lv_o
边沿产生行开始/结束事件,需要在 tx_byte_clk
域驱动 CSI 发包状态机的开始/收尾。
代码(事件检测 + toggle 同步 + FSM 触发)
// 源域:行边沿检测
reg lv_d1;
always @(posedge pixclk_o or negedge rstn) beginif(!rstn) lv_d1<=0; else lv_d1<=lv_o;
end
wire line_start_pulse_pix = lv_o & ~lv_d1;
wire line_end_pulse_pix = ~lv_o & lv_d1;// 跨域(两条事件线)
wire line_start_tx, line_end_tx;
cdc_pulse_sync u_ls(.clk_src(pixclk_o),.rstn_src(rstn),.pulse_src(line_start_pulse_pix),.clk_dst(tx_byte_clk),.rstn_dst(rstn),.pulse_dst(line_start_tx));
cdc_pulse_sync u_le(.clk_src(pixclk_o),.rstn_src(rstn),.pulse_src(line_end_pulse_pix),.clk_dst(tx_byte_clk),.rstn_dst(rstn),.pulse_dst(line_end_tx));// 目标域:发包 FSM
typedef enum logic[2:0]{IDLE, SOF, PAYLOAD, EOL} txs_t;
txs_t st;
always @(posedge tx_byte_clk or negedge rstn) beginif(!rstn) st<=IDLE;else case(st)IDLE: if(frame_start_tx) st<=SOF;SOF: if(sof_done) st<=PAYLOAD;PAYLOAD: if(line_end_tx) st<=EOL;EOL: if(eol_done) st<=PAYLOAD; // 下一行或回 IDLE 视帧逻辑endcase
end
约束与验证
- LPF:对
pixclk_o ↔ tx_byte_clk
双向CUT PATH
。 - 事件间隔(行周期)远大于
2*T_tx
,天然满足 toggle 条件。 - SVA:断言“每次
line_start_pulse_pix
导致line_start_tx
1 拍脉冲”;仿真用不同频比验证。
使用案例 2:APB(或 CPU)配置写 → 发送域寄存器(绝不丢)
场景:APB 在 pclk
域写配置(如 DT/VC/行长),必须可靠送到 tx_byte_clk
域,不丢、不重入。
代码(req/ack 总线握手)
// 源域:APB 写命中时发起事件(带 busy 反压)
wire wr_hit = psel & penable & pwrite & (paddr==CFG_DT);
wire busy_cfg;
wire cfg_strobe_tx;
wire [15:0] cfg_data_tx;cdc_event_req_ack u_cfg (.clk_src(pclk), .rstn_src(rstn), .event_src(wr_hit), .busy_src(busy_cfg),.clk_dst(tx_byte_clk), .rstn_dst(rstn), .pulse_dst(cfg_strobe_tx)
);// 数据保持:源域锁存写数据(仅在 !busy 时更新,避免覆盖)
reg [15:0] cfg_latch;
always @(posedge pclk or negedge rstn)if(!rstn) cfg_latch<=16'h012B;else if (wr_hit && !busy_cfg) cfg_latch<=pwdata[15:0];// 目的域:在 strobe 时采入数据
reg [15:0] dt;
always @(posedge tx_byte_clk or negedge rstn)if(!rstn) dt<=16'h012B;else if(cfg_strobe_tx) dt<=cfg_latch; // 跨域静态线,“拉住”有效
说明:这里把数据放在源域保持寄存器上,目标域在
cfg_strobe_tx
到达时采样即可;由于握手保证“在 ack 前不会发下一次”,因此采样稳定且无丢失。
约束与验证
- LPF:同样
CUT PATH
。 - SVA:
wr_hit
→ 目标域在有限拍内cfg_strobe_tx
,且busy_cfg
在 ack 前恒 1;禁止在busy_cfg
期间再次wr_hit
。
附:当“目标域更慢且事件可能很密”怎么办?
- 优先把案例 1 的
cdc_pulse_sync
升级为握手(案例 2 的写法),保证零丢失; - 或用灰码事件计数减少握手开销(允许一个 dst 周期内累计多个事件);
- 对“数据面”,始终用异步 FIFO。