FPGA 进阶 04:多 bit CDC——异步 FIFO、Gray 码指针与空满判断
本文是
fpga/进阶层第 08 篇。上一篇 讲了单 bit 跨域用两级同步器、并强调「多 bit 不能每位打两拍」。这篇讲多 bit/高速数据流跨时钟域的标准答案——异步 FIFO(async FIFO / dual-clock FIFO),它是 CDC 技术的集大成者,也是 FPGA 设计里用得最多的 IP 之一。通用 FIFO 深度原理可对照 FIFO 深度计算与设计,本篇专注异步 FIFO 的 Gray 码指针/空满机制与 FPGA IP 配置。
本文要回答的问题
- 异步 FIFO 的整体结构是什么?难在哪?
- 为什么指针必须转 Gray 码再跨域?
- 「空」和「满」怎么判断,为什么指针要多 1 位?
- 数据本身为什么不用同步、直接存 RAM 就行?
- 工程上是手写还是调 IP?参数怎么配、深度怎么定?
一、异步 FIFO 要解决的矛盾
两个时钟域持续传多位数据(如 ADC 采样域 → 100MHz 处理域、125MHz 网口 → 系统域),要求:
- 不丢数:写得快、读得慢的突发期数据能暂存;
- 不错位:读出顺序和写入一致;
- 不亚稳:跨域的控制信息(写到哪了、读到哪了)要安全同步。
结构上分三块:

关键洞察:数据本身不用同步!数据写进双口 RAM 的某个地址,读域在之后某个时刻从同一地址读出——RAM 天然「锁存」了数据,不存在采样跳变问题(只要读写不撞同一地址,而空满保证了不会)。真正需要跨域安全传递的只有指针(写到哪、读到哪),而指针是多位的,难点全在这里。
二、为什么指针要 Gray 码
写指针 wptr 要拿到读域去判「满」(写得比读快一整圈就满),读指针 rptr 要拿到写域去判「空」(读追上写就空)。指针是二进制自增的多位总线,问题和 上一篇 多 bit CDC 一样:二进制 011→100 三位同时翻转,打两拍时各 bit 延迟不同,可能采到 110、001 等任意中间态,空满判断直接错乱。
Gray 码(格雷码) 的性质:相邻数值之间恰好 1 bit 变化。3 位对照:
| 十进制 | 二进制 | Gray 码 |
|---|---|---|
| 0 | 000 | 000 |
| 1 | 001 | 001 |
| 2 | 010 | 011 |
| 3 | 011 | 010 |
| 4 | 100 | 110 |
| 5 | 101 | 111 |
| 6 | 110 | 101 |
| 7 | 111 | 100 |
指针自增时 Gray 码每次只变 1 位,跨域采样即使撞上跳变时刻,采到的要么是旧值要么是新值(这 1 bit 亚稳后也会落成 0 或 1),绝不会采到第三个非法值——最坏情况只是「以为对方指针比实际慢一拍/快一拍」,这对空满判断是安全的(最多保守地早一点判满/判空,不会错误地满判空导致撞车)。
所以跨域流程:
- 指针在本域用二进制正常自增(RAM 寻址要用二进制地址);
- 二进制转 Gray:
gray = binary ^ (binary >> 1); - Gray 指针过两级同步器(标
ASYNC_REG)到对方域; - 对方域用同步过来的 Gray 指针和自己的 Gray 指针比,生成空/满。
三、空满判断与「多 1 位」指针
3.1 为什么指针要 N+1 位
深度为 2^n 的 FIFO,地址用 n 位就够寻址。但只用 n 位指针时:读指针追上写指针(空,差距 0)和写指针绕一整圈追上读指针(满,差距 2^n)这两个状态,指针值完全相同,无法区分。
所以指针用 n+1 位(多出的最高位当「绕圈标志」,不参与 RAM 寻址):
- 判空(在读域):同步过来的写 Gray 指针 == 读 Gray 指针(含扩展位全相等)→ 读写位置完全一致、没绕圈差 → 空。
- 判满(在写域):同步过来的读 Gray 指针和写 Gray 指针相比,最高两位相反、其余位全相等 → 写指针比读指针正好多绕一整圈 → 满。
(Gray 码绕回时除最高位外序列对称,所以满条件是 wgray[n] != rgray[n] 且 wgray[n-1] != rgray[n-1] 且低位全等。)
3.2 判断在各自域内完成、用对方的「旧」指针
- 空在读时钟域判:用本域实时
rptr和同步过来的wptr(可能略旧)。同步延迟只会让「写进来了」这个事实晚几拍被读到,最坏是非空时还以为空(保守,不会读到未写数据)。 - 满在写时钟域判:用本域实时
wptr和同步过来的rptr(可能略旧)。同步延迟让「读走了」晚几拍被知道,最坏是非满时还以为满(保守,不会写到未读数据)。
这种「朝保守方向错」的设计是异步 FIFO 正确的关键:宁可早一点停(判满不写、判空不读),也不能撞车。
四、数据路径:双口 RAM
数据写入和读出走真双口 RAM(FPGA 里就是 BRAM/URAM):
- 写口:wclk 上升沿,按
waddr把wdata写入,wen有效; - 读口:rclk 域按
raddr读。两种常见读模式:- 标准模式(standard read):给地址后下一拍数据才出,读延迟 1 拍;
- FWFT(First Word Fall Through):FIFO 非空时下一个数据直接「掉」到输出口,
rdata立即可见、rd_en相当于「弹出」,用起来像寄存器队列,Xilinx IP 可勾选。
五、工程实践:直接调 IP,懂原理为了配参数
FPGA 工程绝大多数情况不手写异步 FIFO,直接用厂商 IP:
- Xilinx Vivado:FIFO Generator,选 Independent Clocks(Block RAM) 即异步 FIFO;
- Intel Quartus:DCFIFO / DCFIFO_MIXED_WIDTHS(双时钟 FIFO)。
IP 内部已实现双口 BRAM + Gray 指针 + 两级同步器,经器件级验证、时序最稳。工程师要懂的是怎么配参数:
| 配置项 | 说明 / 选择建议 |
|---|---|
| 读/写时钟(wclk/rclk) | 接两个异步时钟,IP 内部自动处理 CDC |
| 数据宽度 | 读写可同宽或不同宽(mixed width,如写 8 位读 32 位) |
| 深度 | 2 的幂(Gray 指针要求);按下方估算 + 余量 |
| 读模式 | FWFT(数据先到输出、易用)或 Standard(省一拍延迟) |
| almost full/empty、prog full/empty | 可编程水位,用来提前反压/预取,强烈建议启用 |
| 复位 | 读/写复位处理;注意 IP 复位释放要求,别在复位期间读写 |
| 数据计数 wr_data_count/rd_data_count | 各自时钟域的占用量,注意是近似值(跨域) |
什么时候才手写:面试/吃透原理、资源极致受限想省 IP 开销、或需要 IP 不支持的特殊结构(自定义水位、特殊 handshake)。手写后务必 report_cdc 确认指针跨域走了 Gray + 同步器、report_timing 确认跨域被 set_clock_groups 正确豁免。
六、深度估算
FIFO 深度要能吸收「写快读慢」期间的最大突发积压(详见 FIFO 深度计算):
最小深度 ≈ 突发写入数 − 突发期间能读走的数
= burst_len − burst_len × (读吞吐 / 写吞吐)
(写快读慢时;再向上取整到 2^n,并留安全余量)例:写 100MHz 连续突发 100 个、读 80MHz,突发期最多读走 100 × 80/100 = 80 个 → 积压 ≈ 20,取深度 32 留余量。还要考虑:
- 突发长度(一次最多连续写多少);
- 读写时钟频率比与抖动(异步时钟频率可能漂移);
- 反压时机:用 almost full 提前几个深度就反压,别等到真满(跨域指针有同步延迟,真满信号到写域时可能已经又写了几个);
- 深度过浅→满丢数/频繁反压;过深→白耗 BRAM。
七、典型现象与调试
| 现象 | 可能原因 | 排查 |
|---|---|---|
| FIFO 偶尔丢数/数据错位 | 写满还在写、或读空还在读 | 看 overflow/underflow 标志;用 almost 水位提前反压 |
| 偶发跳变、极难复现 | 指针没走 Gray/同步器(手写时) | report_cdc;IP 一般无此问题 |
| 数据晚几拍才进/出 | 标准读模式/FWFT 选错、同步延迟 | 明确读模式;空满是保守的、有滞后属正常 |
| 复位后立即读写出错 | 复位释放时序/复位期间读写 | 等复位完成、指针回零后再读写 |
| 跨域报 setup 违例 | 漏 set_clock_groups | 补约束(IP 通常自带约束) |
八、空满判断的边界情形:复位、复位释放与「差一」哲学
异步 FIFO 的空满逻辑在稳态下清晰优雅,但系统级故障几乎都发生在边界时刻:复位、复位释放、刚开始读写、同时读写。把这些时刻的行为设计严密,异步 FIFO 才算真正可用于产品。复位的不对称性:写指针和写侧逻辑在写时钟域复位,读指针在读时钟域复位,两个复位来自各自域的复位树、撤除时刻不同。设计上不能要求「两边同时退出复位」,而要保证任意退出顺序下 FIFO 最终进入一致状态:先退出复位的一侧看到的对侧指针是复位初值(全零),其空满判断是保守安全的(写侧以为满、读侧以为空),等对侧同步过来的指针更新后自动恢复正常。复位期间及复位后若干同步周期内,空满标志可能是「保守的错」(暂报空/暂报满),但绝不能是「危险的错」(暂报非满导致写溢出、暂报非空导致读空数据)。这是判断一个异步 FIFO 设计是否可靠的核心准则:标志在过渡期只允许偏向安全侧。初值的传播:Gray 指针寄存器、同步器、比较逻辑的初值要显式完整,不能依赖配置值;同步器初值为零恰好匹配「对方指针在零」,使复位后无需特殊握手。不能空不能满之外:很多应用还需要「将空/将满」(programmable almost-empty/full)提前水位做反压或预取,水位比较同样要在各自时钟域用同步后的对侧指针做,且水位阈值在过渡期的偏差方向要可接受——almost-full 偶尔提前有效只会让上游少发一拍(性能损失),但绝不能滞后到溢出(数据损失)。上电后的第一次操作:建议在复位退出后先做一轮不依赖外部数据的内部训练(写侧发几个固定 pattern、读侧校验或至少确认指针推进),把「接线错误、时钟没跑、复位挂死」这类板级问题在业务流量之前暴露,39 篇 BIST 里常包含这一项。与上层协议的配合:FIFO 只保证「未满才写、非空才读」这一契约,上游若在满时仍强行写(忽略反压)、下游在空时读,得到的行为不由 FIFO 负责;设计评审要检查 FIFO 边界两侧的状态机确实遵守 valid/ready 握手,而不是假设 FIFO「总会接住」。边界哲学总结成一句话:异步 FIFO 的正确性不靠两侧精确同步,而靠在信息滞后的所有时刻都选择不会损坏数据的保守判断。
九、FIFO 之外的多 bit 跨域方案:握手通道与 DMAs 的取舍
异步 FIFO 解决「连续数据流」,但并非所有多 bit 跨域都该上 FIFO;用错结构会付出不必要的面积和延迟,或在不适合流模型的控制场景里埋雷。这一节给出 FIFO 之外的方案谱系。req/ack 握手通道(四相/两相):适合低频、稀疏、每次传输都要确认的控制消息——寄存器配置下发、命令描述符传递、模式切换。发送端把数据放到稳定总线上、翻转 req(单 bit 经同步器);接收端看到 req 后锁存数据、翻转 ack;发送端看到 ack 完成本次传输。每个数据字的传递至少需要两次跨域往返,延迟高但完全确定、无数据撕裂、天然反压(发送端没收到 ack 就不能发下一个)。两相握手(只翻转、电平本身编码事件)比四相少一半跳变、速度略快,逻辑稍绕。实现要点:数据在 req 事件前后必须稳定(握手上协议保证),故数据总线无需同步;同步的只有 req/ack 两根事件线。异步双口 RAM + 完成标志本质是深度为 1 的 FIFO:整块配置结构(几十上百个寄存器值)由慢侧写进双口 RAM,写完后用一个单 bit「就绪」事件通知快侧,快侧收到后整块读出——避免逐寄存器握手。Toggle/事件同步器(单 bit 脉冲跨域):纯事件、无数据,用翻转电平+边沿检测实现,是握手通道的数据宽度为零的特例,使用频率极高(中断、启停命令)。Gray 码直通:只适用于「连续变化的指针/计数器被对侧采样」这一狭窄场景,不要推广到一般数据总线——普通数据相邻两次传输可能多位同时变,Gray 编码救不了。DMAs/AXI 跨时钟桥:SoC 里总线级跨域(AXI/AXI-Stream 跨频)直接用成熟时钟转换桥/异步 FIFO IP,桥内部是 FIFO + 握手协议的组合,自己重写协议级桥风险极高。选择决策:看三个问题——数据流还是消息(流→FIFO,消息→握手)、每个字是否都需接收确认(需要→握手)、速率与突发长度(高速突发必须 FIFO,握手吞吐不够)。工程实践中一个子系统同时有跨域数据流(FIFO)和跨域控制面(握手)是常态,评审时分别核对,别让图省事的「控制信号混进数据总线编码传输」破坏 FIFO 接口的简洁性。
十、仿真验证异步 FIFO:如何在仿真里逼出真实风险
异步 FIFO 的标准 RTL 仿真有个尴尬:读写时钟沿被刻意安排成相位错开(testbench 常给两个整数相关时钟),亚稳态和时序竞态根本不会发生,于是「仿真全过、上板偶发错」。要让仿真真正有杀伤力,必须主动构造对抗性激励和检查。时钟关系刻意恶化:让读写时钟频率接近但不整除(如 100MHz 对 93MHz)、用非整数周期、在长时间运行中偶尔抖动相位,逼迫指针同步长期处于各种相位关系;固定 2:1 整数时钟是最温和的场景,过了不说明问题。定向边界场景:从空开始第一次写;写满后再写(应被反压,验证不溢出);读空后再读(应读到稳定旧值或被阻止,验证不下溢);几乎满时读写交替(最考验 Gray 指针与比较逻辑);复位从任意时刻拉起(在读写进行中触发复位),并枚举写侧先释放/读侧先释放两种顺序,按第一节的安全侧准则自动检查过渡期没有溢出/下溢。随机化压力测试:用约束随机(SystemVerilog/cocotb)驱动两侧独立的随机读写使能,覆盖「写极快读极慢」(反复打满)、「读极快写极慢」(反复打空)、「同速」及两侧突发交错;参考模型用无锁队列维护期望内容,读出数据逐字比对——FIFO 的数据完整性必须严格,任何一个错字即失败。结构检查:检查数据总线本身没有被同步(只同步了 Gray 指针)、Gray 码相邻值确实只变一位(自动生成断言)、指针位宽为地址位宽加一。时序反标/门级仿真抽查:关键设计在布局布线后跑一小段门级时序仿真,实际延迟会暴露「同步器里插了逻辑」「ASYSNC_REG 没生效」这类结构问题,RTL 仿真看不见。形式验证补充:用工具对 FIFO 证明「未满不写时数据不丢、不重复、保序」和满/空标志的安全性质,形式方法不受时钟相位假设限制,对 CDC 特别有效。把这套验证沉淀为 FIFO IP 的回归(自研 FIFO 才值得;调 IP 时验证配置参数和外围握手即可),异步 FIFO 才能从「希望它对」变成「证明过它对」。
十一、FIFO 深度的工程计算:从突发模型到水位设计
第六节给了深度的直觉,这一节把计算方法补齐,因为深度错误的后果极其明确:浅了丢数据(功能事故),深了浪费大量 BRAM(面积和功耗)。先定义流量模型。深度需求来自突发(burst)期间的净积累量:写入与读出不同步的时间窗内,进来多少、出去多少,差额就是必须缓存的量。通用公式是「最坏突发写入量 − 同一时间窗内确定能读出的量」,再加实现裕量。三种典型场景:一是恒定速率、频差场景(如发端 102MHz、收端 100MHz,固定包长 N),积累量 ≈ N × (fW−fR)/fW,取整加裕量;二是突发场景(写侧在 B 拍内连续写 B 个字,之后空闲,读侧均匀消耗),深度 = B − B×读带宽/写带宽,关键是拿到最坏突发长度而不是平均长度——协议规范、DMA 描述符上限、中断响应延迟期间的涌入量都是突发来源;三是帧/包场景(视频行、网络包),深度至少容纳「一个最大包 + 两侧握手不同步的时间差」。别忘记的拍数开销:几乎满标志跨时钟同步有两拍以上滞后,标志到达写侧时,写侧可能又写了若干拍——深度要覆盖「从越线事件发生到写侧真的停笔」期间的写入量,工程上用 almost-full 阈值提前反压(阈值 = 同步延迟拍数 × 每拍最大写宽 + 裕量)。读侧 almost-empty 同理。多拍读写与位宽转换:一个物理周期写两个字、读一个字的不对称 FIFO,按「字数」而非「拍数」算积累;位宽转换 FIFO 的深度选项在 IP 里常常离散(某些深度才支持某宽度比),选最接近且不小于需求的档位。验证深度足够:用第十节的随机测试,使能序列里显式构造「最大突发 + 频差最大方向 + 反压延迟最大」组合,观察水位峰值是否触及极限;测量到的峰值加两到三成裕量就是合理深度。反过来审视系统:如果算出的深度大得离谱(几千),往往不是 FIFO 该解决的问题,而是上游缺少流量控制——正确的架构改动是加反压/信用机制,而不是堆存储。FIFO 抹平的是短期不同步,抹平不了长期带宽不匹配,这条原则能避免很多「BRAM 用完了系统还是丢数」的死局。
十二、异步 FIFO 的常见商用 IP 配置与自检
工程上 99% 的异步 FIFO 直接调用厂商 IP(Vivado FIFO Generator、Quartus DCFIFO),自研只用于教学或特殊需求;因此真正的能力不是会默写 Gray 指针,而是会正确配置 IP、看得懂它的状态和警告。关键配置项逐个看:读写时钟与位宽(决定资源类型,深度浅会综合成分布式 RAM/LUT,深则进块 RAM,超大深用 URAM);读模式——标准 FIFO 模式(数据先出现在输出寄存器、读使能下一拍有效)与首字直通(FWFT,空了之后队头自动出现在输出口,软件友好但时序略不同),两种模式的控制面时序不一样,换模式必须重验;almost full/empty 水位阈值是否可配;数据计数(写侧/读侧各自的近似计数值,文档会警告「跨域使用无效」——只能在本时钟域做粗调,不能拿去做精确判断);复位类型与同步复位内置电路(现代 IP 自带写读两侧的复位处理,比手写安全,但仍要遵守其复位序列,比如复位后最少等待若干拍再写)。下板自检:复位后状态正确(空=1、满=0、数据输出确定);低频跑一轮写满—读空,校验数据模式和顺序;读写时钟用两个非整数比频率长时间压力运行,配合错误检测码图案(递增数、伪随机序列)验证无撕裂丢失;故意写满再写、读空再读,确认反压行为符合契约(不丢已有数据、不产生伪数据)。警告与边界:IP 报告里关于亚稳态同步器级数、CDC 约束是否自动生成的信息要确认——有些配置下需要用户补约束;读写使能与时钟的时序报告要过;跨片使用 FIFO(写在 A 片、读在 B 片经过 TDM)不被原生 IP 支持,跨片异步一定走 57 篇的互联通道。版本锁定:FIFO IP 随工具版本升级可能改行为,把 IP 版本和生成参数随工程锁定,升级工具时重新跑压力回归。把商用 IP 当成「功能可信、配置和集成必须自证」的外部组件,是异步 FIFO 工程实践的正确心态——指针原理(第二、三节)让你有能力审查它,而不是逼着你重造它。
十三、异步 FIFO 的设计审美:什么样的实现值得信任
最后谈一个容易被忽略的话题:在大量「能工作」的异步 FIFO 实现之间,凭什么判断哪一个值得放进产品。值得信任的实现通常有七个特征。一是职责边界清晰:指针与 Gray 码逻辑、同步器、双口存储、空满比较各自独立成块,而不是揉在一个 always 块里——边界清晰才可能被 lint 正确识别、被形式工具分别证明。二是对称的读写侧结构:两侧逻辑互为镜像、各自只消费同步过来的对方指针,没有任何一侧同时操作两侧状态,所有跨域接触点只有同步器和存储本身。三是保守方向正确:空满判断在信息滞后时永远偏向不溢出(第九节的安全侧原则),almost 标志同理。四是复位行为被显式设计:两侧复位独立、初值完整、退出顺序任意时收敛,而不是假定同时释放。五是参数化但范围受控:深度是二的幂(或内部按幂实现)、位宽显式、几乎空满阈值带合法性检查,拒绝深度为零或非对齐的非法配置。六是可观测:内部指针、水位、溢出/下溢 sticky 标志可接寄存器总线,现场出问题时有证据;溢出标志尤其重要,它意味着契约已被破坏,系统应进入错误处理而不是继续跑。七是有独立的验证资产:非整数时钟、复位序列、背靠背突发、同址读写、随机反压的回归用例常驻 CI。反过来,几个危险信号几乎可以一票否决:数据总线经过同步器、空满用组合逻辑混合两侧指针、依赖「两边时钟沿永远错开」的 testbench 假设、复位路径接到数据触发器、没有任何溢出检测。FIFO 是系统里最不起眼、却承载最关键信任的小部件——整个跨域数据通路的正确性都押在它的几行指针逻辑上,用最高的设计审美对待它并不过分。
常见坑对照
| 坑 | 后果 | 正确做法 |
|---|---|---|
| 二进制指针直接打两拍 | 采到中间态、空满乱、撞车 | 转 Gray 再同步 |
| 指针只用 n 位 | 空满不分 | 用 n+1 位,MSB 当绕圈标志 |
| 数据每位也打同步器 | 完全没必要、还可能错位 | 数据走双口 RAM,只同步指针 |
| 等真满才反压 | 同步延迟导致溢出 | 用 prog/almost full 提前反压 |
| 深度不按突发/速率算 | 丢数或浪费 BRAM | 按公式估、取 2^n、留余量 |
| 手写 FIFO 不查 CDC | MTBF 隐患 | report_cdc/report_timing;能用 IP 用 IP |
相关与延伸
前置:CDC 亚稳态与单 bit 同步器(同步器、ASYNC_REG、多 bit 为何不能打两拍)。 后续:复位策略(异步断言同步释放、复位树);FIFO 本身在 常用 IP 核 也有总览。 通用原理(ASIC 视角):FIFO 深度计算与设计(突发、速率与水位的通用算法)。
一句话总结
异步 FIFO 是多 bit/高速数据流跨时钟域的标准答案,结构=双口 RAM 存数据 + 两个时钟域各自维护读写指针:数据本身不用同步(写进 RAM、读出 RAM,空满保证不撞地址,RAM 天然锁存数据),真正要安全跨域的只有指针——而指针是多位总线、直接打两拍会采到跳变中间态,所以指针转 Gray 码(相邻值只变 1 bit)再过两级同步器,最坏只是保守地慢一拍判空满、绝不会采到非法值;判空在读域(两 Gray 指针全等=空)、判满在写域(指针用 n+1 位、高两位相反其余相等=写比读多绕一圈),空满都朝保守方向错、宁可早停也不撞车;工程上 90% 直接调 FIFO Generator(Independent Clocks)/ DCFIFO IP,懂原理是为了正确配宽度/深度(深度按「突发数 ×(1−读速/写速)」估、取 2^n、留余量)、选 FWFT、用 prog/almost full 提前反压(别等真满,跨域指针有同步延迟),手写仅用于极端定制且必须 report_cdc 验证。