# 跨时钟域数据通路如何保证不丢包 ```{contents} ``` 在 SoC 里,几乎不存在整个芯片只有一个时钟频率的设计:CPU 有动态调频(DFS),DDR 控制器有自己的时钟,NoC、外设各自一个域。数据在这些不同频率的时钟域之间流动时,丢包是最高频的一类 bug。 这篇文章回答两个问题: 1. 跨时钟域的数据通路,靠什么机制保证**一个包都不丢**; 2. 当其中一侧的时钟在**持续做 DFS**(Dynamic Frequency Scaling,动态调频)时,这些机制哪些依然成立、哪些会失效,以及如何设计才能保证不丢包。 ```{toctree} :maxdepth: 2 ``` ## 丢包的两个根源 先说清楚丢包到底是怎么发生的。跨时钟域丢包只有两个根源,除此之外别无其他: ### 根源一:亚稳态导致的采样错误 当写侧在 `wclk` 时钟沿更新数据,读侧用 `rclk` 采样时,两个时钟沿的相对相位是完全随机的。如果采样沿恰好落在数据翻转的窗口内,采样到的寄存器会进入亚稳态,输出一个既不是 0 也不是 1 的电平,并在之后随机时间才稳定: ``` wclk: _|¨|_|¨|_|¨|_|¨|_ data: ____|¨¨¨¨¨¨|____ ^ rclk: _|¨|_|¨|_|¨|_|¨|_ |-- 采样沿落在翻转窗口 --> 亚稳态 ``` 亚稳态稳定后的结果是不确定的:可能采到旧值,也可能采到新值,还可能稳定成一个中间电平被后续逻辑解释成错误的值。**采样错误 = 数据损坏 = 丢包(或错包)**。 标准解法是两级同步器: ``` +-----+ +-----+ async_sig --->| D Q |------->| D Q |-------> sync_sig (clk域) | | | | +-----+ +-----+ | | clk 域 clk 域 ``` 第一级寄存器的亚稳态留给整整一个时钟周期去稳定,第二级采样到的是稳定值。两级同步器的失效概率(MTBF,Mean Time Between Failures)由下面的关系决定: ``` MTBF = e^(t_meta/τ) / (T0 × f_clk × f_data) ``` 其中 `t_meta` 是留给亚稳态稳定的时间(一个时钟周期),`τ`、`T0` 是工艺常数。工程上只要 `t_meta` 足够大,MTBF 可以做到几百年,亚稳态问题就被解决了。 ```{warning} 两级同步器**只能用于单比特控制信号或格雷码**。并行数据总线不能直接用多级同步器同步:总线上各比特的走线延迟不同,同步后各比特可能落在新旧两个不同的"字"上(skew 导致的 reconvergence 问题),拼出一个从未存在过的数据。所以数据通路必须用**握手协议**或**异步 FIFO**,而不能把同步器用在数据总线上。 ``` ### 根源二:速率不匹配 第二个根源与亚稳态无关,是纯数学问题:**写进去的速度比读出来的快,缓冲区必然溢出**。 ``` 写入速率(平均或瞬时) > 读出速率 ==> FIFO 溢出 ==> 丢包 ``` 注意"瞬时"两个字。哪怕写侧平均速率远低于读侧,只要写入是突发(burst)式的——比如 DMA 一次连续写 64 拍——突发期间的瞬时写入速率就可能超过读出速率。所以跨时钟域数据通路的设计,核心就是两条: 1. 用同步器/异步 FIFO 解决亚稳态(保证采到的数据是对的); 2. 用**足够的深度 + 流控**解决速率不匹配(保证数据有地方放、没地方放时上游停下来)。 ## 异步 FIFO:跨时钟域数据通路的基础 异步 FIFO 是跨时钟域数据传输的标准载体。它的结构: ``` +--------------------------------------------------------------------+ | 异步 FIFO | | | | 写时钟域 (wclk) 存储阵列 读时钟域 (rclk) | | | | +-----------+ wdata +------------------+ rdata +---------+ | | | 写侧逻辑 | ---------> | 双端口 RAM | -------> | 读侧逻辑 | | | | (push) | | (深度 2^n) | | (pop) | | | +-----+-----+ +------------------+ +----+----+ | | | | | | +-----v-------+ +-------v----+ | | | 写指针(格雷码)| | 读指针(格雷码)| | | +-----+-------+ +-------+----+ | | | | | | | 指针同步(格雷码 + 两级同步器) | | | +<-----------------+ +------------------------>+ | | | +--------------------------------------------------------------------+ ``` ### 指针用格雷码传递 写指针在写时钟域产生,读指针在读时钟域产生。对方要判断空/满,就必须把指针同步过去。前面说过,并行总线不能直接同步,但**格雷码可以**:相邻两个地址的格雷码只有 1 比特翻转,同步时最多采到"旧值"或"新值",永远采不到非法的中间值——采错的那一拍只是把空/满判断推迟或提前一拍,不会产生错误地址。 ### 空满判断是"保守的" 标准异步 FIFO(以经典的 Cummings 结构为例)的空满判断: - **空**:读指针与同步过来的写指针完全相等——没有任何数据可读; - **满**:写指针与同步过来的读指针最高位相反、其余位相等——写一圈追上读了。 由于指针同步有 2 拍延迟,这两个标志天然是**保守的**: - 写侧看到的读指针是"旧"的,读侧实际已经读走了更多数据 → 写侧会**提前判满**; - 读侧看到的写指针是"旧"的,写侧实际已经写入了更多数据 → 读侧会**提前判空**。 ```{important} 异步 FIFO 的空/满标志只会"假满、假空",**永远不会滞后于真实状态**。假满只是损失一点深度利用率,假空只是让读侧空转几拍;它们损害性能,但**绝不会导致丢包**。这正是异步 FIFO 在各种频率组合下都不丢包的底层保证。 ``` ### 深度为什么取 2 的幂 格雷码是循环码,只有深度为 2 的幂时,"绕一圈回到原点"才能用最高位翻转来表示。所以 FIFO 深度要向上取整到 2 的幂,例如算出来需要 43,就取 64。 ## 保证不丢包的条件一:FIFO 深度算对 ### 深度公式推导 设: - 写侧突发长度 `B`(连续写 B 拍,每拍 1 个数据),写时钟频率 `fw`; - 读侧连续读(每周期读走 1 个),读时钟频率 `fr`。 写入 B 拍耗时 `B / fw`,这段时间读侧读走了 `fr × (B / fw) = B × fr/fw` 个数据。突发结束时 FIFO 里剩下的数据量是: ``` 深度需求 = B × (1 − fr / fw) ``` 这就是异步 FIFO 深度计算的基本公式。注意它只与**频率比和突发长度**有关,与绝对频率无关。 ### 计算示例 写侧突发 64 拍,`fw = 800 MHz`,读侧 `fr = 500 MHz`: ``` 深度需求 = 64 × (1 − 500/800) = 64 × 0.375 = 24 ``` 考虑指针同步的保守判满会吃掉约 2~3 拍的有效深度,实际取 `24 + 3 = 27`,向上取 2 的幂 → **深度 32**。 ## 保证不丢包的条件二:流控(背压) FIFO 深度只能解决"突发"问题,解决不了"长期速率不匹配"问题。如果写侧平均速率持续高于读侧,再深的 FIFO 也会满。此时必须让上游**停下来**——这就是流控(flow control / backpressure)。三种常见机制: ### valid/ready 握手 最常见的形式(AXI、valid-ready 接口都属此类): ``` valid ready 写侧 ----------> 读侧 <---------- ``` - `valid` 为 1 表示数据有效; - `ready` 为 1 表示接收方当拍能收; - **当拍 `valid && ready` 才算传输成功**,否则数据保持。 读侧把 `ready` 拉低,写侧必须把数据停在寄存器里等待。握手是逐拍的、无损的——代价是背压要一级一级往前传,传播链上的每一级都要能"停住"。 ### 水位线(almost_full)提前反压 异步 FIFO 的满标志是保守的,但"满"到"上游真正停手"之间还有一段延迟:上游看到 `almost_full` 之后,还要经过若干级流水、若干拍仲裁,才能真正停止写入。**如果等真正的满标志才反压,反应延迟内写入的数据就会溢出**。 所以工程上设置 `almost_full` 水位,提前量等于反压传播延迟: ``` almost_full 阈值 = FIFO 深度 − 反压传播延迟(拍) − 安全裕量 ``` 例如 FIFO 深度 64,写侧从看到反压到停止需要 5 拍,则 `almost_full` 设在 57 附近(再留 1~2 拍裕量)。 ### credit 机制 NoC 中常用:接收方向发送方发放 credit,发送方每发一个数据消耗一个 credit,credit 耗尽即停。credit 的返回路径本身就是异步的,需要在链路两端的寄存器里都保证"credit 不会透支"——发送端以本地计数为准,接收端的释放信息晚到只影响性能,不影响正确性。这和 FIFO 指针保守同步是同一个思想:**让"慢半拍"的信息只造成性能损失,不造成溢出**。 ## DFS 场景:一边频率一直在变 前面的讨论都假设 `fw`、`fr` 是固定值。现在把写侧换成**持续做 DFS** 的时钟域,看看哪些结论依然成立,哪些地方会翻车。 ### DFS 是什么 DFS(Dynamic Frequency Scaling)指运行中动态调整时钟频率:负载高时升频,负载低时降频省电。SoC 里 DFS 的实现有两类,它们的时钟行为完全不同,必须分开讨论: **1. glitch-free 时钟切换(clock mux)** 芯片里 PLL 同时产生多个频率,用选择器在它们之间切换: ``` PLL 输出 0 (800MHz) ---+ | +--------+ PLL 输出 1 (400MHz) ---+--->| MUX |----> clk_out +--------+ ^ 选择信号(先同步到两个时钟,在两个时钟都为低的窗口切换) ``` 切换过程中**时钟从不停止,只是周期变了**。选择信号必须先做同步处理,保证切换点两侧的时钟沿都是完整的——否则切换瞬间会产生一个窄脉冲(glitch),格雷码指针会被这个假沿采错,直接导致地址错误。 ```{warning} DFS 场景下,glitch-free 是异步 FIFO 正确性的硬前提。毛刺时钟下任何同步器都失效——这不是亚稳态概率问题,是确定性采错。 ``` **2. PLL 重新锁定(relock)** 另一种实现是重新配置 PLL 的分频系数,配置期间 **PLL 输出停止**,锁定完成后时钟恢复。停止窗口通常在微秒量级。此时该时钟域的寄存器全部冻结,数据"停在原地"。 ```{note} 现实中的 SoC DFS 多数走 glitch-free mux(切频快、时钟不断),relock 常见于省电场景(时钟门控、动态降电压前的准备)。设计跨时钟域通路时必须明确自己面对的是哪一种。 ``` ### 异步 FIFO 在 DFS 下的行为:正确性不变 先说好消息:**异步 FIFO 的逻辑正确性与时钟频率无关**。指针同步、格雷码比较、保守判空满,这些机制在频率任意变化时依然成立,因为: - 两级同步器解决的亚稳态问题,在低频下只会更安全(MTBF 公式中 `f_data` 降低,MTBF 上升); - 空满判断永远保守,不依赖频率的具体数值。 所以只要满足三个前提,**DFS 本身不会让异步 FIFO 丢包**: 1. 时钟切换 glitch-free(或 relock 期间有流控兜底,见下文); 2. FIFO 深度按**所有频率组合中的最坏情况**计算(见下节); 3. 长期平均速率仍然满足"写入 ≤ 读出"(见下节)。 ### 深度按最坏频率组合计算 这是 DFS 场景下的第一要务。写侧频率在 `[fw_min, fw_max]` 之间跳,读侧频率在 `[fr_min, fr_max]` 之间跳,深度公式里的频率要取**最坏组合**: ``` 深度需求 = B × (1 − fr_min / fw_max) ``` 即:写侧跑到最高频、读侧掉到最低频的瞬间。取其他任何组合算出来的深度都是自欺欺人——DFS 的调频策略不由你控制,最坏组合一定会出现(哪怕概率低)。 ```{important} DFS 场景下深度公式里的 `fw` 是写侧**频率上限**,`fr` 是读侧**频率下限**,而不是标称频率。很多丢包 bug 就是这里用标称频率算深度,而 DFS 恰好在某个瞬间给出了最坏组合。 ``` ### 平均速率校验:最容易被忽视的一点 深度公式只保证"一次突发装得下"。DFS 下还有一个更隐蔽的坑:**平均速率会漂移**。 举例:写侧 DFS 在 400 MHz 和 800 MHz 之间跳,平均频率 650 MHz,突发占空比 50%(每 128 拍写 64 拍),则平均写入速率是: ``` 650 MHz × 50% = 325 M 数据/秒 ``` 读侧 DFS 在 300 MHz 和 600 MHz 之间跳,**最低 300 MHz** 连续读,只能读走 300 M 数据/秒。325 > 300——无论 FIFO 多深,只要调频策略把写侧平均频率压在 650 MHz 上足够久,FIFO 一定溢出。 ```{warning} DFS 下丢包最常见的根因不是深度算错,而是**只校验了瞬时突发、没校验平均速率**。突发分析告诉你"装得下",平均速率分析告诉你"读得完"。两者都要算。 ``` 平均速率校验式: ``` 写侧平均频率 × 写入占空比 ≤ 读侧最低频率 × 读出效率 ``` ### 切换瞬态:假空时间变长 DFS 切换瞬间还会产生一个纯性能效应:读侧看到的写指针同步延迟是 2~3 个**读时钟周期**。读时钟突然从 600 MHz 掉到 300 MHz 时,这 2~3 拍的绝对时间翻倍,读侧"假空"等待的时间变长,有效读出速率短暂下降。 这不会直接丢包,但它会进一步压低读侧的实际吞吐。做平均速率校验时,如果读侧的效率本身就已经算得很满(比如 95%),DFS 切换期的假空就可能成为压垮骆驼的最后一根稻草。**给读侧吞吐留 5%~10% 的余量,或把指针同步级数再打一拍做折中**,是常见的防御措施。 ### PLL relock:时钟停止的窗口 如果 DFS 用的是 relock 实现,读侧时钟会停 `T_lock` 时间(典型 10~100 µs)。这段时间读侧一个数据都读不走,而写侧(异步域)还在全速写: ``` 停止窗口内写入的数据量 = fw × T_lock ``` 代入数字:写侧 800 MHz 持续写、relock 50 µs,就是 40000 个数据。深度 64 的 FIFO 撑不住 40000——**任何实际深度的 FIFO 都撑不住 relock 窗口**,所以不能用"加深 FIFO"来解决,必须用流控: 1. **切换前反压**:拉低读侧的 `ready`/credit,让写侧在 relock 开始前就停下来; 2. **切换前排空**:先把 FIFO 读到接近空,给停止窗口留出缓冲; 3. **读侧域内的数据通路由时钟门控整体冻结**:停止的只是时钟,寄存器状态保持,恢复后继续——只要写侧在窗口内被流控挡住,一个包都不会丢。 换句话说:**relock 场景下,"时钟停了也不丢包"靠的是切换前的流控握手,而不是 FIFO 深度**。如果写侧不接受反压(没有 ready 接口),那就必须保证写侧平均速率足够低、且 FIFO 深度能容纳 `fw × T_lock` 的数据——这在工程上几乎等价于"不允许这么设计"。 ### 同步域 DFS:读写同源的情况 如果写读两侧的时钟**同源**(同一个 PLL 分频出来,比如 800 MHz 和 400 MHz),DFS 切换时两个时钟一起变、频率比不变。这种情况下: - 用**同步 FIFO** 即可,不需要指针跨域同步,深度按固定频率比计算; - DFS 切换期间(无论 glitch-free 还是 relock)整个通路一起变慢或暂停,数据停在寄存器里,不丢包; - 真正的异步边界在**这个 DFS 域与外部固定频率模块的接口处**——那里必须用前面说的异步 FIFO + 流控,并且外部模块不知道你正在 DFS,深度和流控必须按 DFS 的最坏组合设计。 ## 完整设计示例 把上面的方法串起来,给一个完整的计算。场景:视频通路,写侧 DMA 突发写 64 拍,写时钟 DFS 范围 400~800 MHz;读侧处理引擎持续读,读时钟 DFS 范围 300~600 MHz,glitch-free 切换;写侧支持 ready 反压,反压传播延迟 5 拍(写时钟域)。 **第一步:最坏组合深度** ``` 深度需求 = B × (1 − fr_min / fw_max) = 64 × (1 − 300/800) = 40 ``` **第二步:同步/反应裕量** - 保守判满吃掉约 3 拍有效深度; - 反压传播延迟 5 拍,用 `almost_full` 提前反压,不占深度; - 需求 `40 + 3 = 43`,向上取 2 的幂 → **FIFO 深度 64**。 **第三步:平均速率校验** - 写侧 DFS 调度保证平均频率不超过 550 MHz,突发占空比 64/128 = 50%; - 平均写入速率 = 550 × 50% = 275 M 数据/秒; - 读侧最低 300 MHz,留 10% 假空余量后有效 270 M 数据/秒; 275 > 270,不满足!调整方案二选一: - 写侧 DFS 调度把平均频率上限压到 540 MHz(平均写入 270); - 或读侧 DFS 调度保证平均频率不低于 340 MHz(有效吞吐 ≥ 306)。 **第四步:水位设置** ``` almost_full = 64 − 5(反压延迟) − 1(安全裕量) = 58 ``` 写侧看到水位 ≥ 58 就拉低上游 ready。最坏组合(fw 最高、fr 最低)下,FIFO 在 58 处被截停,突发内最多再进 5~6 拍数据,远不到 64。 ```{note} 这个例子中深度 64 实际上留了约 20 拍的裕量(需求 43)。工程上这是合理的:DFS 调度器、频率切换点、电压变化带来的时钟抖动都会侵蚀理论裕量,除非有精确的调度保证,否则**别把深度抠到刚好**。 ``` ## 设计检查清单 一个跨时钟域数据通路交付前,逐条核对: - [ ] 并行数据总线走异步 FIFO,不直接过同步器;单比特控制信号才用两级同步器; - [ ] FIFO 深度按 `B × (1 − fr_min / fw_max)` 计算,取 2 的幂,并加同步/反应裕量; - [ ] 平均速率校验通过:写侧平均速率(含 DFS 平均频率)≤ 读侧最低频率吞吐(含假空损失); - [ ] `almost_full` 水位提前量 ≥ 反压传播延迟; - [ ] 写侧必须有可反压的接口(ready/credit);没有反压接口的通路必须证明任何时刻瞬时写入都不会超过 FIFO 剩余空间; - [ ] DFS 时钟切换 glitch-free;若用 relock,切换前必须完成流控握手或排空; - [ ] 仿真中用随机化 DFS 频率(在每个切换窗口随机选点)跑最坏突发模式,断言 FIFO 永不溢出; - [ ] CDC 静态检查(SpyGlass CDC / Meridian 等)通过:同步器识别、格雷码同步、reconvergence 清零。 ## 总结 跨时钟域不丢包,本质是三件事: 1. **采得对**:同步器解决亚稳态,格雷码传递指针,数据本体走 FIFO; 2. **装得下**:深度按最坏频率组合(`fw_max` / `fr_min`)和最大突发计算; 3. **读得完**:平均速率校验 + 流控反压,让"慢半拍"的同步信息只损失性能、不造成溢出。 DFS 不改变异步 FIFO 的正确性,它改变的是**最坏情况**:频率组合变成一个区间,深度和平均速率都必须按区间的端点算;切换瞬态的假空、relock 的停止窗口,则需要余量和流控来兜底。把"标称频率"换成"频率区间"来重新推导一遍,DFS 场景下的丢包问题就回到了同一套方法论的范畴内。 ## 参考资料 - [Clifford E. Cummings, "Simulation and Synthesis Techniques for Asynchronous FIFO Design" (SNUG 2002)](http://www.sunburst-design.com/papers/CummingsSNUG2002SJ_FIFO1.pdf) - [Clifford E. Cummings, "Clock Domain Crossing (CDC) Design & Verification Techniques Using SystemVerilog" (SNUG 2008)](http://www.sunburst-design.com/papers/CummingsSNUG2008Boston_CDC.pdf) - [Ran Ginosar, "Fourteen Ways to Fool Your Synchronizer" (ASYNC 2003)](https://webee.technion.ac.il/~ran/papers/Synchronizer-8-2003.pdf) - [AMBA AXI and ACE Protocol Specification — 握手与背压机制](https://developer.arm.com/documentation/ihi0022/latest/) - [SpyGlass CDC — 跨时钟域静态检查方法论](https://www.synopsys.com/implementation-and-signoff/signoff/spyglass.html)