ARTICLE DETAIL

资讯详情

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

FPGA跨时钟域数据缓冲:AXI4-Stream FIFO设计与调试指南

FPGA跨时钟域数据缓冲:AXI4-Stream FIFO设计与调试指南 但凡在 FPGA 里做过超过一个模块的数据通路就一定绕不开 AXI4-Stream 和 FIFO 这两个东西。尤其是上游产生数据的时钟和下游消费数据的时钟不是同一个频率的时候一个可靠的跨时钟域数据缓冲系统就是你整个设计能不能稳定跑起来的基石。这篇文章从一个实际项目的角度讲讲怎么从零把 AXI4-Stream FIFO 搭起来里面会给出可以直接参考的 Verilog 例化代码和手写实现也会把 FICO 深度计算、空满标志时序、上板调试这些最坑的地方讲清楚。适合正在做接口对接、数据采集、 DMA 搬运的同学参考。1. 为什么需要一个 AXI4-Stream FIFO1.1 AXI4-Stream 协议抓住这几个信号就够用接触过 Xilinx IP 的人对 AXI4-Stream 都不会陌生它本质上就是一套基于 valid/ready 握手的点对点数据流协议。相比 AXI4 全协议它去掉了地址通道只保留了数据通道非常适合做高速数据流的搬运。协议本身看起来简单核心就这几个信号TVALID发送方拉高表示本拍数据有效。TREADY接收方拉高表示本拍可以接收数据。TDATA数据总线位宽可以自定义。TLAST一个包packet/burst的最后一个数据字。TKEEP / TSTRB字节有效掩码用于窄位宽传输时标记哪些字节是有效的。握手规则只有一条只有当 TVALID 和 TREADY 同时为高时这一拍的数据才算真正被传输transfer 发生。这条规则必须刻在脑子里。因为不管是写侧还是读侧数据有没有被消费、指针该不该加、计数器该不该递增全部以这一次握手为依据。我在调试中见过太多人把数据发出的条件写成 “只要 TVALID 拉高就算发出”结果 downstream 还没 ready数据就丢了。AXI4-Stream 还有一个特点发送方在 TVALID 拉高后在握手完成之前不能改变 TDATA。也就是说一旦你拉高了 valid数据就要保持稳定直到对面的 tready 也拉高为止。这个特性在写自动机的时候要特别注意很多跨时钟域丢数的问题就是这么来的。1.2 跨时钟域为什么不能直接连举个最常见的场景ADC 芯片输出的采样时钟是 100 MHzFPGA 内部逻辑把采样数据打包成 AXI4-Stream 流下游是一个工作在 150 MHz 的 DMA 控制器或者 DSP 模块。两个模块时钟频率不同相位也不确定数据线直接连过去必然出问题。这里面的根本原因叫亚稳态。当时钟沿到达时如果触发器的数据输入刚好在变化那么触发器的输出可能进入一个既不是 0 也不是 1 的中间状态而且这个状态要过一段时间才能稳定。更麻烦的是这个不稳定时间长度是随机的如果后面再接组合逻辑亚稳态可能继续传播最后导致整个模块状态错乱。解决亚稳态的经典办法是加两级同步寄存器让亚稳态在同步器内部消化掉。两级同步器的原理是即使第一级寄存器出现亚稳态经过一个时钟周期后信号大概率已经稳定下来第二级寄存器采到的就是稳定值。注意我这里说的是“大概率”——从工程统计角度两级同步器的失效率已经低于芯片本身的 MTBF 要求所以被行业普遍接受。但是问题来了如果跨时钟域传的是一个多 bit 的计数器或者数据总线直接用两级同步器同步是危险的。因为多 bit 信号在某一时刻可能只有部分 bit 发生了变化同步器采样时可能采到新旧混杂的值这在工程上叫“中间态错码”。比如计数器从 0111 变成 1000如果四个 bit 到达同步器的时序有微小差异接收端可能采到 0100、1100 等任意中间值。为了解决这个问题异步 FIFO 出来了。它的核心思想是用格雷码表示读写指针保证指针递增时每次只有 1 bit 变化即使跨时钟域采样产生亚稳态最多也只是多等一拍不会读到完全错误的指针值。然后用同步后的指针做空满判断通过“保守方向”的延迟来保证不溢出、不读空。这就是异步 FIFO 能够在跨时钟域场景下稳定工作的原理。1.3 同步 FIFO 还是异步 FIFO很多人把 FIFO 当成一个东西用实际上同步 FIFO 和异步 FIFO 的适用场景差别非常大。同步 FIFO 的读时钟和写时钟是同一个时钟源它的作用是缓存节奏解决“上游突发写入、下游偶尔读取”的速率不匹配问题。比如一个数据包处理模块输入可能在一段时间内连续收到大量数据而处理单元每处理一个数据需要多个周期中间就需要一个同步 FIFO 来吸收突发。异步 FIFO 的读时钟和写时钟是独立的两个时钟域它的核心作用是跨时钟域数据传输同时兼有缓冲能力。比如 ADC 采样数据进入 FPGA采样时钟域是 100 MHzDSP 处理时钟域是 200 MHz两者之间必须用异步 FIFO。在 AXI4-Stream 接口层面FIFO 可以是同步的也可以是异步的主要看读写两侧的时钟是否同源。Xilinx 的 AXI4-Stream Data FIFO IP 允许你在配置时选择“Common Clock”或者“Independent Clocks”选后者就是异步 FIFO选前者就是同步 FIFO。我见过不少人在只是做数据缓冲的时候误用了异步 FIFO虽然功能上也能跑但资源占用更大、时序收敛更难还会引入不必要的 CDC 风险。反过来如果读写时钟确实不同却用了同步 FIFO那基本上板上跑起来第一轮就会丢数据。所以动手前第一件事是搞清楚收发两侧到底是不是同源时钟。2. 整体设计思路与 FIFO 深度估算2.1 从系统角度看数据通路在真正写代码之前我习惯先画一条数据流的完整链路把写侧和读侧分别列出来。写侧通常包含数据源ADC、网络 MAC、图像传感器等、打包逻辑拼接 bit、插入包头、生成 TLAST、FIFO 写端口。读侧通常包含FIFO 读端口、解包逻辑、下游处理模块DMA、DSP、CPU 接口等。设计时要明确的几个问题数据包的粒度是什么是字节流还是定长包这决定了 TLAST 的用法。读写时钟频率分别是多少这决定了必须用同步还是异步 FIFO。上游允许的反压延迟是多少有的数据源不支持长时间被拉低 ready这需要 almost_full 提前预警。下游读取的急迫程度如何如果读侧长期不读FIFO 会写满此时写侧必须能停下来。这些问题里最容易忽略的是“上游能不能被反压”。比如有的 ADC 接口时序要求每 N 个时钟必须读走一个采样否则后续数据会覆盖。这种情况下不仅要算 FIFO 深度还可能要加大 FIFO 深度或者降低上游数据率否则再深的 FIFO 也只是延迟了问题爆发的时间。还有一个常见误区只看平均速率不看突发。两个模块的平均吞吐可能相近但是只要出现瞬时突发FIFO 深度不够就会在一定时间后溢出。所以深度计算一定要基于“突发 延迟补偿”的角度而不是简单的带宽比。2.2 FIFO 深度计算一个例子搞定深度计算这个问题网上一搜一大堆公式看起来复杂实际上抓住一个核心思路就能算清楚在最坏情况下FIFO 里要存下“还没被读走的数据量”的最大值。用一个实际的例子来讲。假设写侧是一个 ADC 采集模块100 MHz 采样时钟16 bit 位宽。每次触发后 ADC 连续输出 256 个采样然后停一段比较长的时间等待下一次触发。读侧是一个 DMA 控制器工作在 150 MHzDMA 收到请求后需要 20 个读时钟的仲裁延迟才开始读数据之后可以连续读取。考虑最坏情况ADC 突发刚开始时DMA 还没有开始读。那么在这段突发时间内FIFO 里至少要能装下 256 个采样。同时DMA 从请求到真正开始读有 20 个 150 MHz 时钟的延迟换算成写时钟域是 20 × 100 / 150 ≈ 13.3 个写时钟也就是大约还要多缓冲 14 个数据。另外异步 FIFO 的空满信号经过两级同步器是有延迟的一般要额外留 2~4 个数据的余量。所以粗略算下来FIFO 深度 ≥ 256 14 4 274取 2 的幂次所以选 512 深度留出一倍余量稳妥。这里强调一下FIFO 深度取 2 的幂次在异步 FIFO 实现中是必须的因为格雷码回绕要求深度是 2 的幂。用 IP 核时深度选择框也通常只允许填 2 的幂。再看另一种更严峻的情况写侧平均速率大于读侧平均速率。比如写侧持续以 100 MHz、每拍 1 个数据灌入读侧只有 80 MHz 的读取能力那么即使 FIFO 深度再大最终也会被写满。这种情况只能靠反压强行降低写侧速率或者修改系统架构让写侧不是持续写。FIFO 在这里的作用只是吸收突发不能解决长时间的平均速率差。2.3 位宽、带宽匹配与额外开销AXI4-Stream FIFO 还经常承担一个额外功能位宽转换。Xilinx 的 AXI4-Stream Data FIFO IP 允许写侧和读侧配置不同的数据位宽IP 内部会把一个宽数据字拆成多个窄数据字或者反过来合并。这个功能很实用比如写侧来自网口是 64 bit读侧接的 DDR 控制器是 256 bit就可以直接靠 FIFO 做位宽转换省去一个独立的转换模块。但是位宽转换隐藏的坑很多。最典型的是 TKEEP 的映射问题。写侧 64 bit 数据里如果只有 3 个字节有效TKEEP 是 8b00000111读侧是 32 bit 位宽那么拆成两个 32 bit 字时第一个字 TKEEP 是 4b0111第二个字 TKEEP 是 4b0000。IP 会处理好这个映射但如果你在 FIFO 后面又接了自定义逻辑并且使用了 TKEEP那么一定要搞清楚 IP 输出的 TKEEP 规则否则数据错位很难查。还有 TLAST 的定位。一个宽字拆成多个窄字后TLAST 必然落在最后一个窄字上。如果原始数据不是 64 bit 的整数倍最后一个宽字里有效的窄字数量不确定TLAST 的时序就会变得很微妙。这时候建议在 FIFO 之前的打包逻辑里就把长度对齐避免在 FIFO 输出端处理不规则的包尾。另外要注意一旦使用了异步 FIFO 位宽转换跨时钟域的字节序和位序要保持一致。有些 IP 默认是 little-endian有些是大端上下游必须统一。我曾经在一个数据采集项目里踩过这个坑FIFO 出来的数据顺序和原始字节序相反在仿真里完全正常因为 testbench 是自己写的自己给自己圆过去了一到板子上和 PC 端软件对接就全乱了。3. 实操IP 核配置与 Verilog 例化3.1 直接用 Xilinx AXI4-Stream Data FIFO IP实际工程里我强烈建议优先用 Xilinx 官方的 AXI4-Stream Data FIFO IPVivado IP Catalog 里搜 “AXI4-Stream Data FIFO”不要自己手写异步 FIFO。这个 IP 经历过的验证量比任何个人代码都大而且时序收敛、资源优化都做得很好。IP 核系统自带的 AXI 参考设计也比自己写的容易接入 CTS、跨时钟域约束。配置 IP 时我一般固定会检查这几项FIFO Depth按上一节的深度估算结果填写单位是数据字word注意和位宽组合后的实际字节数区分。Read Data Width / Write Data Width如果读写位宽不同IP 会自动插入位宽转换逻辑。建议在配置页明确读出和写入位宽避免默认值不符合实际。Enable TLAST如果数据流有包边界必须勾上。Enable TKEEP如果数据流存在非对齐字节有效必须勾上如果数据永远是全字节有效可以关掉以节省资源。Independent Clocks 还是 Common Clock异步场景选 Independent Clocks同步场景选 Common Clock。这里注意即使两边时钟频率一样只要不是同一个时钟源也要选 Independent Clocks否则仍然是跨时钟域问题。Almost Full / Almost Empty 阈值如果上游不能被深度反压可以把 Almost Full 阈值调小提前拉高预警信号。Synchronization Stages默认 2 即可除非约束要求更高 MTBF一般不需要动。配置完 IPVivado 会生成一个例化模板你只需要把对应的时钟、复位和数据信号连上即可。3.2 顶层模块例化模板下面给出一段常见的 AXI4-Stream Data FIFO 的 Verilog 例化代码这里假设写侧时钟 clk_wr 100 MHz读侧时钟 clk_rd 150 MHz数据位宽 32 bit带 TLAST 和 TKEEP。axi4stream_data_fifo_0 u_axis_fifo ( .s_axis_aclk (clk_wr), .s_axis_aresetn (rst_wr_n), .s_axis_tvalid (wr_tvalid), .s_axis_tready (wr_tready), .s_axis_tdata (wr_tdata), .s_axis_tkeep (wr_tkeep), .s_axis_tlast (wr_tlast), .m_axis_aclk (clk_rd), .m_axis_aresetn (rst_rd_n), .m_axis_tvalid (rd_tvalid), .m_axis_tready (rd_tready), .m_axis_tdata (rd_tdata), .m_axis_tkeep (rd_tkeep), .m_axis_tlast (rd_tlast), .almost_full (fifo_almost_full), .almost_empty (fifo_almost_empty) );需要注意AXI4-Stream Data FIFO 的复位是低电平有效异步复位但复位释放必须与各自时钟同步。最稳妥的做法是在每个时钟域里做一个“异步复位、同步释放”的复位同步器然后把同步后的复位信号分别接到 s_axis_aresetn 和 m_axis_aresetn 上。直接用一个全局复位信号同时驱动两端在异步时钟域里是不安全的。关于 almost_full 和 almost_empty我在实际项目中一般会让它们参与上游流量控制而不是只在调试时看。比如 ADC 采集模块可以在 almost_full 拉高时主动暂停产生新的突发这样能避免 FIFO 真的写满后写侧被强制反压导致的时序问题。如果上游没有这个机制那 almost_full 至少可以用来触发一个计数统计方便定位瓶颈。3.3 手写一个精简版 AXI4-Stream 异步 FIFO有的读者可能会问如果我不想用 IP或者希望搞明白内部原理能不能手写一个 AXI4-Stream 异步 FIFO答案是能。手写一个不复杂但前提是你知道它比 IP 少哪些东西。下面给出一个教学用的精简版实现。它的核心是一个标准的异步 FIFO双口 RAM 格雷码指针同步外面套上 AXI4-Stream 的 valid/ready 握手逻辑。为了突出 FIFO 核心这里只实现了 TDATA 和 TLAST没有实现 TKEEP如果需要 TKEEP只需要把 RAM 位宽扩展把 TKEEP 和数据一起存进去读侧原样输出即可。module axis_async_fifo #( parameter DATA_WIDTH 32, parameter ADDR_WIDTH 8 )( input wire s_axis_aclk, input wire s_axis_aresetn, input wire s_axis_tvalid, output wire s_axis_tready, input wire [DATA_WIDTH-1:0] s_axis_tdata, input wire s_axis_tlast, input wire m_axis_aclk, input wire m_axis_aresetn, output wire m_axis_tvalid, input wire m_axis_tready, output wire [DATA_WIDTH-1:0] m_axis_tdata, output wire m_axis_tlast ); localparam PTR_WIDTH ADDR_WIDTH 1; localparam DEPTH (1 ADDR_WIDTH); reg [PTR_WIDTH-1:0] wptr, rptr; reg [PTR_WIDTH-1:0] wptr_gray, rptr_gray; reg [PTR_WIDTH-1:0] rptr_gray_sync1, rptr_gray_sync2; reg [PTR_WIDTH-1:0] wptr_gray_sync1, wptr_gray_sync2; reg [DATA_WIDTH:0] mem [0:DEPTH-1]; wire write_en; wire read_en; wire full; wire empty; wire [PTR_WIDTH-1:0] wptr_gray_next wptr ^ (wptr 1); wire [PTR_WIDTH-1:0] rptr_gray_next rptr ^ (rptr 1); assign write_en s_axis_tvalid s_axis_tready; assign read_en m_axis_tvalid m_axis_tready; assign full (wptr_gray {~rptr_gray_sync2[PTR_WIDTH-1:PTR_WIDTH-2], rptr_gray_sync2[PTR_WIDTH-3:0]}); assign empty (rptr_gray wptr_gray_sync2); assign s_axis_tready ~full; assign m_axis_tvalid ~empty; assign {m_axis_tlast, m_axis_tdata} mem[rptr[ADDR_WIDTH-1:0]]; // 写指针与写数据 always (posedge s_axis_aclk or negedge s_axis_aresetn) begin if (!s_axis_aresetn) begin wptr h0; end else if (write_en) begin mem[wptr[ADDR_WIDTH-1:0]] {s_axis_tlast, s_axis_tdata}; wptr wptr 1b1; end end // 写指针格雷码 always (posedge s_axis_aclk or negedge s_axis_aresetn) begin if (!s_axis_aresetn) begin wptr_gray h0; end else begin wptr_gray wptr_gray_next; end end // 读指针 always (posedge m_axis_aclk or negedge m_axis_aresetn) begin if (!m_axis_aresetn) begin rptr h0; end else if (read_en) begin rptr rptr 1b1; end end // 读指针格雷码 always (posedge m_axis_aclk or negedge m_axis_aresetn) begin if (!m_axis_aresetn) begin rptr_gray h0; end else begin rptr_gray rptr_gray_next; end end // 读指针格雷码同步到写时钟域 always (posedge s_axis_aclk or negedge s_axis_aresetn) begin if (!s_axis_aresetn) begin rptr_gray_sync1 h0; rptr_gray_sync2 h0; end else begin rptr_gray_sync1 rptr_gray; rptr_gray_sync2 rptr_gray_sync1; end end // 写指针格雷码同步到读时钟域 always (posedge m_axis_aclk or negedge m_axis_aresetn) begin if (!m_axis_aresetn) begin wptr_gray_sync1 h0; wptr_gray_sync2 h0; end else begin wptr_gray_sync1 wptr_gray; wptr_gray_sync2 wptr_gray_sync1; end end endmodule这段代码的思路是标准异步 FIFO 的教科书写法。写指针和读指针都多出一位用于判断空满回绕。写侧把“读指针格雷码同步值”和本地写指针格雷码比较如果两者只有最高两位相反、其余位相同说明 FIFO 已满读侧把“写指针格雷码同步值”和本地读指针格雷码比较相等即为空。这里有一个很重要的点由于同步器存在延迟写侧看到的“满”会滞后于实际状态。但这恰恰是安全的——滞后意味着写侧认为的满比实际满更保守只会多等不会溢出。同理读侧看到的“空”也滞后只会少读不会读空。不过边沿处可能多读一拍的问题我在第 4 章会专门讲。另外s_axis_tready 直接用 ~full 驱动说明只要不满就可以写入。这种简单写法在满信号刚拉高时可能还有一拍写入窗口但标准异步 FIFO 的满判断保证了这个窗口不会造成实际溢出最多损失一点吞吐。如果你要更高吞吐可以把 tready 生成逻辑加一级流水线但代价是增加一拍延迟。4. 集成调试与常见问题排查4.1 仿真阶段先确认这几件事写完代码先别急着上板在仿真环境里把几种典型场景跑一遍能省掉后面大量的调试时间。仿真要做的最核心的一件事写侧连续灌数据、读侧随机反压跑足够长时间然后比对进出数据量是否一致。不要只在理想情况下跑要在读侧随机拉高、拉低 tready模拟真实下游的忙闲状态。如果这个测试能跑通说明 FIFO 的基本数据完整性没问题。第二个要测的是背靠背读。也就是 FIFO 在写满后读侧连续读空检查会不会出现读空后还有数据溢出的情况。这个场景最容易暴露空满标志的边界问题。第三个是包边界测试。如果用了 TLAST要验证包长度是否正确。比如写侧发 10 个长度为 100 的包读侧必须收到 10 个 TLAST且每个包长度都是 100。这个可以用计数器在 testbench 里自动检查。还有一个容易被忽视的点跨时钟域仿真时两个时钟的频率差不能太大否则仿真时间会非常长。我一般用 100 MHz 和 150 MHz 这种比例既能覆盖异步场景又不会让一次仿真跑很久。4.2 空满标志与握手时序的坑异步 FIFO 的空满标志是“保守”的这在原理上是安全的但在边界条件下会引出一些奇怪的时序问题。最典型的一个坑是读侧空标志延迟导致的“多读一拍”。前面说了读侧判断空用的是同步过来的写指针格雷码这个值会滞后于实际的写指针。当 FIFO 真正为空的那一刻读侧可能还没看到写指针更新empty 还是低电平所以 m_axis_tvalid 仍然为高。如果这时候读侧发起一次握手读到的就是地址——也就是已经读过的旧数据数据被重复读取。这个问题在手写代码里是真实存在的在 Xilinx IP 里基本被内部机制消化掉了但理解它对你排查问题很有帮助。解决思路有两个一是给读侧加“输出寄存器 空标志对齐”。也就是把读数据打一拍同时把 valid 也打一拍使 valid 和 data 对齐并在内部增加指针比较逻辑使得当真实空时提前拉低 valid。这实际上就是 Xilinx IP 内部 output register 的做法。二是在协议层面规避。如果下游是 DMA 这类模块它们读取的总长度是已知的读完预定长度后就不再读这种情况下空边界多读的问题影响有限。但如果是自由运行的数据流下游一直等到 valid 拉低才算完那么多读的一拍会直接造成数据错误。我建议在项目里直接使用 IP除非你有非常明确的需求和足够的时间去验证手写 FIFO 的边界行为。手写 FIFO 适合学习原理、适合特殊资源场景但在量产项目里IP 的成熟度带来的收益远远大于省下来的那点 LUT。4.3 上板调试三板斧上板调试 AXI4-Stream 数据通路我自己的固定流程是三板斧抓波形、数个数、对内容。第一板斧是 ILA 抓波形。在 FIFO 的写端口和读端口各挂一个 ILA采样时钟分别用写时钟和读时钟。重点看 TVALID 和 TREADY 的握手情况。如果写侧 TREADY 长期为低说明 FIFO 满了或者没有真正读走如果读侧 TVALID 长期为低说明 FIFO 空或者写侧没有真正写入。这一步能快速定位问题在写侧还是读侧。第二板斧是数个数。在写侧统计完成握手的次数在读侧也统计完成握手的次数。两个计数器最终必须严格相等。如果对不上差多少、差在哪里都能通过 ILA 采到的握手信号逐拍定位。这个方法比单纯看波形高效得多因为长时间运行时的数据量是波形里看不出规律来的。第三板斧是对内容。如果数据内容是递增序列可以在读侧加一个校验模块检查读出来的值是否连续递增。如果是包头包尾格式就在包尾处校验整个包的累加和或者 CRC。通过内容校验能确认不是“数量对了但数据错位”这种隐性错误。我更推荐的做法是把递增计数器的宽度做得和数据位宽一致写侧用wr_cnt递增产生数据读侧用rd_cnt校验。注意在异步时钟域里这两个计数器不能直接比较只能在各自时钟域内独立校验“是否连续递增”。4.4 常见问题速查表下面这个表是我在带项目时总结出来的遇到类似现象可以直接对着排查。现象可能原因排查方向写侧 tready 一直为低FIFO 满标志一直被置位检查读侧时钟是否工作、复位是否释放、读侧是否真的在消费数据读侧 tvalid 一直为低FIFO 空标志一直被置位检查写侧时钟是否工作、写侧握手是否发生、数据是否真的写进去了数据偶尔丢一拍写侧在 FIFO 满时仍然握手成功检查 tready 生成逻辑确认是否用了满标志的异步版本检查同步器级数数据重复读出一拍读空边界多读检查 empty 标志是否滞后考虑加输出寄存器或者改用 IP复位后状态不定异步复位未同步释放每个时钟域独立做“异步复位同步释放”再接入 FIFO位宽转换后数据错位TKEEP/TLAST 映射错误核对 IP 配置观察 TKEEP 和 TLAST 波形确认字节序一致almost_full 一直为高阈值设置太小调整 almost_full 阈值或者检查下游是否长期不读仿真通过上板失败异步复位问题重点检查复位释放时序以及跨时钟域信号的约束这里多说一句复位。AXI4-Stream FIFO 的 aresetn 是低有效异步复位但释放必须同步。很多初学者在仿真里直接用一个 reg 生成复位信号testbench 里跑得挺好上板后偶尔出现“第一次复位后 FIFO 里残留数据”的灵异现象多半就是复位释放没做同步。两个时钟域各做一个同步释放的复位模块是标配。5. 写在最后的经验我做 FPGA 数据通路这几年FIFO 几乎出现在每一个项目里。如果让我给一个优先级排序第一建议是能用 IP 就用 IP不要自己在工程里造异步 FIFO 的轮子。IP 在时序、资源、边界处理上都经过了充分验证省下来的时间足够你去排查真正有挑战性的问题。第二建议是无论用 IP 还是手写实现一定要把空满标志当成“异步信号”来理解所有跟空满相关的边界条件都要专门建 testbench 去验证不要指望仿真的正常路径能覆盖到。第三建议是调试 AXI4-Stream 数据通路不要只盯波形一定要数个数、对内容。波形只能告诉你“看起来在工作”计数和校验才能告诉你“数据真的是对的”。如果你的项目里还涉及多个 FIFO 串联或者要做带宽监控、拥塞控制可以在 almost_full 信号的基础上加一个水位计数器把 FIFO 占用率实时统计出来。这个数据在上板调试时非常有用能直观看到瓶颈到底在写侧还是读侧。我后来做 PCIe 数据通路时就是靠这种水位统计定位到对方模块的反压时长从而重新分配了 FIFO 深度和数据调度策略。
返回列表