FPGA中的时序问题
FPGA中的时序问题
一、跨时钟域设计(CDC)
1.1 什么是CDC
跨时钟域(CDC) = 一个信号,从“时钟 A”控制的逻辑,进入“时钟 B”控制的逻辑。
也就是说里面有四个主角:
| 原始信号A | 原始时钟A | 目标信号B | 目标时钟B |
|---|---|---|---|
| eg:[15:0] Data0_A | eg:AFE给的时钟 40MHz@0° | eg:[15:0] Data0_B | eg:FPGA内部时钟 120MHz@90° |
1.2 为什么要做CDC
CDC核心问题是:一个时钟域,永远无法“可靠地判断”另一个时钟域的跳变时刻。因为两个时钟:不同频率、不同相位、不同步。
所以当 B 域去采样 A 域的信号时:① 可能采早了 → 采不到;② 可能采晚了 → 采错边沿;③ 可能正好压在跳变点 → 触发器亚稳态。
1.2.1 物理层原因:触发器有建立保持时间
任何一个触发器,都要求:
- 建立时间(setup time)
- 保持时间(hold time)
但是 A 域信号的跳变时刻 和 B 域触发器的采样边沿是完全无关的,这会导致亚稳态。
因此需要一个中间商来协调这一切,也就是CDC。
1.2.2 电路层后果:亚稳态会导致什么?
亚稳态意味着:
- 输出在 0 和 1 之间抖动
- 延迟不确定
- 可能传播给下一级
结果就是:
- 状态机乱跳
- FIFO 指针错乱
- 计数器加错
- 数据对不齐
- 成像系统直接出伪影(工程层)
这就是为什么:
CDC 错误是 FPGA 里“最隐蔽、最难查、最容易复现失败”的问题。
1.3 怎么来做CDC
1.3.1 先判断这是 CDC 吗?
只要源时钟 ≠ 目标时钟,就必须当 CDC 处理。
铁律 1:先判断是不是 CDC
只要源时钟 ≠ 目标时钟,就是 CDC,哪怕频率一样相位不一样也算。
铁律 2:控制信号一定走同步器
电平 → 原语:
xpm_cdc_single脉冲 → 原语:
xpm_cdc_pulse或 toggle多 bit → 异步 FIFO(如
xpm_fifo_async)
铁律 3:CDC 后不要直接进核心逻辑
要至少经过:
- 1~2 拍 pipeline
- 再做边沿检测或状态判断
1.3.2 用什么 CDC 结构?
1.3.2.1 xpm_cdc_single 原语
是什么
xpm_cdc_single 是 Xilinx 提供的 单 bit 电平型 CDC 同步器。
内部结构可以理解为:
- 源时钟域打一拍(可选)
- 目的时钟域串联 N 级触发器(通常 2–4 级)
通过增加同步级数,把亚稳态概率压到极低,从而满足系统平均无故障工作时间(MTBF)要求。
xpm_cdc_single 官方参数:
| 参数 | 取值 | 作用 |
|---|---|---|
| DEST_SYNC_FF | 2~10 | 目标域同步级数,数值越高,稳定性越好 |
| INIT_SYNC_FF | 0/1 | 是否在仿真时给同步触发器一个已知初值,0:初值为X,1:初值为0 |
| SIM_ASSERT_CHK | 0/1 | 在仿真阶段是否自动检测 CDC 违规,并打印警告,0:不检查,1:检查 |
| SRC_INPUT_REG | 0/1 | 是否在源时钟域先打一拍,0:不打拍,1:打拍(推荐) 把“不干净的组合输入信号”变成“源时钟域内的干净同步信号”,再送去做 CDC。 就是如果是个垃圾波形,那就写1 |
适用场景
- 布尔信号0/1:
enable、mode、flag等- 寄存器配置完成标志、软复位信号
- !!!不要求“精确一拍脉冲”,只要求电平(使能信号、复位信号、标志位信号、模式选择信号、锁定信号)最终正确传递
※注意:不适用于数据传输,传过来的数据经过时钟线采回来的是错的!!!仅限于标志位、使能en信号(长期信号)!!
代码模板
1 | // cdc_single.v |
不加 CDC 的“错误写法”(对比用)
1 | // cdc_single_wrong.v |
仿真对比
纯 RTL 仿真不能真实体现亚稳态问题,因为仿真模型不会把触发器采到“半高电平”或随机延迟。
但仍然可以通过仿真看到几件事:
当 src_in 在 src_clk 域产生慢速电平变化时,
cdc_single输出会稳定、无毛刺;cdc_single_naive在切换瞬间可能出现一拍的毛刺(特别是你在 tb 里刻意安排“刚好撞在边沿附近”时)。
1 | // tb_cdc_single.sv |
Run Behavioral Simulation仿真就行
后面可以在 Vivado 里打开:CDC Checker(工具报错)
1 | report_cdc |
结果

可以看到经过原时钟80MHz→120MHz,电平信息成功传输,但是数据信息无法传递信息,这就对应了上面说的**※注意**的内容,这是 xpm_cdc_single 原语所决定的,它只传输标志位、使能en信号这些,不负责传输数据,我在80MHz时钟下src_in中有一个高电平,120MHz时钟下的dest_safe能被采到一个高电平,功能就达到了。
其次可以看到dest_naive也成功传输了这个使能信号(src_in),这是因为,我们仿真无法仿真亚稳态状态,所以对于电平来说,无法在仿真中感受出来CDC的作用,但是它确实在真实的情况下起到不可替代的作用,接下来的 xpm_cdc_pulse 原语仿真可以感受到如果不用CDC,信号的缺失。
1.3.2.2 xpm_cdc_pulse 原语
是什么
xpm_cdc_pulse 是 Xilinx 提供的 单 bit 脉冲型 CDC 同步器
它针对的是“一个源域的窄脉冲,要确保在目标域至少产生一拍脉冲”的场景。
它和上面的xpm_cdc_single的区别在于,要传输的信号不同,pulse是要传输一个窄脉冲,时间短,而single是要传输一个时间较长的信号。
✅ xpm_cdc_single:用于同步“电平状态”
✅ xpm_cdc_pulse:用于同步“一次性事件(脉冲)”
实现:
- 源域:检测脉冲,置位一个内部 toggle/持久状态
- 目的域:同步这个状态并检测变化,从而输出一个目标时钟域的脉冲
| 参数 | 取值 | 作用 |
|---|---|---|
| DEST_SYNC_FF | 2~10 | 抗亚稳态能力,数值越高,抗性越强 |
| INIT_SYNC_FF | 0/1 | 是否在仿真时给同步触发器一个已知初值,0:初值为X,1:初值为0 |
| REG_OUTPUT | 0/1 | 是否在目标域 再用一个寄存器 把脉冲打一拍, 决定 dest_pulse 是“组合脉冲”还是“寄存器脉冲”,0:组合逻辑,1:寄存器输出 时序逻辑(推荐) |
| RST_USED | 0/1 | 是否启用复位,0:不启用复位(推荐),1:外接复位信号 |
| SIM_ASSERT_CHK | 0/1 | 仿真 CDC 违规检查,0:不检查,1:打开检查 |
适用场景
| cdc跨的是什么 | 用哪个 | 为什么 |
|---|---|---|
| 使能 enable | xpm_cdc_single |
这是“状态” |
| 复位 reset | xpm_cdc_single |
这是“状态” |
| 模式选择 mode | xpm_cdc_single |
这是“状态” |
| 锁定标志 locked | xpm_cdc_single |
这是“状态” |
| 触发一次 start | xpm_cdc_pulse |
这是“事件” |
| 来了一次中断 irq | xpm_cdc_pulse |
这是“事件” |
| 写寄存器一次 | xpm_cdc_pulse |
这是“事件” |
现在是 1 还是 0 → ✅ xpm_cdc_single
有没有发生过一次 → ✅ xpm_cdc_pulse
代码模板
1 | // cdc_pulse.v |
不加 CDC 的错误写法:
1 | // cdc_pulse_naive.v |
仿真对比
1 | // tb_cdc_pulse.sv |
结果

可以看到使用xpm_cdc_pulse原语,实现了对于src_pulse脉冲信号的检测,并且通过对脉冲信号→脉冲事件,可以看到输出的已经变成非常规整的脉冲信号了。对比实验可以看到dest_pulse_naive信号就无法如实的采集脉冲信号,会产生信号丢失。
但是还是要注意:xpm_cdc_pulse也不是用来传输数据的,只是传输有几个脉冲过来了,这个信息不会因为cdc的原因丢失。
除此之外,可以看到dest_pulse_safe信号,在中间部分产生了一个很长(两个dest_clk时钟周期的脉冲信号),这是为什么呢?
是因为:这是
xpm_cdc_pulse的设计特性之一,不是 bug,不是仿真错误,也不是写错了而是由这三个因素共同决定的:
- 源脉冲与目标时钟完全异步
xpm_cdc_pulse内部是“toggle + 同步 + 边沿检测”结构- 配置了:.REG_OUTPUT(1)
理想对齐(会看到 1 拍宽),如果 toggle 的翻转 正好非常靠近
dest_clk上升沿,再加上:REG_OUTPUT = 1` 再打一拍结果看到的就是: 目标域连续两个时钟周期为高电平
但是不管是1拍还是2拍,在目标域都是被当做计数一次,不会当成两个独立事件
1.3.2.3 xpm_fifo_async 异步FIFO
目前没研究明白,按理说应该是从01开始,但是现在从1e开始,等下次遇到再研究研究吧

