FPGA 进阶 03:跨时钟域 CDC——亚稳态、MTBF 与单 bit 两级同步器
本文是
fpga/进阶层第 07 篇。前面 时序收敛 都假设数据在同一个时钟沿之间传递。真实 FPGA 里常有多个时钟(100MHz 系统、125MHz 以太网、SerDes 恢复时钟、ADC 采样时钟),信号从一个时钟域进另一个时钟域(CDC,Clock Domain Crossing)时,就会撞上数字电路里最隐蔽的故障源——亚稳态。通用原理可对照 跨时钟域 CDC,本篇专注 FPGA 上的同步器写法、ASYNC_REG属性和工具行为。
本文要回答的问题
- 什么是亚稳态?为什么触发器输出会「悬在半空」?
- MTBF 是什么,为什么打两拍就能把故障率从「秒级」拉到「几十年」?
- 单 bit 的电平、脉冲、边沿跨时钟域分别怎么同步?
ASYNC_REG属性做了什么,不加会怎样?- 多 bit 为什么不能每位打两拍?CDC 路径约束怎么配?
一、亚稳态:触发器的「犹豫不决」
触发器要求输入数据在时钟沿之前稳定一段时间(建立时间 setup)、在沿之后再保持一段时间(保持时间 hold)。如果数据恰好在这个采样窗口内变化,触发器的输出既不干脆是 0 也不干脆是 1,会进入一个介于两者之间的中间电压,经过一段随机长短的「决断时间(resolution time / metastability settling time)」后才稳定到某个值——这就是亚稳态(metastability)。

关键认知:
- 亚稳态无法彻底消除,只能用「给足决断时间」的方式把发生概率压到可接受。
- 在 FPGA 里,CDC 几乎是亚稳态的唯一现实来源(纯同步设计中,STA 保证了 setup/hold 满足,不会进入窗口)。所以只要有信号跨时钟域,就必须处理 CDC。
- 亚稳态故障的典型现象:功能仿真完全正常、上板偶发、难以复现——因为它取决于相位和布线延迟的微小关系,几天甚至几周才出一次。这也是它危险的原因。
二、两级同步器:给亚稳态一个周期去决断
最经典的单 bit CDC 电路是两级触发器同步器(two-FF synchronizer):目的时钟域里串两级(或三级)触发器,第一级可能采到亚稳态,但它有整整一个目的时钟周期去决断落定,第二级再采时,输入几乎总是已经稳定为干净的 0/1。
// 单 bit 电平同步器:把 src 域的异步电平信号同步到 dst 时钟域
module sync_2ff (
input wire dst_clk,
input wire dst_rst_n,
input wire async_in, // 来自另一个时钟域(或纯异步,如按键/外部引脚)
output reg sync_out
);
(* ASYNC_REG = "TRUE" *) reg meta;
(* ASYNC_REG = "TRUE" *) reg sync;
always @(posedge dst_clk or negedge dst_rst_n) begin
if (!dst_rst_n) begin
meta <= 1'b0;
sync <= 1'b0;
sync_out <= 1'b0;
end else begin
meta <= async_in; // 第一级:可能亚稳
sync <= meta; // 第二级:一个周期后,meta 多半已落定
sync_out <= sync;
end
end
endmodule为什么两级够用?用 MTBF(Mean Time Between Failures,平均无故障时间) 衡量。亚稳态在下一级采样时刻仍未决断的概率近似随决断时间指数下降——多给一个完整时钟周期,失败率指数级降低。工程上两级同步在几十~几百 MHz 的 FPGA 设计里 MTBF 通常能到数十年~数百年;速率更高、工艺更激进或可靠性要求高(车规/航天)会用三级。Xilinx/Intel 的手册都给 MTBF 估算公式,Vivado 还有 report_metastability(基于 ASYNC_REG 识别同步链)报告每条跨域路径的 MTBF 估计。
ASYNC_REG 属性是 FPGA 特有的关键:它告诉工具「这两级构成同步器、采样异步信号」,工具据此:
- 把两级 FF 放进同一个 SLICE、布得极近——减小第一级 Q 到第二级 D 的布线延迟,把省下的时间留给亚稳决断(分辨率时间最大化);
- 保护同步链不被优化拆散——防止 retiming/复制/合并把这两级挪开或优化掉。
不加 ASYNC_REG,工具可能把两级 FF 布到芯片两端、或因「第一级扇出=1」把它当冗余逻辑处理,MTBF 直接恶化。写同步器务必加属性。Quartus 对应设置 (* altera_attribute = "-name SYNCHRONIZER_DETECTION ON" *) 或在 Assignment 里开「识别同步器」,并同样把同步链布局进同一 LAB。
三、单 bit CDC 的三种形态
打两拍只解决「把电平变干净」,不同语义的信号还要加电路:
3.1 电平同步(level sync)
上面的 2FF 就是电平同步:异步输入是个持续多拍的电平(如别的域的 enable、mode 配置、外部中断电平)。只要信号在目的域保持至少一个 dst 周期,最终会被采到。适合状态/电平,不适合「只持续一个 src 周期的脉冲」——若脉冲比 dst 周期窄,可能完全漏采。
3.2 边沿检测(edge detect)
要在目的域感知「src 信号跳变了一下」,先同步再边沿检测:
// 同步 + 上升沿检测:sync_out 为同步后电平,pos_pulse 在跳变后 dst 域拉高 1 拍
reg sync_d;
always @(posedge dst_clk or negedge dst_rst_n) begin
if (!dst_rst_n) sync_d <= 1'b0;
else sync_d <= sync_out;
end
wire pos_pulse = sync_out & ~sync_d; // 上升沿:当前 1、上一拍 0
wire neg_pulse = ~sync_out & sync_d; // 下降沿3.3 脉冲同步(toggle + 同步 + 边沿检测)
src 域的单周期脉冲若比 dst 周期窄会漏采,标准做法是先把脉冲转成电平翻转(toggle),同步后再边沿检测还原:
// src 域:脉冲到来时把 toggle 信号翻转一次
reg toggle_src = 1'b0;
always @(posedge src_clk) if (pulse_src) toggle_src <= ~toggle_src;
// dst 域:两级同步 toggle_src,再检测边沿 → 还原出 1 个 dst 周期脉冲
(* ASYNC_REG="TRUE" *) reg t_meta, t_sync, t_sync_d;
always @(posedge dst_clk or negedge dst_rst_n) begin
if (!dst_rst_n) begin
{t_meta, t_sync, t_sync_d} <= 3'b0;
end else begin
t_meta <= toggle_src;
t_sync <= t_meta;
t_sync_d <= t_sync;
end
end
wire pulse_dst = t_sync ^ t_sync_d; // 检测到翻转 = 收到一次 src 脉冲约束:① src 脉冲间隔必须足够长——要保证前一次 toggle 被 dst 采到并完成检测之后,下一次才来(即两次脉冲间隔 ≥ 2 个 dst 周期 + 握手余量),否则会合并丢脉冲。高频脉冲流跨域不能用它,要用异步 FIFO;② 若要求「每个 src 脉冲都被 dst 可靠收到且不丢」,上**握手(req/ack)**或异步 FIFO。
3.4 握手(req/ack)
需要可靠传「事件」时:src 置 req → 同步到 dst → dst 处理后回 ack → ack 同步回 src → src 撤 req。双向同步保证事件不丢,代价是每个事件几个~十几个周期延迟,适合低速控制/配置传递。数据本身是多 bit 时,req/ack 配合「数据在 req 有效期间保持稳定」也能传多位(但带宽低),高带宽多 bit 一律异步 FIFO。
四、多 bit CDC:打两拍不够,要 FIFO / 握手 / Gray 码
铁律:多 bit 信号绝不能每位独立打两拍。 多位同时跳变(如地址/数据总线、计数器 011→100)时,各 bit 布线延迟不同,dst 采样瞬间可能采到变化中间的某个值(100 采成 110、000 都行)——每位单独打拍只保证「每位是干净 0/1」,保证不了「多位采到同一时刻的一致值」。三位计数器打两拍可能采到任意一个跳变中间态,后果是地址跳变、数据错乱。
多 bit CDC 三种正解:
| 场景 | 方案 | 原理 |
|---|---|---|
| 高速数据流(连续多位) | 异步 FIFO | 写指针用 Gray 码跨到读域(每次只变 1 bit,可安全打拍),空满判断跨域,数据本身靠双口 RAM 存储,见 异步 FIFO 篇 |
| 低速多位(寄存器配置) | 握手 req/ack | req 有效期间数据保持稳定,dst 采一次再回 ack |
| 计数器/指针跨域 | Gray 码 | 二进制转 Gray 后每次仅 1 bit 变化,打两拍不会采到非法中间态 |
异步 FIFO 的 Gray 码指针、空满生成是 CDC 的集大成专题,在进阶层下一篇专门讲(规划 #08)。
五、CDC 路径的时序约束
CDC 电路写对了,约束也要配(见 约束实战篇):
# 声明两时钟域异步:跨域路径不做 STA 收敛
set_clock_groups -asynchronous -group {clk_src} -group {clk_dst}要点:
- 异步域之间用
set_clock_groups -asynchronous(或set_false_path)关闭跨域时序检查——这些路径本来就不指望一个周期传完,STA 硬收敛会产生误报和过度优化。 - 同步器内部两级 FF 都在 dst 域,它们之间是同步路径、照常收敛,不要误标 false path 把它们也关掉。
- 约束只关时序检查,不替代同步电路——加了 clock_groups 但没放同步器,亚稳态照样发生。Vivado 的
report_cdc会静态检查:哪些跨域路径缺同步器、哪些多 bit 跨域没走 FIFO/握手,是上板前必看报告(对应专家层 CDC 治理)。
六、典型现象与定位
| 现象 | 可能原因 | 排查 |
|---|---|---|
| 仿真全过、上板偶发疯、重启偶尔好 | 亚稳态(CDC 漏同步) | report_cdc 找跨域路径;关键控制信号查是否过同步器 |
| 低速配置偶尔生效/不生效 | 多 bit 配置直接打拍采到中间态 | 改握手或保持稳定 + req/ack |
| 高频数据跨域丢数/跳变 | 用电平/脉冲同步传数据流 | 改异步 FIFO |
| 同步器加了仍偶发 | 没加 ASYNC_REG、两级被布远 | 检查属性、report_metastability 看 MTBF |
| 跨域路径报 setup 违例 | 漏 clock_groups(假违例) | 补 set_clock_groups,违例即消失 |
七、亚稳态的概率本质:MTBF 不是玄学
两级同步器为什么通常是两级而不是一级或三级?答案藏在平均故障间隔时间(MTBF)的计算里,理解这个计算,才能在「两级不够」的罕见场合做出正确判断,而不是把同步器级数当宗教习惯。触发器进入亚稳态后,并不是无限期悬而未决,它会以一定时间常数随机地收敛到 0 或 1;给它一个完整时钟周期去决断,到下一级采样时仍未稳定的概率随可用决断时间指数下降。工程上把这个概率写成:亚稳态导致采样错误的故障率正比于「异步输入变化频率 × 采样频率 × 每个周期未及时收敛的概率」,而后者随同步器级数和时钟周期指数衰减。这给出三个直接推论。第一,两级是速度与可靠性的折中点,不是定理:单级的 MTBF 在典型频率下可能只有毫秒到小时级(必然出事);两级通常把 MTBF 推到几十年甚至更久(对消费电子足够);三级再把它推到几乎不可观测。在高速工艺、紧周期、极恶劣温度或高可靠性要求(车规、航天)下,两级的计算 MTBF 可能落到不可接受区间,此时应升到三级——判断依据是厂商提供的同步器 MTBF 报告(Xilinx/Intel 都有相应报表命令和参数),而不是感觉。第二,同步器的物理实现必须被约束保护:两级寄存器要放得近、之间只有布线延迟(不要插入逻辑),用 ASYNC_REG 属性告知工具这个意图;如果在两级之间插了组合逻辑、或者布局把它们分到芯片两端,决断时间被侵蚀,MTBF 计算全部作废。第三,多扇出是隐形杀手:同一个异步信号若被同步两次(两处各自的同步器),两个同步器可能收敛到不同值,系统看到矛盾的控制面——异步信号进入一个时钟域必须只经过一个同步器、同步后再扇出。复位按钮这类人工异步输入也不例外。把 MTBF 当数量级概念来用:你永远不能证明亚稳态「不会发生」,只能把它的发生率降到设备寿命和出货总量之下可接受的水平——一万台设备、每台跑三年,要求的 MTBF 比一台实验板高四个数量级,这就是为什么同一个电路在实验室没问题、量产后翻车。
八、复位信号与低速信号的 CDC:容易被忽视的边界
讨论 CDC 时注意力都在数据通路上,有两类边界信号却被反复漏掉,专门讲清。复位的跨域:异步复位断言本身不需要同步(按下瞬间所有寄存器立刻回到初始态正是想要的),危险的是复位释放——若复位释放沿落在时钟有效沿附近,不同寄存器的复位撤除时刻不同,部分 FF 已经开始运行、部分还在复位,状态机会从一个不确定的中间态启动。正解是「异步断言、同步释放」:复位按钮/上电复位经每个时钟域各自的复位同步器(一对专做复位的 FF,异步置位、同步撤防)释放,且各域按依赖顺序解除(见 09 篇)。把全局复位直接拉给所有域的 FF 复位脚,等于在每次上电时主动制造一次跨域竞争。「慢速信号」不是免死金牌:常听到「这个信号变化很慢,不用同步」——错。亚稳态与信号变化快慢无关,只与「变化时刻相对于采样沿是否不确定」有关。一个一秒钟翻转一次的按钮,它的边沿照样可能恰好落在时钟建立窗内,照样亚稳态;慢只意味着故障窗口在时间轴上占比小,但系统上电、按按钮的那一次正好踩中,故障就是百分之百。电平 vs 脉冲的处理差异:跨域传「电平」(持续多个快时钟周期的状态,如模式开关、使能)两级同步器足够;传「脉冲」(慢域单周期脉冲到快域可能被采样多次,快域单周期脉冲到慢域可能完全丢失)则必须握手或转电平再边沿检测。配置和状态寄存器的读取:软件经低速总线读快域计数器,读时刻与计数翻转竞争会读到撕裂值(高位还没翻、低位已回绕,典型如 0x0FFF→0x1000 读成 0x0000);正解是 Gray 码或「读两次一致则采信」或握手锁存。上电与断电顺序:多电压系统中一个域已上电另一个未上电时,跨域 IO 可能处于异常电平,FPGA 内部同步器的行为依赖供电正常,热插拔场景要靠支持带电的 IO 结构和显式电源就绪门控。把这些边角纳入 CDC 评审 checklist,跨域故障才不会总从「以为安全」的地方冒出来。
九、CDC 的工程化治理:评审、工具与设计模式库
靠每个工程师各自记得「打两拍」无法在百人项目规模上保证 CDC 正确性;可复制的做法是把 CDC 治理工程化。强制静态检查:CDC lint(Spyglass、Verdi CDC、Vivado 内置的部分报告)应进门禁,列出所有跨域网并分类:已识别同步器、Gray 指针、异步 FIFO、握手、未保护(裸连)、违例(多 bit 直连、组合逻辑生成跨域信号、同步器带逻辑、多扇出同步)。未保护项为零是硬门槛,且检查要在综合前的 RTL 视图做——综合后很多结构信息丢失,工具反而看不清。同步器原语化:不要让工程师手写两级 FF(写法五花八门、属性漏加、综合识别率参差),团队提供统一的 sync_2ff、sync_pulse、sync_handshake 小模块,CDC 工具识别白名单只认这些原语;代码评审中任何手写跨域同步一律打回。原语内部带 ASYNC_REG、可配置级数、仿真模型(能注入随机延迟模拟亚稳态窗口)。设计模式文档:把四类合法跨域(电平同步、脉冲握手、Gray 指针、异步 FIFO)连同「什么时候用哪个」做成一页决策图——单 bit 状态用电平同步;事件通知用握手;数据流用异步 FIFO;计数器读回用 Gray/双读。新人不需要发明方案,只需要选模式。仿真支持:常规 RTL 仿真不产生亚稳态,CDC 错误在仿真里隐形;配合带 CDC 感知的仿真选项或在跨域点插入随机延迟,让撕裂和丢脉冲在定向测试里有机会暴露,尤其在随机回归里。架构评审前置:CDC 问题最便宜的消灭时机是画框图时——评审时钟域划分、列出所有跨域接口及同步方案,避免「RTL 写完 lint 一跑发现有 300 处跨域,其中 40 处说不清」。签收留档:CDC 报告与例外(确有理由的非常规处理)随版本签收归档。工程化之后,CDC 从「老工程师的经验玄学」变成全团队可执行、可审计的流程,这正是从能点亮板子到能做产品的差距之一。
十、CDC 在 SoC 集成层面的形态:总线、NoC 与复位域
模块级 CDC 模式(同步器、FIFO、握手)在单模块设计里够用,到了 SoC 规模,CDC 问题的主要形态从「信号怎么过域」变成「域的拓扑怎么管理」。域清单即设计文档:大型芯片有几十个时钟域和复位域,集成阶段必须维护一张权威的域图——每个域的源头(晶振/PLL/恢复时钟)、频率、模块归属、复位拓扑,以及域之间允许存在的接口清单。没有这张图,CDC lint 报告会输出几千条跨域,没人能判断哪些是设计、哪些是失误。跨域接口的标准化:不允许模块之间用裸信号跨域,所有跨域通信必须穿过库提供的标准通道(异步 AXI/AXIS 时钟转换桥、异步 FIFO、握手模块、事件同步器),模块端口级评审就能看到通道存在。这样 CDC lint 的白名单极小(只信任库原语),任何非标准跨域直接报红。NoC 与互连的角色:现代 SoC 用片上网络统一互连,NoC 内部自带每个端口的异步桥,IP 只需要接到 NoC 同步侧;这把上千个零散 CDC 收敛成少数几个受审边界,是集成层面 CDC 管理的根本性简化。反过来,绕过 NoC 的直连调试口、私有时钟走线要格外警惕。复位域与时钟域不完全重合:两个模块可能同时钟但不同复位(不同电源域、可独立重启的处理器),或同复位但跨时钟;CDC 评审必须同时过时钟域和复位域两张图,复位撤防跨时钟的问题见 09 篇。动态启停的时钟域:可门控关停的域在关停期间,对侧看到的是恒定电平而非异步跳变,重启时边界逻辑(FIFO 排空、握手复位)要有定义良好的关停/唤醒协议,否则唤醒瞬间是 CDC 故障高发时刻。验证集成层 CDC:模块级仿真测不到域间交互,需要在全芯片随机环境里注入域频比变化、随机停钟、复位序列,配合形式 CDC 工具做拓扑证明(工具证明每一条跨域都经过了合法结构,且结构被正确使用——有同步器不等于使能语义正确,后者仍要审)。SoC 级 CDC 的工程本质是拓扑治理和接口纪律,而不是更多的两级触发器。
十一、亚稳态的物理补充:同步器之外的板级与器件因素
数字教科书把亚稳态封进同步器这一个知识点,真实系统里还有几个器件和板级因素会改变亚稳态行为,做高速或高可靠设计时需要知道。触发器类型的差异:不是所有 FF 都适合同步器第一级。普通 FF 的亚稳态决断时间参数各异,厂商通常指定一类同步器友好的单元(更陡的转移特性、平衡的建立保持窗),综合器有专用推断模式(ASYNC_REG 属性)把它们放近、约束内部延迟;随手抓两个普通寄存器可能让厂家 MTBF 表的假设不成立。温度、电压与寿命的影响:决断速度随电压降低、温度升高而变慢,慢角下 MTBF 可能比典型工况差几个数量级。车规、工业产品要按最坏角核算同步器级数,消费电子实验室常温跑通不构成证据。器件老化效应同样缓慢恶化参数,十年寿命要求的设计要留额外裕量。板级输入的特殊性:真正异步的外部信号(按键、另一块板的电平、开漏总线)进入 FPGA 前还要考虑输入缓冲器、可能的慢沿——沿太慢会让输入缓冲本身在线性区停留,输出模拟电平并穿入内部,一个同步器解决不了;施密特触发输入和合适的外部整形是标准做法。多 bit 外部总线:并行外部总线即使「同源」,板级偏斜加异步采样仍可能撕裂,要么用随路时钟做源同步接收(走正常时序约束路径),要么用握手/选通,不能当成一组各自同步的控制位。开关噪声与同步失效的区分:电源跌落、地弹造成的大面积随机失效看起来像多个同步器同时失效——真亚稳态几乎永远是单 bit、单域现象;多寄存器同时异常应先查电源和时钟完整性,别在同步器级数上钻牛角尖。把 CDC 放回物理世界看,它的完整防线是:干净的输入波形、可靠的时钟和供电、正确选择和约束的同步单元、充足的最坏情况 MTBF 裕量——电路结构只是其中一环。
十二、写在最后:CDC 问题的认知层级
用一句话区分 CDC 能力的成长阶段,作为收束。最初级的认知是「异步信号打两拍就行」——它只覆盖了单 bit 电平这一种情况,把多 bit、脉冲、复位、外部慢沿全部留在风险里。第二级认识到「多 bit 要用 FIFO 或握手」,开始按数据和控制选择结构,但仍依赖个人记忆,漏域靠运气。第三级建立系统观:时钟域和复位域是架构设计的一部分,域图画在框图阶段,跨域只走标准原语,lint 进门禁,评审有清单,仿真注入对抗场景,签收有独立报告。最高一级则是统计学和物理学的认知:亚稳态不可消灭,只能用 MTBF、最坏角参数、同步器物理约束和出货量级估算把概率压到可接受;同时清楚同步器管不了电源噪声和慢输入,完整防线延伸到板级。把这四级反过来当作教学顺序也很有效:先在实验里故意制造一次偶发失败,感受「概率性硬件 bug」的质感,再讲同步器和 MTBF,再讲多 bit 结构,最后讲组织治理——没有现场感的规则背诵最容易在赶工时被丢掉。CDC 之所以值得这样严肃对待,是因为它制造的故障具有一切工程师最痛恨的性质:仿真全绿、上板偶发、与温度和批次相关、现场无法复现。对付这种故障的唯一办法不是更努力地调试,而是在设计和流程层面让它在结构上无法发生。愿每一位工程师都在职业生涯的早期就与亚稳态有过一次、且仅有一次深刻的交手,之后它便永远停留在评审清单和 lint 报告里,而不是出现在量产设备的售后记录中。
常见坑对照
| 坑 | 后果 | 正确做法 |
|---|---|---|
| 单 bit 跨域不打拍 | 亚稳态传播、偶发错乱 | 至少两级 FF + ASYNC_REG |
| 多 bit 每位打两拍 | 采到跳变中间态 | 异步 FIFO / 握手 / Gray 码 |
| 同步器忘加 ASYNC_REG | 两级被布远/优化,MTBF 恶化 | 两级 FF 都标 ASYNC_REG="TRUE" |
| 窄脉冲直接电平同步 | dst 漏采 | toggle 翻转 + 边沿检测,或握手 |
| 用同步路径思路收敛跨域 | 误报违例、过度优化 | 异步域 set_clock_groups |
| 加了 clock_groups 就不放同步器 | 约束关检查≠亚稳消失 | 电路同步 + 约束声明,两者都要 |
相关与延伸
前置:静态时序分析(setup/hold 与时钟域)、约束实战(set_clock_groups 写法)。 后续:多 bit CDC 专题——异步 FIFO(Gray 码指针、空满判断,规划 #08)、复位策略(异步断言同步释放本身也是一种同步器,规划 #09)。 通用原理(ASIC 视角):跨时钟域 CDC:亚稳态与两级同步器;工程规范见 工程实践——CDC/复位/功耗。
一句话总结
CDC 的根源是亚稳态:数据在触发器采样窗口内变化时,输出会悬在非法电平一段随机时间才落定,纯同步设计靠 STA 规避、跨时钟域则必然遇到;单 bit 用两级触发器同步器——第一级可能亚稳,但给它一整个目的时钟周期决断,第二级采到的几乎总是干净 0/1,把失败率(MTBF)指数级压到数十年一遇,且两级 FF 必须标 ASYNC_REG="TRUE" 让工具锁进同一 SLICE、别优化拆散;电平直接打两拍、跳变用「同步后边沿检测」、窄脉冲用「toggle 翻转→同步→边沿还原」、可靠事件用 req/ack 握手;多 bit 绝不能每位打两拍(同时跳变会采到中间态),高速数据流走异步 FIFO(写读指针用 Gray 码每次只变 1 bit 安全跨域)、低速多位走握手、计数器走 Gray 码;约束上异步域用 set_clock_groups -asynchronous 关跨域检查,但同步器两级是同步路径照常收敛,且约束只关误报、不能替代同步电路——上板前跑 report_cdc/report_metastability 静态揪漏。