ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

异步FIFO设计精讲:跨时钟域格雷码与空满判断实战

异步FIFO设计精讲:跨时钟域格雷码与空满判断实战 做FPGA和数字IC的同学对异步FIFO应该都不陌生。不管是简历上写“熟悉跨时钟域处理”还是面试被问“异步FIFO怎么设计空满信号怎么产生”几乎绕不开这个东西。我在工程里第一次真正被它坑是调试一块采集板sensor输出的像素时钟和DDR写侧时钟完全异步数据一出sensor就往FIFO里灌结果画面偶尔出现整行错位、颜色条纹查了整整两天最后发现是读指针同步时序处理得不对。从那以后我才明白异步FIFO不是“能读写就行”它真正难的是在跨时钟域下的指针同步和空满判断。这篇文章我就以“异步FIFO设计”为主线从原理讲到底层实现再给一套可直接落地的Verilog代码最后补充工程调试中常见的坑。适合正在学FPGA的同学、准备数字IC面试的求职者以及被异步FIFO空满标志折磨过的在职工程师。内容偏实战代码可以直接抄但更重要的是把为什么这么做讲清楚。1. 异步FIFO到底难在哪1.1 表面是存储本质是跨时钟域FIFO本身是个存储结构数据按顺序写入、按顺序读出。但异步FIFO的“异步”二字意味着写时钟和读时钟没有任何相位关系。这个时候你面临的核心问题不是我往里面写多少、读多少而是两个时钟域之间的信号传递。举个生活中的例子。两个人隔着一条河对话一个人用鼓声发信号另一个人盯着河面看波纹。鼓声什么时候落下去、波纹什么时候传到对岸中间有一段无法精确衡量的延迟。异步FIFO里的写指针和读指针就相当于这两边的“鼓声”。读时钟域想知道写指针现在到哪了不能直接去抓寄存器因为直接抓很可能抓到正在变化的中间态这就是著名的亚稳态问题。所以异步FIFO的第一个难点不是FIFO本身而是如何安全地把一个时钟域的指针传递到另一个时钟域。直接打两拍同步可以但有个前提被同步的信号在目标时钟域采样时不能发生跳变冲突。如果直接用二进制指针做跨时钟域同步多个bit同时翻转采样结果可能既不是旧值也不是新值而是两者拼接出来的“混合值”。比如二进制指针从011变成100三个bit同时翻转接收端采到的可能是111、000之类的任意组合。这样一来空满判断直接失效FIFO就可能出现读空还在读、写满还在写的严重错误。1.2 解决思路格雷码 两级同步器为了规避多bit同时翻转的问题工程上的标准做法是把二进制指针转换成格雷码。格雷码的特点是相邻两个数值之间只有1个bit发生变化。这样指针在递增时跨时钟域同步最多只有一个bit在翻转哪怕采到亚稳态最终稳定下来的值也只有两种可能旧值或新值不会出现第三种混乱值。但这个转换不是随便做的。你得保证指针比较时用的是同步后的格雷码而FIFO内部的读写地址用的仍然是二进制码。格雷码负责跨时钟域传输和空满判断二进制码负责真正的存储寻址两者各司其职不能混用。还有一点很多人容易忽略空满标志的计算必须在各自的时钟域内完成。也就是说写满信号必须在写时钟域内产生读空信号必须在读时钟域内产生。为什么因为判断写满时需要用到读指针而这个读指针是同步到写时钟域的判断读空时需要用到写指针而这个写指针是同步到读时钟域的。如果反过来在写时钟域判断读空信号经过了跨时钟域同步本身就滞后了极有可能出现误判。1.3 深度必须是2的幂次异步FIFO的深度设计成2的幂次不是随便定的而是格雷码指针天然的要求。格雷码的循环规律是2的幂次对称翻转只有深度为2的n次方二进制指针到格雷码的转换才是严格等价的空满判断逻辑才能按位精确比较。如果深度不是2的幂次比如深度6指针从5跳回0格雷码跳变就不止1个bit同步机制失效整个设计就是错的。所以参数化设计时我一般会加一个参数检查FIFO深度必须是2的幂次不满足直接报错从源头避免这类问题。2. 指针同步和空满判断的核心逻辑2.1 读写指针从哪里来、到哪里去先理清四个关键信号写指针 wptr_bin写时钟域内的内部指针决定写地址同时转成格雷码 wptr_gray 用于同步读指针 rptr_bin读时钟域内的内部指针决定读地址同时转成格雷码 rptr_gray 用于同步同步后的读指针 wq2_rptr_gray把读指针格雷码同步到写时钟域用于产生写满信号同步后的写指针 rq2_wptr_gray把写指针格雷码同步到读时钟域用于产生读空信号注意看这个数据流写满信号不是“写侧主动判断的”而是通过“把读指针拉过来和本地写指针比较”得到的。读空信号同理。这种结构引出异步FIFO一个很有意思的特性空满标志相对于真实状态是“保守滞后”的。也就是说FIFO可能其实没满但写满信号已经拉高了FIFO可能其实没空但读空信号已经拉高了。这种滞后是安全的因为它只会导致性能轻微下降不会导致数据覆盖或读出无效数据。2.2 格雷码转换与指针递增二进制转格雷码的公式是gray (bin 1) ^ bin。这个公式是固定写法不需要记推导但要知道为什么。右移一位再异或本质是让相邻两个值之间只有1个bit翻转从最低位开始逐级“错开”。格雷码转二进制也有公式bin[i] 对gray从最高位到第i位求异或。但在异步FIFO设计中一般从格雷码转换回二进制的场景不多因为我们只需要在本地时钟域用二进制指针做寻址格雷码只用于跨时钟域传输。所以代码实现时只需要做 bin_to_gray不需要做 gray_to_bin。指针递增有个关键细节写指针和读指针的位宽要比FIFO深度所需的地址位宽多1位。比如深度16地址需要4bit但指针必须是5bit。多出来的最高位用来区分“写指针追上读指针”到底是空还是满。具体判断逻辑是如果写指针和读指针的最高位不同次高位相同其余位都相同说明写指针比读指针多跑了一圈FIFO为满如果所有位都相同说明两者在同一圈内相遇FIFO为空。这个设计非常巧妙用一圈额外空间来消除“空”和“满”在指针相等时的歧义。2.3 空满判断的完整条件用同步后的格雷码指针比较时条件是这样的读空当 rptr_gray rq2_wptr_gray即读指针等于同步过来的写指针。因为在格雷码域只要完全相等就说明所有bit都一致处于同一圈FIFO空写满当同步后的读指针最高位和本地写指针最高位相反次高位相同其余位相同说明写指针已经多走了一圈FIFO满这里有个容易踩的坑如果用格雷码比较不能用“最高两位都不同”来判断因为格雷码和二进制不一样一圈结束时最高位翻转后符号判断逻辑不能照搬二进制。我见过有同学直接用二进制指针的比较逻辑套在格雷码上结果深度8时的FIFO在写入第5个数据时就误报满了。稳妥的做法是比较时统一用格雷码判断写满时先判断最高位相反、次高位相同再判断低位全部相等。这个逻辑在格雷码域和二进制域是等价的因为格雷码是循环码循环一圈后最高位取反次高位从0变1。2.4 两级同步器的原理两级同步器就是背靠背两个D触发器把异步信号打两拍之后再用。第一拍采样可能进入亚稳态但经过一个时钟周期的稳定时间后第二拍采样大概率能采到稳定值。这里的关键是第一拍寄存器的输出不要直接送给任何组合逻辑只送给第二拍这样亚稳态不会被传播出去。工程上两级同步器已经能满足绝大多数单bit跨时钟域场景。但它依然有概率失败只是这个概率低到可以忽略。如果对可靠性要求极高可以加反馈握手但代价是吞吐率下降FIFO场景一般不这么做。异步FIFO里同步的是格雷码指针不是数据本身。数据是通过RAM的写端口和读端口硬隔离的写入后的数据稳定存在RAM里读时钟域想读哪个地址等RAM输出稳定后取走即可不存在跨时钟域采样的风险。这个点要理解透——异步FIFO真正做同步的只有指针不会把数据总线直接从写时钟域引到读时钟域去打两拍。3. 完整Verilog实现与代码解读3.1 顶层模块设计我提供一个参数化的异步FIFO设计深度可配置只要满足2的幂次即可。代码风格偏工程化每个模块职责单一方便移植到其他项目。module async_fifo #( parameter DATA_WIDTH 8, parameter FIFO_DEPTH 16 )( input wire wr_clk, input wire wr_rst_n, input wire wr_en, input wire [DATA_WIDTH-1:0] wr_data, output wire wr_full, output wire [clog2(FIFO_DEPTH):0] wr_count, input wire rd_clk, input wire rd_rst_n, input wire rd_en, output wire [DATA_WIDTH-1:0] rd_data, output wire rd_empty, output wire [clog2(FIFO_DEPTH):0] rd_count ); // 检查参数合法性 initial begin if (FIFO_DEPTH (FIFO_DEPTH - 1)) begin $error(FIFO_DEPTH must be power of 2); $finish; end end localparam ADDR_WIDTH clog2(FIFO_DEPTH); localparam PTR_WIDTH ADDR_WIDTH 1; // 指针信号 reg [PTR_WIDTH-1:0] wptr_bin, wptr_bin_next; reg [PTR_WIDTH-1:0] rptr_bin, rptr_bin_next; wire [PTR_WIDTH-1:0] wptr_gray, rptr_gray; reg [PTR_WIDTH-1:0] wptr_gray_reg, rptr_gray_reg; // 同步寄存器 reg [PTR_WIDTH-1:0] wq1_rptr_gray, wq2_rptr_gray; reg [PTR_WIDTH-1:0] rq1_wptr_gray, rq2_wptr_gray; // 存储RAM reg [DATA_WIDTH-1:0] mem [0:FIFO_DEPTH-1]; wire wclk_en wr_en ~wr_full; wire rclk_en rd_en ~rd_empty; // 写指针逻辑 always (posedge wr_clk or negedge wr_rst_n) begin if (!wr_rst_n) wptr_bin {PTR_WIDTH{1b0}}; else if (wclk_en) wptr_bin wptr_bin 1b1; end always (*) begin wptr_bin_next wptr_bin; if (wclk_en) wptr_bin_next wptr_bin 1b1; end // 读指针逻辑 always (posedge rd_clk or negedge rd_rst_n) begin if (!rd_rst_n) rptr_bin {PTR_WIDTH{1b0}}; else if (rclk_en) rptr_bin rptr_bin 1b1; end always (*) begin rptr_bin_next rptr_bin; if (rclk_en) rptr_bin_next rptr_bin 1b1; end // 二进制转格雷码 assign wptr_gray (wptr_bin_next 1) ^ wptr_bin_next; assign rptr_gray (rptr_bin_next 1) ^ rptr_bin_next; // 写指针格雷码打拍同步到读时钟域 always (posedge rd_clk or negedge rd_rst_n) begin if (!rd_rst_n) begin rq1_wptr_gray {PTR_WIDTH{1b0}}; rq2_wptr_gray {PTR_WIDTH{1b0}}; end else begin rq1_wptr_gray wptr_gray; rq2_wptr_gray rq1_wptr_gray; end end // 读指针格雷码打拍同步到写时钟域 always (posedge wr_clk or negedge wr_rst_n) begin if (!wr_rst_n) begin wq1_rptr_gray {PTR_WIDTH{1b0}}; wq2_rptr_gray {PTR_WIDTH{1b0}}; end else begin wq1_rptr_gray rptr_gray; wq2_rptr_gray wq1_rptr_gray; end end // 写满判断 assign wr_full (wptr_gray[PTR_WIDTH-1] ! wq2_rptr_gray[PTR_WIDTH-1]) (wptr_gray[PTR_WIDTH-2] ! wq2_rptr_gray[PTR_WIDTH-2]) (wptr_gray[PTR_WIDTH-3:0] wq2_rptr_gray[PTR_WIDTH-3:0]); // 读空判断 assign rd_empty (rptr_gray rq2_wptr_gray); // RAM写 always (posedge wr_clk) begin if (wclk_en) mem[wptr_bin[ADDR_WIDTH-1:0]] wr_data; end // RAM读 assign rd_data mem[rptr_bin[ADDR_WIDTH-1:0]]; // 数据计数可选用于调试和流量控制 reg [PTR_WIDTH:0] wr_count_reg, rd_count_reg; always (posedge wr_clk or negedge wr_rst_n) begin if (!wr_rst_n) wr_count_reg 0; else if (wclk_en !wr_full) wr_count_reg wr_count_reg 1b1; end always (posedge rd_clk or negedge rd_rst_n) begin if (!rd_rst_n) rd_count_reg 0; else if (rclk_en !rd_empty) rd_count_reg rd_count_reg 1b1; end assign wr_count wr_count_reg; assign rd_count rd_count_reg; // clog2 函数 function integer clog2(input integer depth); begin clog2 0; depth depth - 1; while (depth 0) begin clog2 clog2 1; depth depth 1; end end endfunction endmodule3.2 代码里几个容易写错的细节第一个是写满判断用的是wptr_gray还是wptr_bin。很多初版代码会把指针直接打拍到对端然后在对端比较这样逻辑上能跑通但实际上把“本地指针的下一状态”和“对端同步过来的当前状态”混在一起空满标志会提前或延后一个周期极端情况下会导致FIFO写出脏数据。正确做法是本地侧指针用格雷码组合逻辑输出对端指针用打两拍后的格雷码这两个值本身就有同步延迟比较结果天然是保守的安全。第二个是RAM读数据不能用rd_en做时钟使能来控制。我见过有的写法是always (posedge rd_clk) begin if (rd_en !rd_empty) rd_data mem[rptr_bin[ADDR_WIDTH-1:0]]; end这种写法不能说不可以但它引入了一个额外的输出寄存器导致读数据比读指针晚一个周期。如果你的下游逻辑是用rd_empty作为数据有效标志这个延迟会把时序搞乱。更稳妥的方式是组合逻辑直接读RAM让rd_data与读指针同步变化然后由下游在rd_en有效时采样。设计上建议按FIFO IP核的习惯来读数据不额外寄存简单直接。第三个是复位同步问题。上面的代码用的是异步复位、同步释放的标准写法。但要注意异步FIFO两端有不同的复位如果一端还在复位状态另一端已经开始工作指针可能处于不确定状态。工程上应保证两端复位释放后再进行正常的读写操作。具体实现时可以使用复位同步器把各时钟域的复位信号同步到本地时钟后再使用。3.3 格雷码和二进制指针的位宽选择我见过有人在参数化设计时只把地址位宽设成ADDR_WIDTH指针就用了ADDR_WIDTH位然后空满判断靠计数器。这种做法在同步FIFO里没问题但在异步FIFO里会出大问题因为你无法区分FIFO空和满时指针相等的情况。所以指针位宽必须比地址位宽多1位。以深度16为例地址位宽是4指针位宽是5。RAM寻址只取低4位最高位用于空满判断。这个最高位本质上是记录“当前指针相对于另一个指针跑了第几圈”。写指针跑了第一圈、写回起始地址时最高位为1读指针追上时如果两者最高位不同、剩余位相同说明写指针多跑了一圈FIFO满。用这个方法即便FIFO深度很大比如1K、4K也只需要把参数改大逻辑不用动。我项目中用到深度最大的异步FIFO是4096数据位宽64bit只靠这一套代码就能覆盖。3.4 非2的幂次深度怎么办虽然我强烈建议深度用2的幂次但工程中偶尔会遇到16.5K、48K这种非整数幂次需求。这种情况下有两个思路一是用同步FIFO异步桥接但结构复杂二是直接把深度向上取到2的幂次比如需求48K就用64K多出来的空间只影响资源占用不影响逻辑正确性。还有一些设计会在RAM写侧加“写指针不覆盖未读区域”的保护逻辑本质上是把FIFO当成循环队列用深度天然是2的幂次。遇到这种需求我建议直接评估使用Xilinx或Intel的FIFO IP核它们对非2幂次支持更完善省时省力。4. 仿真与验证怎么证明FIFO是对的4.1 基本testbench结构异步FIFO的验证核心是随机化读写覆盖空满标志跳变。简单的testbench结构如下两个独立时钟写侧随机使能读侧随机使能交叉校验数据。module tb_async_fifo; reg wr_clk, wr_rst_n; reg rd_clk, rd_rst_n; reg wr_en, rd_en; reg [7:0] wr_data; wire [7:0] rd_data; wire wr_full, rd_empty; async_fifo #(.DATA_WIDTH(8), .FIFO_DEPTH(16)) dut ( .wr_clk(wr_clk), .wr_rst_n(wr_rst_n), .wr_en(wr_en), .wr_data(wr_data), .wr_full(wr_full), .rd_clk(rd_clk), .rd_rst_n(rd_rst_n), .rd_en(rd_en), .rd_data(rd_data), .rd_empty(rd_empty) ); initial begin wr_clk 0; forever #5 wr_clk ~wr_clk; // 100MHz end initial begin rd_clk 0; forever #7 rd_clk ~rd_clk; // 约71.4MHz故意不同频 end // 测试激励... endmodule两个时钟频率特意选得不那么“整齐”比如一个100MHz一个71.4MHz这样读写指针的相位关系会持续变化能暴露更多边界问题。有些同学验证时两个时钟频率设成整数倍关系比如100MHz和200MHz边界相对固定问题不容易触发。4.2 验证的核心指标第一个指标是数据完整性。写入的数据如果是递增序列读出时也必须是递增序列中间不能丢、不能重、不能乱。这个在testbench里用一个期望值变量每读一个数就加1和读出的数据比对。第二个指标是空满标志的正确性。可以通过两种方式交叉验证一是用高层次的参考模型同步监控读写计数估算FIFO内应该有多少数据和rd_empty、wr_full做比对二是看时序行为比如wr_full拉高后继续写FIFO不能被覆盖rd_empty拉高后继续读读出数据保持不变。第三个指标是跨时钟域延迟。同步两级会带来2~3拍延迟这意味着rd_empty不可能在最后一个数据写入后立即拉低wr_full也不可能在最后一个数据读取后立即拉低。这些滞后是正常的如果完全不滞后反而说明同步逻辑可能被优化掉了需要检查综合后的网表。4.3 常见仿真失败模式仿真最容易遇到两个问题。第一个是死锁。写侧不停地写读侧因为空标志一直不读或者读侧不停地读写侧因为满标志一直不写两边互等仿真挂起。这通常是空满判断逻辑写反了。排查方法是分别观察wq2_rptr_gray和rq2_wptr_gray是否在变化确认同步链路通了再查判断逻辑。第二个是初始状态错误。复位后wptr_gray和rptr_gray都是0rd_empty应为高wr_full应为低。如果复位后rd_empty没拉高说明复位或空判断逻辑有问题。我调试过一个案例复位逻辑写成了同步复位但复位信号本身是异步释放的在仿真里一直对不上换成异步复位后立刻正常。5. 工程落地OV7670图像采集场景中的异步FIFO5.1 为什么OV7670要接FIFOOV7670是OV公司的CMOS图像传感器输出格式可以从YUV到RGB565帧率常见30fps输出像素时钟PCLK大约在24MHz左右。按照VGA分辨率640x480算一帧约30万个像素30fps就是每秒约920万个像素换算成数据量大约18MB/s。这个速率对底层来说不算快但问题是PCLK完全由sensor产生和你的主控时钟没有任何同步关系。如果你直接用主控的GPIO去采PCLK和数据会出现两种情况要么PCLK频率低时轮询等待浪费时间要么PCLK频率高时来不及采样丢数据。而OV7670输出不带FIFO的型号很常见它只有一组并口PCLK、DATA[7:0]、VSYNC、HREF没有内置缓存能力你的处理器要么每一拍都盯住PCLK要么就彻底放弃。解决方案就是在sensor和主控之间加一颗异步FIFO。sensor的PCLK作为FIFO的写时钟主控的系统时钟作为FIFO的读时钟。这样主控只需定时去读FIFO不需要实时跟踪PCLK。FIFO在这里起到两个作用跨时钟域缓冲和速率匹配。5.2 深度怎么算FIFO深度取决于最坏情况下的burst长度。OV7670的图像数据是逐行输出的每行有效像素约640个每像素2字节RGB565也就是一行约1280字节。如果主控响应足够快FIFO只需要缓存一行多一点的数据即可比如深度2048。但如果主控被其他中断占用最长响应时间不确定深度就要加大。工程上可以这样算根据主控最坏响应时间 T_max 和写数据速率 R_wFIFO深度应至少为 T_max x R_w。假设主控最坏情况下1ms才响应一次读取而sensor写速率按PCLK 24MHz、每像素2字节算约48MB/s那么1ms就是48KBFIFO深度至少48KB。如果超过这个量就得选择用DMA或者更大的SRAM做帧缓存了。实际项目中我在OV7670采集卡上用的是深度4096x16bit的异步FIFO主控每扫描线中断一次去读FIFO配合DMA搬运效果很稳定。如果你的主控总线带宽紧张建议从深度4096起步做测试再根据实际丢帧情况调整。5.3 同步逻辑在图像采集中的特殊要求图像数据对错位非常敏感。如果FIFO的读指针跨时钟域同步出了问题读出来的数据可能整体偏移几个像素视觉表现就是画面出现斜纹或局部错位。这种故障很难通过看波形发现因为数据本身还是连续的只是起始位置不对。这种情况下光靠仿真不够必须做上板验证。我的经验是先把VSYNC、HREF这些行场同步信号也接入FIFO控制逻辑作为数据包的边界标记。每次检测到VSYNC有效后主控先清空FIFO再开始读取新的帧数据这样即使上一帧读指针有偏移下一帧也会被重新对齐不至于持续错位下去。另外异步FIFO的空标志在图像采集场景中不能直接用作行数据有效的判断。因为空标志是跨时钟域同步后的滞后信号可能造成数据采样的起始点晚几拍。正确的做法是以HREF和主控侧的读使能共同来决定数据是否有效FIFO只负责数据缓冲和时钟转换。6. 常见问题与排查技巧实录6.1 问题速查表现象可能原因排查方法仿真中rd_empty一直为高复位异常或指针同步未通检查复位时序观察rq2_wptr_gray是否跟随wptr_gray变化wr_full提前拉高格雷码空满判断逻辑错误核对判断条件尤其注意最高位和次高位的处理读出数据偶尔乱序读指针同步异常或RAM读时序不匹配观察rptr_gray同步到写侧后的值检查RAM读端口是否有竞争上板画面错位、斜纹帧同步信号处理不当在VSYNC时清空FIFO用帧同步信号复位读指针逻辑两个时钟频率接近时丢数同步器延迟过大FIFO吞吐率不足增大FIFO深度或改用IP核检查读写带宽余量综合后时序违例两级同步器位置不合理将同步器打拍寄存器放到靠近目标时钟域的触发器旁6.2 一个真实的排查案例我调试一块采集板时遇到的问题是这样的FIFO深度1024数据位宽32bitsensor侧写时钟20MHz主控侧读时钟150MHz。理论上一行数据约9KB平均每秒读取完全够用。但实际运行时偶尔会出现整行数据错位而且不是固定行而是随机行。排查过程花了两天。先抓波形看wr_full和rd_empty都很正常没有出现误判。再看数据内容错位行整行偏移了1个像素。这个现象很典型——不是数据丢了而是读侧采样的起始位置晚了1拍。最后定位到问题读侧代码里读取FIFO时直接用了rd_empty作为数据有效的判断但rd_empty本身是跨时钟域同步后的信号在FIFO为空时拉高在FIFO写入第一个数据后还要经过2~3个主控时钟周期才能拉低。如果主控恰好在这个延迟窗口内启动了读取就会读到一个无效数据或错过第一个有效数据。修复方案是在读出数据前加一个“预读”使能逻辑主控不直接用rd_empty判断而是用一个额外的握手状态机检测到rd_empty拉低后再等两个周期才开始读确保数据真正有效。改完后再跑上板验证错位彻底消失。6.3 如何避免深亚稳态问题两级同步器在绝大部分场景下够用但如果两个时钟的频率比非常接近比如100MHz和101MHz同步器失效的概率会明显上升。原因是两个时钟的上升沿经常“挤”在一起被采样信号在时钟沿附近翻转的概率增大。这种情况下有几种处理手段。一是用三极同步器多打一拍给亚稳态更多衰减时间。代价是延迟多一拍空满判断更滞后。二是调整FIFO深度或时钟频率比让读写时钟避开近似同频。三是对于极关键的应用用专门的CDC验证工具做形式化验证确保跨时钟域路径没有漏约束。我在工程里的原则是时钟频率比超过1:2时用两级同步器1:1到1:2之间用三级同步器。如果需求特殊直接调IP核或做异步桥接不自己手搓同步器。6.4 综合约束怎么写异步FIFO的综合约束和同步逻辑不太一样。两条跨时钟域路径要设为异步约束或false path一是wptr_gray到rq2_wptr_gray的同步路径二是rptr_gray到wq2_rptr_gray的同步路径。如果没设约束综合工具会认为这些路径需要在一个周期内收敛极有可能报出时序违例。在Vivado里可以用set_clock_groups -asynchronous来声明两组时钟异步或者在XDC里写set_false_path -from [get_clocks wr_clk] -to [get_clocks rd_clk]。但要注意只对跨时钟域路径设false pathFIFO内部的写时钟域逻辑和读时钟域逻辑分别按各自的时钟约束收敛不能把整个FIFO都设为false path。还有一个细节同步器的两拍寄存器最好在RTL里用(* ASYNC_REG TRUE *) 或 synthesis attribute 显式标注避免综合工具把它们优化合并或搬到不理想的位置。找得越早后端越省心。7. 我的实测心得与扩展建议异步FIFO这套东西光看代码觉得不难真正上手踩过坑才明白细节有多重要。几个我个人的习惯分享出来供参考。第一坚持指针位宽比地址位宽多1位。哪怕深度很小比如8也老老实实让指针位宽为4。这个习惯能避免很多低级的空满误判问题而且代码改成参数化时不用再回头补位宽。第二仿真时钟频率不要用整数倍关系。我习惯让写时钟和读时钟的周期互质或者至少是不成倍数关系这样能在仿真里覆盖更多相位组合比固定频率比能多暴露不少问题。第三上板验证时加帧同步清空逻辑。不管是摄像头数据还是高速ADC数据只要数据有帧的概念就一定在帧起始位置把FIFO清一次保证每帧数据从已知状态开始。这比工程上去抓数据错位要省力得多。第四能用IP核就用IP核。Xilinx和Intel的FIFO IP核在跨时钟域处理上经过了大量验证时序约束和属性标注都很完善。自己写异步FIFO适合学习、面试、以及需要特殊定制比如多个时钟域复用指针的场景但量产项目里没必要拿稳定性冒险。异步FIFO以后大概率还会遇到各种变体比如多通道异步FIFO、带水印可编程标志的FIFO、多个读写端口的FIFO。掌握了这套指针同步和空满判断的核心思想换什么变体都是围绕同一套逻辑展开。我建议读者拿到这份代码后先跑通仿真再上板验证然后把FIFO深度、数据位宽改几个值重新跑一遍看看代码是否真的参数化合格。这套流程走完你对异步FIFO的理解会扎实很多。
返回列表