这一篇在干嘛?
教科书总说”整个设计只用一个时钟域”,但现实工程中 FPGA 常常要在两个频率互不相干的系统间传数据、接多个 I/O 接口、处理异步信号。本章讲清楚跨时钟域为什么会出事(亚稳态),以及四种实用解法:相位控制、双触发器同步、异步 FIFO(配合格雷码),外加 ASIC 原型验证中的时钟门控处理。
6.0 什么是时钟域
时钟域(clock domain):由同一根时钟网络驱动的所有同步元件(触发器、同步 RAM、流水线乘法器等)构成的逻辑区域。如果所有触发器都由 FPGA 的主时钟输入驱动,就只有一个时钟域;如果有两个时钟输入(比如”接口 1”和”接口 2”各一个),就有两个时钟域:

图 6-1:双时钟域。域间数据传递就是本章要解决的核心问题。
门控时钟(gated clock)、派生时钟(derived clock)、事件驱动的触发器也都属于时钟域范畴。下图展示了用一个简单门控时钟”造”出新时钟域的方式:

图 6-2:通过门控时钟创建新时钟域。注意这种控制方式在 FPGA 里不推荐(通常应该用第二组触发器上的 clock-enable 引脚),这里只是用来说明概念。
6.1 跨时钟域为什么危险
问题的四个特点
跨时钟域传递信号之所以是 FPGA 设计中的头号暗坑,是因为:
- 故障不可复现。两个异步时钟域之间的失败往往取决于时钟边沿之间的相对时序,而时钟常来自与你的功能毫无关系的外部源——出错时机近乎随机。
- 问题随工艺而变。建立/保持时间更小的高速工艺在统计上出问题更少;同步器件的实现细节(比如输出如何缓冲)也会显著影响故障概率。
- EDA 工具通常不帮你查。静态时序分析工具按单个时钟域分析时序,除非明确指定,否则不做跨时钟分析。
- 难以调试。不理解原理的话,跨时钟域故障极难定位。所以必须在实现之前就把所有跨时钟接口定义清楚、处理妥当。
相位对齐≠永远安全
设想信号在两个时钟域间传递:

图 6-3:时钟域间的简单传播。慢时钟周期恰好是快时钟的两倍。

图 6-4:两个时钟域之间的时序。慢时钟到快时钟上升沿的时间差恒为 dC,且始终大于逻辑延迟加建立时间——这种上电相位下一切正常。
但如果同样这两个时钟上电时相位差不同呢?

图 6-5:上电相位不巧,时钟边沿对齐,直接产生建立/保持违例。这种场景可能发生在任意频率关系的两个时钟域之间;若频率不匹配,违例不会如此规律地出现。
结论:时钟同步问题一般不可复现,会直接损害 FPGA 设计的可靠性。要理解解法,先得理解违例发生时电路内部到底发生了什么。
6.1.1 亚稳态
当数据在建立/保持时间窗口内相对有效时钟沿翻转时,就发生了时序违例。违例的物理后果是:触发器内部某个节点可能停在既不是逻辑 0 也不是逻辑 1 的有效电压上——晶体管没有饱和到高或低电平,而是徘徊在中间电压,经过一段不确定的时间后才落到某个合法电平(还不一定是对的电平)。这就是亚稳态(metastability):

图 6-6:数据在建立/保持窗口内翻转时,输出可能升到对两个逻辑值都不合法的电压。
亚稳态的危害链:
- 如果触发器有输出缓冲,内部信号稳定过程会表现为输出上的一个假翻转;
- 输出停留在亚稳态的时长是概率性的,甚至可能持续整个时钟周期;
- 亚稳态值送进组合逻辑,不同门限的 gates 会做出不同判断,产生错误操作;
- 从时序收敛角度:路径延迟本应小于最小时钟周期,而亚稳态的持续时间会吃掉可用的路径延迟预算。
一句话:亚稳态可以造成灾难性的功能失效,且失效行为随时钟边沿关系而极不一致。
更麻烦的是仿真很难抓到它:纯数字仿真器不检查建立/保持违例;RTL 仿真中根本不会出现违例,信号永远不会亚稳;即使是检查建立/保持的门级仿真,让两路异步信号恰好对齐触发同步失败也很困难——尤其当验证者压根没在找这个问题时。所以必须靠设计规范来保证可靠性,而不能指望仿真兜底。
6.1.2 解法一:相位控制(Phase Control)
如果满足两个条件:
- 至少一个时钟可以在 FPGA 内部通过 PLL(锁相环)或 DLL(延迟锁相环) 控制;
- 一个时钟的周期是另一个的整数倍(在 PLL/DLL 分辨率范围内);
那么可以用相位匹配消除时序违例。
做法:让 DLL 把较快(捕获)时钟域的相位调整到与较慢(发送)时钟域一致。这样域间可用时间 dC 始终处于最大值:

图 6-7:DLL 相位匹配。只要慢域触发器到快域触发器的传播延迟小于快时钟周期,就不会出现建立违例。如果偏斜无法保证保持时间,还可以让快时钟改在下降沿捕获(前提是有足够裕量满足建立时间)。
适用条件:一个时钟周期是另一个的整数倍,且其中一个时钟可由内部 PLL/DLL 控制。
现实中经常两个条件都不满足——比如 FPGA 两端接口的输入/输出延迟都有严格的外部时序要求,动不了相位;或者两个时钟周期之间根本没有倍数关系。这时就需要下面更通用的方法。
6.1.3 解法二:双触发器同步(Double Flopping)
适用场景:两个完全异步的时钟域之间传递单比特信号。
原理很朴素:第一级触发器可能进入亚稳态,但只要让信号在被第二级重新锁存、被其他逻辑看到之前有整整一个时钟周期的时间去”沉淀”,后续逻辑就几乎不会看到亚稳态。两级之间不放任何组合逻辑,把沉淀时间最大化。(其他文献也叫同步位、双列触发器、双列同步器。)

图 6-8:双触发器结构。Din 进入第一级 Dsync,输出进第二级 Dout。

图 6-9:Dsync 是第一级,Dout 是第二级输出。Dout 保护其他电路不接触亚稳态信号。
理论上亚稳态可以无限持续,但现实中它会稳定下来——就像停在山顶正中央的球,热噪声、辐射之类的微小扰动终究会把它推下某一侧。但必须接受两个代价:
- 延迟不确定 ±1 个时钟:无法预测目标翻转落在你期望的那个时钟沿还是下一个。这对接收端无妨,但如果这些位是数据总线的一部分(有的位比同伴晚一拍)就会出错——所以双触发器只适用于能容忍 ±1 时钟误差的控制信号,比如外部事件触发的开关位(延迟几个纳秒完全无所谓);它也适合送进状态机控制结构——不用它的话,不同部分的决策逻辑可能对亚稳态做出不同解读,同时激活状态机的多个分支!
- 混合信号场景:数模混合系统常给 FPGA 送异步反馈信号,这是双触发器最常见的用武之地:

图 6-10:模拟反馈在本地时钟域重同步。
Verilog 实现非常简单:
module analog_interface(
...
output reg fbr2,
input feedback);
reg fbr1;
always @(posedge clk) begin
fbr1 <= feedback;
fbr2 <= fbr1; //; double flop
end代码点评:feedback 可能造成时序违例,fbr1 可能在时钟沿之后亚稳一段不确定的时间,因此 fbr2 才是唯一可交给其他逻辑使用的信号——其他逻辑绝不允许直接碰 fbr1。
配套的时序约束有两条:
- 时序分析应忽略第一级重同步触发器到第二级之间的路径(这个结构本来就是重同步用的,域间不存在有效的同步路径);
- 同时最小化两级触发器之间的布线延迟,降低亚稳态穿透第二级的概率。
时序分析应忽略第一级重同步触发器,并确保两个同步触发器之间的时延被最小化。
6.1.4 解法三:FIFO 结构
多比特数据跨时钟域,标准答案是FIFO(First-In First-Out,先进先出队列)。典型应用包括标准总线接口之间的数据传递、突发存储器的读写。

图 6-11:FIFO 在 PCI 应用中缓冲突发数据。
超市排队的类比
FIFO 最直观的类比是超市收银台排队:顾客(数据)到达时间随机、有均值频率,偶尔一波高峰(突发);收银员(接收端)以大致恒定的速率服务,来不及处理就排队。只要平均到达速率不超过服务速率,队伍就能维持;否则要么提高服务速率,要么降低到达速率——否则队伍无限增长,系统不可持续。对数据系统而言,“队伍无限长”等于”需要的存储无限大”,这必须在系统架构层面解决。

图 6-12:异步 FIFO。数据到达可以是任意时间间隔(含突发),接收域按自己的处理能力从队列取数。
防溢出的两个选项
FIFO 深度有限,必须防止溢出。两个选项:
- 先验知识:事先掌握发送速率(是否突发)、接收最小速率,据此算出最大队列长度。注意:发送域时钟更慢并不代表不会溢出——慢时钟域用更少的时钟周期就可能塞满数据,而接收域处理同样数据要更多周期。没有握手机制时,必须搞清最坏情况;一般传输呈”突发+空闲”交替,FIFO 深度不小于最大突发长度即可。
- 握手控制:突发大小和数据分布都无法事先定义时,用标志位控制流量:

图 6-13:FIFO 握手。full 告诉发送侧没有空间了,empty 告诉接收侧没有数据了,必要时还需要状态机来管理握手。
FIFO 自己也有跨域问题:格雷码救场
FPGA 里的 FIFO 通常是对双端口 RAM 的封装,而 full/empty 这些”看似简单”的标志恰恰是最难实现的部分——因为输入侧需要的 full 标志由输出侧的读进度决定,输出侧需要的 empty 标志由输入侧的写指针决定,标志信号本身就要跨时钟域:

图 6-14:简化异步 FIFO。写地址和读地址都要传到对端做 full/empty 判断。
问题来了:多比特地址跨域重同步时,各比特可能因走线传播时间不同而有的位被本沿捕获、有的位被下个沿捕获——接收逻辑会看到一个既不等于旧地址也不等于新地址的完全非法的地址,这是灾难性的。
解法是把二进制地址换成格雷码(Gray code):相邻数值只差一个比特。地址每次变化只翻动一位,即使那一位没被本时钟沿捕获,同步后的值也停留在旧地址上——非法地址(既非旧也非新)被彻底排除。
格雷码可以在异步时钟域间传递多比特计数器数据,FIFO 内部大量使用。
还有一个反直觉的点:地址跨域晚到一拍并不意味着 full/empty 会误判导致溢出。最坏情况只是地址延迟:
- 读地址传给写域时若延迟,写逻辑会误以为没有空间而暂缓写——只损失一点吞吐,不会溢出(写满还写);
- 写地址传给读域时若延迟,读逻辑会误以为空而暂缓读——同样只损失吞吐,不会下溢(空了还读)。
也就是说格雷码方案的失败模式是”变保守”而不是”变错误”,这正是它安全的原因。
现实中 FIFO 太常用,各 FPGA 厂商都有按用户规格自动生成软核(IP)的工具,一般不必手写 FIFO。但理解这些原理仍然重要——类似问题在一切跨域数据传递中都会出现。
6.1.5 同步器模块的分区
良好的工程实践:把同步器电路放在独立模块中、置于所有功能模块之外,而不是散落在功能逻辑里:

图 6-15:同步器作为独立模块放在顶层,各功能模块内部保持单时钟域。
这样分区有三大好处:
- 每个功能模块内部完全同步,时序分析变得极其简单;
- 时序例外(timing exception,即让工具忽略某些路径的约束)可以按整个同步块统一定义;
- 同步器及其时序例外被提升到顶层,大幅降低因人为疏忽而漏掉某个同步器的概率。
同步寄存器应作为独立模块分区,置于功能模块之外。
6.2 ASIC 原型中的门控时钟
背景
ASIC 对功耗要求苛刻,加上 ASIC 时钟树设计自由度大,常用门控时钟散布全芯片、在没有活动时关掉时钟。用 FPGA 做 ASIC 原型验证时,逻辑功能可以仿真,但物理特性(如功耗)本来就不一样——所以不必也不总是可能在 FPGA 上复刻所有低功耗优化(FPGA 的时钟资源很粗糙)。本节讲的是:怎么在 FPGA 原型上处理这些门控时钟,以及怎么改 ASIC 设计让原型更省事。(门控时钟做功耗优化的深入讨论见第 3 章。)
6.2.1 集中式时钟模块
如果 ASIC 中有多处门控时钟,把所有门控操作集中到一个专门的时钟生成模块:

图 6-16:集中式时钟模块。所有门控集中在一处,约束好处理,FPGA 原型改造(比如用一个编译期宏去掉全部门控元件)也只需改一个模块。
所有门控时钟都放在专门的时钟模块里,与功能模块分离。
6.2.2 门控消除
最直白但麻烦的方法——用宏切换:
`define FPGA
// `define ASIC`
module clocks_block(...)
`ifdef ASIC
assign clock_domain_1 = system_clock_1 & clock_enable_1;
`else
assign clock_domain_1 = system_clock_1;
`endif代码点评:ASIC 宏开启时按门控综合,切到 FPGA 原型时只改 define 即可去掉门控。缺点是每次切换目标都要改源码,设计者面对的不再是同一份 RTL,容易心里不踏实。
更优雅的方法是让综合工具自动消除门控——如 Synplify 的 “fix gated clocks” 选项,自动把门控从时钟线上挪到数据路径。看这个例子:
module clockstest(
output reg oDat,
input iClk, iEnable,
input iDat);
wire gated_clock = iClk & iEnable;
always @(posedge gated_clock)
oDat <= iDat;
endmodule代码点评:gated_clock = iClk & iEnable 是典型写法——系统时钟被使能信号门控后驱动触发器。不做处理的话,综合工具会原样实现:

图 6-17:直接门控实现。门控位于时钟线上,于是凭空出现两个时钟域,需要分别约束、各占独立时钟资源。
开启门控消除后,这个门被挪到数据路径:

图 6-18:门控消除后。时钟线干净了,代价是数据路径上多了一点延迟——现代器件的触发器都带时钟使能引脚,一般用不上这种技巧;但如果目标工艺的触发器没有时钟使能,此法就会给数据路径增加延迟。
6.3 本章要点速查
- 时钟同步问题一般不可复现,会损害设计可靠性;
- 亚稳态可造成灾难性失效;
- 一个时钟周期是另一个的整数倍、且可用 PLL/DLL 控制时,用相位控制;
- 单比特信号跨异步域,用双触发器同步;
- 时序分析忽略第一级同步触发器,并最小化两级之间的时延;
- 多比特数据跨异步域,用 FIFO;
- 多比特计数器跨域,用格雷码(FIFO 内部标配);
- 同步寄存器分区为功能模块之外的独立模块;
- 能不用门控就不用;必须用则集中到专门时钟模块。
常见坑:多比特总线直接"双触发器"同步
双触发器只适用于单比特控制信号。初学者常把 8 位数据总线每一位都各接一对触发器就以为万事大吉——由于各比特传播时间不同,有的位被本时钟沿捕获、有的位被下一沿捕获,接收端会拼出一个既不是旧值也不是新值的”幽灵数据”。多比特数据请用异步 FIFO(或握手+格雷码指针),绝不要逐位双触发器同步总线。
通关标准:
学完本篇你应该能做到:
- 用自己的话解释亚稳态的物理成因(建立/保持窗口内采样 → 内部节点停在非法电压)及其三大危害;
- 对一个跨时钟域场景判断该用哪种方案:倍频关系+可控相位选相位控制,单比特控制信号选双触发器,多比特数据流选 FIFO;
- 写出双触发器同步代码并配好时序约束(忽略第一级 + 最小化两级间延迟);
- 解释为什么 FIFO 的地址指针要用格雷码,以及”地址晚到一拍只损失吞吐、不会溢出/下溢”的安全逻辑;
- 按规范组织工程结构:同步器独立成模块、门控时钟集中在专门时钟模块,会综合工具的门控消除选项。
自测:为什么跨时钟域问题"仿真很难抓到"?
三层原因:(1) 纯数字仿真器根本不检查建立/保持违例;(2) RTL 仿真不模拟物理时序,违例不会发生,信号永远不会亚稳;(3) 即便是检查建立/保持的门级仿真,要恰好让两路异步信号对齐触发同步失败也是小概率事件——尤其当验证者没在主动找这个问题时。所以可靠性要靠设计规范保证,不能指望仿真。
自测:双触发器同步的两级之间为什么不能放组合逻辑?
两级触发器之间唯一的时钟周期就是亚稳态的”沉淀时间”。中间插任何组合逻辑都会吃掉这段时间、增加延迟,降低亚稳态在第二级锁存前稳定下来的概率。不放逻辑才能最大化沉淀窗口。
自测:为什么格雷码能安全跨域而二进制地址不能?
二进制地址相邻值可能多位同时变化(如 0111→1000 变 4 位),跨域捕获时各比特可能落在不同时钟沿,接收端会看到既非旧值也非新值的非法地址。格雷码相邻值只差 1 位:即使那一位没被捕获,同步结果也只是停留在旧地址上,永远不会出现”第三种地址”。
自测:接收域时钟比发送域慢,FIFO 就一定不会溢出吗?
不一定。溢出取决于”数据到达速率 vs 处理速率”,不是时钟频率高低。慢时钟域可能用更少的周期就塞进一批数据,而接收域处理这批数据要更多周期。判断依据是最坏情况下的突发长度与最小处理速率,必要时上 full/empty 握手。
自测:为什么说格雷码 FIFO 的最坏失败模式是"变保守"而不是"出错"?
地址跨域最多晚到一拍:读地址晚到 → 写逻辑低估空闲空间 → 暂缓写入(不溢出);写地址晚到 → 读逻辑低估已写数据 → 暂缓读取(不下溢)。两种情况都只降低一点吞吐量,绝不会写满还写或读空还读——失败方向是安全的保守侧。