FPGA-FIFO

一、什么是FIFO

FIFO(First Input First Output,先进先出)是一种缓存机制。它的核心特点是:最先写入的数据最先被读出,常用于解决数据传输过程中的速率不匹配位宽不匹配跨时钟域问题。
在 FPGA 设计中,FIFO 通常以硬核IP(调IP核)或软逻辑(写代码的时候数据流转就按照FIFO形式)形式存在,是系统级设计中最常用的基础模块之一。

二、为什么一定要用FIFO

  1. 提高传输效率,提升存储带宽利用率
    在多通道采集场景下,如果直接把数据写入DDR,往往会造成带宽浪费。
    例如一块128路AFE(模拟前端)芯片采集回来的数据,假设每一路通道是14 位分辨率,采样速率达到几十MSPS。如果把这128路数据逐路、直接写入DDR,DDR总线一次写操作仅仅往里面写14bit,这时由于利用率会很低,很多位会被闲置。
    更高效的方法是,先将这128路数据通过FIFO缓存起来,待累积到足够的数据宽度(比如DDR总线宽度128bit或256bit的整数倍)后,再批量写入DDR。这样就能充分利用DDR的带宽,避免了“DDR每次写操作只装了一点点数据”的低效问题。
    只要FIFO的深度足够,就可以保证采集到的数据不会丢失,同时大幅提升DDR的带宽利用率。
  2. 数据位宽转换
    在系统中,常见到数据总线位宽不匹配的情况,例如 32bit 数据需要写入 128bit 总线。
    借助 FIFO,可以将多个 32bit 数据拼接缓存,再一次性以 128bit 的形式输出,实现位宽转换。
  3. 跨时钟域传输
    当数据源和数据接收端处于不同的时钟域时,直接传输会导致亚稳态问题。
    FIFO 提供了安全可靠的跨时钟域机制,能够保证数据正确同步,避免丢失或出错。

三、 FIFO 存储资源来自哪里?

在 FPGA 中,FIFO 本质就是存储器 + 控制逻辑:

  • 分布式 RAM (LUTRAM 也就是 Distributed RAM):用 LUT 做的小容量 RAM,适合做深度小、宽度小的 FIFO。
  • ※※※Block RAM (BRAM):常见 36Kb 块(= 36K bits ≈ 4.5 KB),大多数 FIFO 都基于它实现。※※※
  • UltraRAM (URAM)(UltraScale+ 架构,支持 URAM):单块 288Kb,适合超深 FIFO。

FIFO能有多深,取决于芯片BRAM/URAM的数量和FIFO的位宽。

最常用的是用FPGA片上的BRAM资源,那么为什么要用BRAM来实现FIFO?

序号 原因 解释
1 容量需求 FIFO 通常用于跨时钟域或者缓存大量数据(比如 DMA、AXI 总线传输、图像/音频数据流),存储深度往往较大。
BRAM 本身就是片上专用存储器,容量大(单块 BRAM 可以是 18Kb、36Kb 甚至更高)。
如果用分布式 RAM 实现深 FIFO,会消耗大量 LUT 逻辑资源,极不经济。
2 时序性能 BRAM 是专门为高速存储设计的,支持 同步读写、双口访问,性能稳定。
FIFO 的关键需求就是高速数据吞吐和低延迟切换,BRAM 在这方面表现更优。
分布式 RAM 则因为依赖 LUT 实现,布局分散、时序不稳定,延迟更大。
3 资源利用效率 LUT(查找表)本身是宝贵的逻辑资源,主要应用于组合逻辑和小规模存储。
FIFO 如果用分布式 RAM,大量 LUT 会被占用,导致逻辑电路资源紧张,影响整个设计的综合布线。
而 BRAM 是 FPGA 芯片专门为大容量存储准备的硬块,用它来实现 FIFO 能做到“各司其职”,效率更高。

四、怎么用FIFO

4.1 IP核法(参考mlk教程)

Xilinx 的 FIFO Generator IP 或原语,本质上是把底层的 BRAM 封装成标准的 FIFO 接口。这样用户只要调用 IP,就可以快速生成高性能 FIFO,而不用手动去管理 BRAM 读写控制。

4.1.1 IP核

下面讲两种FIFO的使用流程方法:半空半满法与关键信号法。

4.1.2 半空半满法

通过 FIFO 的空/满信号判断读写操作,避免溢出或欠读,原理图如下:

fifo_yuanlitu

什么是半空半满?半空/半满 并不是指 FIFO 里正好 50% 数据,而是指 设置一个阈值,当 FIFO 到达这个阈值时拉高标志位,但是也可以就写一半、读一半。比如:

  • FIFO总大小 = 32,768bit = 32bit * 1024深度 = 128bit * 256深度
  • 设置 almost_full 阈值 =127
  • 当FIFO的rd_data_count(fifo generate ip核自带的接口,同wr_data_cound可以显示蓄水池的水量)里存了≥127个128bit数据时,almost_full=1,提醒快要满了。这时需要触发读逻辑,唏哩呼噜读出来128个。
  • 设置 almost_empty 阈值 = 511
  • 当 FIFO 里 <=511个32bit数据时,almost_empty=1,提醒快要空了。这时需要触发写逻辑,写进去512个32bit数据。

在vivado中使用FIFO generate IP核,如下图:

fifo1

fifo2

fifo3

fifo4

配置好后,例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FIFO32_2_128 u_FIFO32_2_128 (
.rst (fifo_rst), //复位 可以0复位也可以1复位(IP核设置)
.wr_clk (clk_200m), //写时钟
.rd_clk (clk_100m), //读时钟 读写时钟分离、异步
.din ({24'd0, wr_cnt[7:0]}), // 32bit写入
.wr_en (wr_en),
.rd_en (rd_en),
.dout (rd_data), // 128bit读出
.full (full),
.almost_full (almost_full),
.empty (empty),
.almost_empty (almost_empty),
.rd_data_count (rd_data_count), // 128bit为单位
.wr_data_count (wr_data_count) // 32bit为单位
);

4.1.3 关键信号法:

利用 FIFO 的 wr_enrd_enfullempty 等信号精确控制。

4.2 原语法(参考zx)

原语中关于fifo的有很多,zx使用的是XPM这个,Xilinx Parameterized Macros是指的Xilinx 提供的参数化宏,例如 XPM_FIFO, XPM_MEMORY,可移植性和灵活性比直接用原语更好。推荐在新工程里使用,更抽象,跨器件可移植性好。

fifo

名称 文件名 作用 典型应用场景 比较
Asynchronous FIFO xpm_fifo_async 【异步】跨时钟域的 FIFO,写时钟和读时钟不一样,内部自动做好同步处理。 AFE 采集时钟 → FIFO → AXI 总线时钟 跨时钟域,解决“两个时钟不同步”的问题
AXI Memory Mapped FIFO (AXI Full) xpm_fifo_axif 带 AXI4 full 接口的 FIFO,支持突发读写。 DDR 控制器前的数据缓存、DMA 通道缓冲。 大吞吐量,用在 DDR/DMA 之前
AXI Memory Mapped FIFO (AXI Lite) xpm_fifo_axil 带 AXI4-Lite 接口的 FIFO,适合寄存器级访问,单次读写。 CPU 控制逻辑和 FPGA 内部数据缓冲之间的通信。 小容量、寄存器读写用
AXI Stream FIFO xpm_fifo_axis 带 AXI4-Stream 接口的 FIFO,适合数据流传输,支持 TREADY/TVAILD 握手。 视频流、采样数据流处理,或者模块间流式数据缓存。 流式数据管道之间传数据
Synchronous FIFO xpm_fifo_sync 【同步】单时钟域 FIFO,写时钟和读时钟相同。 纯缓存、对齐、延迟处理,比如 DSP 管道里的数据对齐。 单时钟域,最简单的缓存

五、EXAMPLE

六、一些经验

DDR4 写带宽要想接近理论值,必须保证:

  1. 对齐突发长度(一般 64bit 或 128bit)
  2. 持续突发写,减少命令间隙
  3. 写入总线宽度 ≥ 128bit

注意:

  • FIFO 深度设计为总线宽度的整数倍(比如 256bit 总线 → FIFO 先凑够 256bit 才写)

  • 通过 AXI4 Burst 模式连续写入 DDR(AWLEN 设为 16 或 32,表示一次突发 16 或 32 beat)

  • MIG/EMIF IP 自带仲裁、刷新管理,用户逻辑只需管好 AXI 接口即可。

  • 小 FIFO(几百到几千深):常用 LUTRAM 或 BRAM。

  • 中 FIFO(上万深):用 BRAM。

  • 大 FIFO(几十万深):用 URAM。

  • 一般不会把所有 BRAM/URAM 全部用光,因为还要给缓存、DMA、图像/信号处理等逻辑留空间。

  • 对 DDR 写通路的 FIFO,常见深度是几 KB 到几十 KB,用来攒齐突发写包。