异步FIFO是如何工作的?它的原理是什么? 1. 为什么 FIFO 能做跨时钟域2. 为什么 Vivado/Xilinx FIFO 的实际深度有时候不是你配置的深度而是多 1、少 1 或多 2一、先纠正一下一直以来我都认为fifo是做了数据的跨时钟域同步实际上严格说FIFO 不是把“数据本身同步”过去。异步 FIFO 是把两个时钟域“隔离开”然后只同步少量控制信息。比如你有wr_clk 域摄像头 / ADC / 以太网接收 rd_clk 域系统处理 / DDR / AXI / 图像处理如果你直接把data[15:0]从wr_clk域送到rd_clk域会出问题因为rd_clk可能在数据变化的瞬间采样导致多 bit 数据被采成乱值。异步 FIFO 的做法是wr_clk 域只负责写入 RAM rd_clk 域只负责从 RAM 读出 数据本身存在 RAM 里 跨时钟传递的不是 data而是“写指针”和“读指针”所以 FIFO 能跨时钟域的本质是数据不直接跨域跨域的是经过特殊处理的指针。这么说可能大家还是不太明白相当于是两个接力赛运动员之间要进行交接A运动员速度很快B运动员速度很慢A运动员直接把接力棒放在一个容器里面。B运动员即使很慢只要A运动员把接力棒放好了B运动员就可以非常顺利的带着接力棒出发。二、异步 FIFO 的基本结构一个异步 FIFO 大概长这样wr_clk 域 rd_clk 域 wr_en, din rd_en, dout | | v v 写指针 wr_ptr_bin 读指针 rd_ptr_bin | | v v 双口 RAM / BRAM / FIFO Memory ^ ^ | | 同步过来的 rd_ptr_gray 同步过来的 wr_ptr_gray 判断 full 判断 empty它里面有几个核心部分双口存储器一个端口用写时钟一个端口用读时钟写指针写域维护表示下一个写入位置读指针读域维护表示下一个读出位置Gray 码指针用于跨时钟同步两级同步器同步对方时钟域传过来的 Gray 指针full / empty 判断逻辑各自在本时钟域判断。FIFO_full是写时钟域的信号FIFO_empty是读时钟域的信号三、为什么用Gray 码指针假设写指针是二进制0111 - 1000从 7 变成 8 的时候4 个 bit 都变了。如果你把这个二进制指针直接同步到读时钟域读时钟域可能在它变化的中间采样到0000 1100 1010 1110这些都是错误中间态。这和多 bit 数据跨时钟域一样会出现“撕裂”。时钟在进行采样的时候很容易采样到错误的数据。所以异步 FIFO 通常会把二进制指针转换成Gray 码关于格雷码感兴趣的可以了解一下binary: 000 - 001 - 010 - 011 - 100 gray: 000 - 001 - 011 - 010 - 110Gray 码的特点是相邻两个数之间只有 1 个 bit 变化。所以格雷码就比较稳定。这样即使跨时钟域采样时刚好遇到变化也最多是“旧值”或“新值”不会采到乱七八糟的中间值。四、怎么判断 full / empty FIFO 的本质就是一个环形队列。写指针表示下一个要写的位置。读指针表示下一个要读的位置。如果wr_ptr rd_ptr有两种可能FIFO 空了FIFO 满了写指针绕了一圈追上读指针。所以真实设计里指针通常会多加一位“圈数位”。比如 FIFO 深度是 8需要 3 bit 地址addr ptr[2:0]但内部指针可能用 4 bitptr[3:0]低 3 bit 表示地址高 1 bit 表示绕圈状态。empty 判断在读时钟域里如果 本地读指针 同步过来的写指针 说明没有可读数据也就是empty (rd_gray_next wr_gray_sync);注意这里的wr_gray_sync是从写时钟域同步过来的写指针。full 判断在写时钟域里如果 写指针再前进一步会追上读指针 说明 FIFO 满经典 Gray 指针判断里满通常不是简单等于而是高位取反后比较。类似full (wr_gray_next {~rd_gray_sync[ADDR_W:ADDR_W-1], rd_gray_sync[ADDR_W-2:0]});这表示写指针已经比读指针多绕了一圈并且地址位置追上了读指针。五、为什么 FIFO 可以保证数据可靠关键在于这句话写域只在自己的时钟下写 RAM读域只在自己的时钟下读 RAM。数据写进去以后它存在 RAM 里并不是一个正在变化的跨域组合信号。读域什么时候读它只根据同步过来的写指针判断有没有数据可读如果有才读。写域什么时候写它只根据同步过来的读指针判断还有没有空间可写如果有才写。所以异步 FIFO 的跨域风险被压缩到了两个地方写指针同步到读域读指针同步到写域。而这两个指针又用Gray 码 两级同步器处理所以可靠性大幅提高。六、异步 FIFO 的读写过程假设wr_clk 比 rd_clk 快 FIFO 初始为空第 1 步写域写入 data0wr_clk W0: wr_en 1 din data0 写入 mem[wr_addr] wr_ptr_bin 1 wr_ptr_gray 更新这时候数据已经进入 RAM。但读域还不知道因为写指针要同步过去。第 2 步写指针跨到读域rd_clk R0: wr_ptr_gray_sync1 wr_ptr_gray rd_clk R1: wr_ptr_gray_sync2 wr_ptr_gray_sync1两拍之后读域看到同步过来的写指针 ! 本地读指针于是empty 拉低读域知道 FIFO 里有数据了。第 3 步读域读取 data0rd_clk R2: rd_en 1 dout mem[rd_addr] rd_ptr_bin 1 rd_ptr_gray 更新读完以后读指针变化。但写域还不知道读域已经腾出空间因为读指针也要同步回写域。第 4 步读指针同步回写域wr_clk W3: rd_ptr_gray_sync1 rd_ptr_gray wr_clk W4: rd_ptr_gray_sync2 rd_ptr_gray_sync1写域看到读指针变化后才知道空间释放了所以异步 FIFO 的full和empty都会有延迟。这里的读写指针同步是多bit信号之所以可以使用两级寄存器进行跨时钟域处理是因为它一次只变化一个bit这种变化相当于电平的跨时钟域处理所以可以只使用两级寄存器进行处理。七、为什么 FIFO 的 full / empty 有延迟因为对方指针要同步两拍。比如读域读走了一个数据理论上 FIFO 空间马上多了一个。但写域并不能立刻知道。它必须等rd_ptr_gray - 两级同步 - wr_clk 域所以full可能晚一点释放。同理写域写入一个数据后读域也不会马上知道。所以empty可能晚一点拉低。这不是 bug而是 CDC 安全机制带来的必然结果。八、为什么 Vivado 里 FIFO 深度不一定等于你设定的AMD/Xilinx 官方文档里明确说过FIFO 的有效深度或实际深度不一定和 GUI 里选择的深度一致因为实际深度取决于 FIFO 的实现方式以及启用的功能Vivado IDE 里也会报告 actual depth。影响因素包括 common/independent clock、standard/FWFT read mode、对称/非对称端口比例等。(AMD文档)也就是说你在 Vivado 里填的 depth是“配置目标”或“用户视角深度”。IP 内部为了实现 full/empty、FWFT、输出寄存器、非对称位宽、同步逻辑实际可写/可读深度可能会变。九、为什么有时候会多 1、少 1、多 2常见原因有几个。1. 异步 FIFO 需要区分 full 和 empty可能会牺牲一个深度经典环形 FIFO 里如果只看wr_ptr rd_ptr那你无法区分FIFO 空还是FIFO 满所以有些 FIFO 设计会采用最多只使用 depth - 1 个位置比如你配置 16 深度标准异步 FIFO 可能实际能可靠装 15 个。官方文档里的例子也提到对于深度 16、对称读写位宽、FWFT 的情况实际深度会从 15 增加到 17。(AMD文档)比如我下面这个仿真图我fifo例化的是16实际上仿真图里面写入了17个数据。这个例子背后的意思就是标准结构可能有效深度是 15 FWFT 又额外增加读侧可见深度 最后变成 17所以看到的“不是刚好等于配置值”并不是异常。2. FWFT 模式会让深度增加FWFT First Word Fall Through。意思是当 FIFO 里有数据时第一个数据会自动出现在dout上不需要你先rd_en才出来。AMD 文档里说明FWFT 可以在不发起读请求的情况下预先看到下一个数据当 FIFO 有数据时第一个 word 会自动出现在输出总线上。(AMD文档)更关键的是文档明确说FWFT 实现会把 FIFO 深度增加2 个 read words。(AMD文档)所以如果你用了 Vivado FIFO Generator 或 XPM FIFO并且配置了Read Mode First Word Fall Through你就可能看到类似配置 depth 2048 实际行为像 2049 / 2050 / 2047 / 2050具体表现取决于你看的是什么信号fullalmost_fullprog_fullwr_data_countrd_data_countdata_validemptydout3. 输出寄存器 / pipeline 会改变可见深度很多 Xilinx FIFO 为了提高时序性能会在输出侧加寄存器或 pipeline。这会造成RAM 里有一部分数据 输出寄存器里还有一部分数据 FWFT 预取逻辑里可能还有数据于是从用户接口看FIFO 似乎“多装了几个”。不是 RAM 真的变大了而是FIFO 内部除了主存储阵列还有输出暂存/预取路径。UG573 也提到考虑输出寄存器级、FWFT 模式或非对称端口时FIFO depth 会变化。(AMD文档)

本周精选

本月热点