(四)零基础学懂FIFO——最详细的FIFO IP核应用教程
0 致读者
此篇为专栏 《FPGA学习笔记》 的第四篇,记录我的学习FPGA的一些开发过程和心得感悟,刚接触FPGA的朋友们可以先去此专栏置顶 《FPGA零基础入门学习路线》来做最基础的扫盲。
本篇内容基于笔者实际开发过程和正点原子资料撰写,将会详细讲解此FPGA实验的全流程,诚挚地欢迎各位读者在评论区或者私信我交流!
FIFO(First In First Out,即先入先出),是一种数据缓存器, 用来实现数据先入先出的读写方式。在FPGA 或者 ASIC 中使用到的 FIFO 一般指的是对数据的存储具有先入先出特性的缓存器,常被用于多比特数据跨时钟域的转换、读写数据带宽不同步等场合。
本文的工程文件开源地址如下(基于ZYNQ7020,大家 clone 到本地就可以直接跑仿真,如果要上板请根据自己的开发板更改约束即可):
https://github.com/ChinaRyan666/FPGA-IP-FIFO
1 实验任务
本文的实验任务是使用 Vivado 生成一个异步 FIFO,并实现以下功能:当 FIFO 为空时,向 FIFO 中写入数据, 直至将 FIFO 写满后停止写操作;当 FIFO 为满时, 从 FIFO 中读出数据,直到 FIFO 被读空后停止读操作,以此向大家详细介绍一下 FIFO IP 核的使用方法。
2 FIFO IP核简介
FIFO 本质上是由 RAM 加读写控制逻辑构成的一种先进先出的数据缓冲器,其与普通存储器 RAM 的区别在于 FIFO 没有外部读写地址线,使用起来非常简单。
但 FIFO 只能顺序写入数据, 并按顺序读出数据,其数据地址由内部读写指针自动加 1 完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址,不过也正是因为这个特性,使得 FIFO 在使用时并不存在像 RAM 那样的读写冲突问题。
根据 FIFO 工作的时钟域,可以将 FIFO 分为同步 FIFO 和异步 FIFO。同步 FIFO 是指读时钟和写时钟为同一个时钟,在时钟沿来临时同时发生读写操作,常用于两边数据处理带宽不一致的临时缓冲。异步 FIFO 是指读写时钟不一致,读写时钟是互相独立的,一般用于数据信号跨时钟阈处理。
对于 FIFO 我们还需要了解一些常见参数:
-
FIFO 的宽度: FIFO 一次读写操作的数据位宽 N。
-
FIFO 的深度: FIFO 可以存储多少个宽度为 N 位的数据。
-
将空标志: almost_empty, FIFO 即将被读空。
-
空标志: empty, FIFO 已空时由 FIFO 的状态电路送出的一个信号,以阻止 FIFO 的读操作继续从 FIFO 中读出数据而造成无效数据的读出。
-
将满标志: almost_full, FIFO 即将被写满。
-
满标志: full, FIFO 已满时由 FIFO 的状态电路送出的一个信号,以阻止 FIFO 的写操作继续向 FIFO中写数据而造成溢出。
-
写时钟: 写 FIFO 时所遵循的时钟,在每个时钟的上升沿触发。
-
读时钟: 读 FIFO 时所遵循的时钟,在每个时钟的上升沿触发。
-
可配置满阈值: 影响可配置满信号于何时有效,其可配置范围一般为 3~写深度-3。
-
可配置满信号: prog_full,表示 FIFO 中存储的数据量达到可配置满阈值中配置的数值。
-
可配置空阈值: 影响可配置空信号于何时有效,其可配置范围一般为 2~读深度-3。
-
可配置空信号: prog_empty,表示 FIFO 中剩余的数据量已经减少到可配置空阈值中配置的数值。
这里还有两点需要大家注意:
- “almost_empty” 和 “almost_full” 这两个信号分别被看作 “empty” 和 “full” 的警告信号,他们相对于真正的空(empty)和满(full)都会提前一个时钟周期拉高。
- FIFO 中,先写入的数据被置于高位,后写入的数据被置于低位,由于其先入先出的特性,所以读出的数据也是高位在前,低位在后。这一点在读写数据位宽不对等时尤为重要,例如我们写数据位宽为 8,读数据位宽为 2,当写入的数据为 11000111 时,读出的数据依次为 11、 00、 01、 11,如下图所示:
读位宽大于写位宽时,原理是相同的,所以我们就不再赘述了,其示意图如下:
对于 FIFO 的基本知识先了解这些就足够了, 接下来我们看下在逻辑设计的时候,尤其是在 FPGA 设计中, FIFO 都有哪些实现方法:
- 根据需求自己设计并编写 FIFO 的控制逻辑,一般对 FIFO 的功能有特殊需求时,可以使用此种方法。
- 使用第三方提供的开源 IP 核,一般是以源码的形式提供的,能够快速应用到用户系统中,当对 FIFO 有特殊需求时,可以在此源码的基础上自行修改,以适应自己的设计需求。
- 调用官方开发软件中免费提供的 FIFO IP 核,我们可以通过软件中的图形化界面对 FIFO 的各项参数和结构进行配置,生成的 FIFO IP 核针对旗下的不同系列器件,还可以实现结构上的优化,且其可配置的功能足以实现大部分的设计需求,因此在实际应用中推荐大家使用 IP 核来实现 FIFO 设计。
接着我们就来看下在 Vivado 软件中用于实现 FIFO 设计的 FIFO Generato IP 核(FIFO 发生器) ,其信号框图如下图所示:
首先说明下,上图中黑色箭头表示此信号为必要信号;蓝色箭头表示此信号为可选信号;灰色箭头表示此信号为可选的边带信号。
从图中我们可以了解到,当被配置为同步 FIFO 时,只使用 wr_clk, 所有的输入输出信号都同步于 wr_clk 信号。而当被配置为异步 FIFO 时,写端口和读端口分别有独立的时钟,所有与写相关的信号都是同步于写时钟 wr_clk,所有与读相关的信号都是同步于读时钟 rd_clk。
这里我们对框图中的常用信号端口做一下讲解,其他很少用到的信号如果大家感兴趣的话也可以在课后打开 IP 核的数据手册进行学习,各常用端口的功能描述如下:
需要注意的是在使用 Vivado 的 FIFO Generato IP 核时,输入的读写时钟频率不能超过 500MHz。
3 程序设计
3.1 FIFO IP核讲解
3.1.1 FIFO IP核配置
首先我们创建一个名为 “ip_fifo” 的空白工程,然后点击 Vivado 软件左侧 “Flow Navigator” 栏中的 “IP Catalog” ,在弹出的 “IP Catalog” 窗口的搜索栏中输入 “fifo” 关键字后, 我们找到 “FIFO Generator” 如下图所示:
双击 “FIFO Generator” 后弹出 IP 核的配置界面,接着我们就可以对 BMG IP 核进行配置了, “Basic” 选项卡配置界面如下图所示。
最上面的 “Component Name” 一栏可以设置该 IP 元件的名称,这里我们保持默认命名,当然也可以命名为其它方便自己一眼看出其功能的名称。
接着目光回到“Basic” 选项卡上,我们重点关注 “lnterface Type(接口模式)” 和 “Fifo Implementation(FIFO 实现)” 中的内容:
“lnterface Type(接口模式)” : 有三种接口模式可选,分别为 Native(常规)接口、 AXI Memory Mapped(内存映射) 接口和 AXI Stream(流)接口。 其中 AXI Memory Mapped 接口一般用于与 PS 端进行数据交互; AXI Stream 接口一般应用于高速信号处理场景中,例如光口通信;通常情况下我们一般采用 Native 模式,所以本次实验我们选择 Native 模式。
“Fifo Implementation(FIFO 实现)” : 用于设置用什么资源来实现什么样的 FIFO。可配置用于实现 FIFO 的资源有四种,分别为 Block RAM(块 RAM)、 Distributed RAM(分布式 RAM)、 Shift Register(移位寄存器)和 Builtin FIFO(内置 FIFO),其中移位寄存器仅可用于实现同步 FIFO。可配置的 FIFO 类型有两类,分别为 Common Clocks(公共时钟,即同步 FIFO)和 Independent Clocks(独立时钟,即异步 FIFO)。资源与种类两两组合之下便有了七种不同的选择。需要说明的是 BRAM 和分布式 RAM 是创建 FIFO 时最常选用的存储资源类型,一般来说, FIFO 容量超过 1024 个字节就需要考虑使用 BRAM 了,没超过 1024 字节选择分布式 RAM。当然,如果芯片 BRAM 资源很富余的话,全部采用 BRAM 也是可以的,后两种基本用不到。本次实验我们选择 “Independent Clocks Block RAM” ,即使用 BRAM 资源来实现一个异步 FIFO。
接下来我们对 “Native Ports” 选项卡进行配置,如下图所示:
“Native Ports” 选项卡下,我们重点关注 “Read Mode(读取模式)” 、“Data Port Parameters(数据端口参数)” 和 “Initialization(初始化) ” 中的内容,含义如下:
“Read Mode(读取模式)” : 有 “Standard FIFO(标准 FIFO) ” 和 “First Word Fall Through(首字直通,简称 FWFT 模式,即预读模式)” 两种可选,需要注意的是标准模式的数据输出会比读使能延迟一拍,预读模式的数据与读使能同时输出,这里我们选择默认的标准模式。
“Data Port Parameters(数据端口参数)” : 用于设置 FIFO 的读写数据位宽和读写深度,其中写数据位宽可在 1~ 1024 内任意设置;写深度的可支持参数配置我们可以通过下拉来查看,这里我们设置为 256,需要注意的是,虽然我们设置的深度为 256,但实际深度只有 255;读数据位宽支持 1:8~8:1之间的偶数比,这里我们保持默认的 1: 1 比例,即读数据位宽为 8;读深度是我们设置完读写数据位宽和写深度后自动确定的,无需我们进行设置。
“Initialization(初始化)”,也就是设置复位相关的参数,详情如下:
- Reset Pin(复位脚): 选择是否引入复位信号,高电平有效。实际设计中,在 FPGA 配置完成后,读写操作开始前, FIFO 必须要进行复位操作,需要注意的是,在进行复位操作时,读写时钟必须是有效的。这里我们保持默认的勾选状态,即启用复位信号。
- Enable Reset Synchronization(启用复位同步): 用于设置异步 FIFO 时是否启用同步复位,需要注意的是官方文档中建议复位信号至少要保持三个时钟周期(以慢时钟为准)的有效,且在复位后至少要经过三十个时钟周期(以慢时钟为准)后,才能对 FIFO 进行写数据操作。这里我们保持默认的勾选状态,即启用同步复位。
- Enable Safety Circuit(启用安全电路): 用于设置 FIFO 是否向外输出 wr_rst_busy(写复位忙信号)和 rd_rst_busy(读复位忙信号),这两个信号皆是高电平表示处于复位状态,低电平表示空闲,我们可以通过这两个信号来判断 FIFO 是否复位完成,防止我们在复位完成前对 FIFO 进行读写操作而导致读写错误,所以我们保持默认的勾选状态,即启用安全电路。需要注意的是官方文档中建议当启用安全电路时,复位信号至少要保持八个时钟周期(以慢时钟为准)的有效,且在复位后至少要经过六十个时钟周期(以慢时钟为准)后, 才能对 FIFO 进行写数据操作。
- Reset Type(复位类型): 当选择使用非 Builtin FIFO 资源来实现同步 FIFO 时,可以选择复位类型为 Asynchronous Reset(异步复位)或 Synchronous Reset(同步复位), 使用异步 FIFO 模式时不需要考虑该配置。
- Full Flags Reset Value(满信号的重置值): 用于设置复位时三个满信号(满信号,将满信号,设置满信号)的状态是高电平还是低电平。这里我们保持默认设置 1 即可。
- Dout Reset Value(输出的数据重置值): 设置复位期间 FIFO 输出总线上的数据值,若未启用,则复位期间输出总线上的值时未知的。切记设置时此值的位宽不可超过读数据的位宽,这里我们保持默认的 0 即可。
接下来我们对 “Status Flags” 选项卡进行配置,如下图所示:
“Status Flags” 选项卡下,我们重点关注 “Optional Flags(可选标准)” 中的内容,含义如下:
“Optional Flags(可选标准)” : 可勾选是否输出 Almost Full Flag(将满信号)和 Almost Empty Flag(将空信号),两个信号皆为高有效。其中将满信号是在 FIFO 内部写数据个数 >=FIFO 深度 -1 之后的第一个写时钟上升沿置高,直到 FIFO 内部写数据个数 <FIFO 深度 -1 后的第一个写时钟上升沿拉低; 将空信号是在 FIFO 内部读数据个数 <=1 之后的第一个读时钟上升沿置高,直到 FIFO 内部读数据个数 >1 后的第一个读时钟上升沿拉低。这里我们选择输出这两个标志信号。
接下来我们对 “Data Counts” 选项卡进行配置,如下图所示:
“Status Flags” 选项卡下,我们重点关注 “More Accurate Data Counts(更精确的数据计数)” 中的内容,含义如下:
“More Accurate Data Counts(更精确的数据计数)” : 该功能只有选择使用块 RAM 或者分布式 RAM 来实现 FIFO 时,将读取模式设置为预读模式才可进行配置和使用。
最后一个是 “Summary” 选项卡,该界面显示了我们配置的存储器的类型,消耗的 BRAM 资源等信息,我们直接点击 “OK” 按钮完成 FIFO Generator IP 核的配置,如下图所示:
可以看到 “Summary” 选项卡中有一条警告时建议我们启用安全电路,我们在之前的配置中已经启用了,所以不用理会。之后的几步操作是生成 IP 核时的必要操作,在我之前的几篇 IP 核相关的博客中已经讲解过数次了,这里就不再赘述了。
3.1.2 时序图详解
无论是同步 FIFO 还是异步 FIFO,我们都需要注意写满和读空的情况,若写满时继续写入数据,则会导致有效数据的丢失;若读空时继续读数据,则会导致读出的是无效数据。下面我们通过一个典型的 FIFO 读写时序图来理解一下写满信号和读空信号。
可以看出,初始状态时 empty(读空)信号是拉高的,即此时 FIFO 为空,如果我们在此时发起读操作,那么我们读出的就会是未知的无效数据。当拉高 wr_en(写使能)信号后,开始向 FIFO 发出写操作,当 FIFO 中有了数据, empty 信号便会拉低,说明此时 FIFO 非空。接着同时发起了读操作和写操作,此时因为是同步 FIFO,读写速率相同,所以状态标志是没有发生变化的。
当只写不读时,因为 FIFO 中存在两个或两个以上的数据,所以 almost_empty(将空)信号也会被拉低,表示 FIFO 不处于将空状态。经过一段时间只写操作后, FIFO 就会逐渐趋于写满状态, 当 FIFO 只能再接受一次写入(不带读取)时, almost_full(将满)信号就会拉高。最后,在没有进行读操作的情况下,单独进行了一次写操作, full(写满)信号就会被拉高,说明此时的 FIFO 已经写满了,在发出读请求之前将无法再写入任何数据, 如果此时我们还在发起写操作,就会导致我们的有效数据因为无法写入FIFO 而丢失。所以大家在使用 FIFO 时一定要注意 FIFO 的空信号和满信号,防止发生读无效和写丢失的现象。
除此之外,大家还需要注意下 FIFO 的复位信号,其时序图如下所示:
由上图可总结出以下几点注意事项:
- 建议在安全电路下的异步 FIFO 的复位信号(RST)至少要保持八个时钟周期(以慢时钟为准)的有效。
- 在 RST 上升沿时期, 经过 7 个 WR_CLK(写时钟) 周期后 WR_RST_BUSY(写复位忙) 信号拉高,FULL(满) 信号拉高,此时的 WR_EN(写使能) 信号应该拉低。
- 在 RST 上升沿时期, 经过 7 个 RD_CLK(读时钟) 周期后 RD_RST_BUSY(读复位忙) 信号拉高,EMPTY(空) 信号拉高,此时的 RD_EN(读使能) 信号应该拉低,而且此时的数据输出端口均为无效。
- 图中建议我们至少在复位结束后,经过 60 个时钟周期(以慢时钟为准)后再对 FIFO 进行写操作,在实际应用中我们以读写复位的忙信号来判断是否对 FIFO 进行写操作也是可以的。
所以大家在对 FIFO 进行复位时,一定要注意避免在复位结束时就立马对 FIFO 进行写操作,因为此时的 FIFO 仍然处于复位状态。
接着我们再来看下写操作的时序图,一个典型的写操作时序如下图所示:
当 wr_en(写使能)信号使能时,会在 wr_clk(写时钟) 的下一个上升沿上发生写操作,由于 FIFO 未满,因此 wr_ack(写应答)信号处于有效状态,表示写入操作成功。当只能再执行一次写操作时, almost_full(将满)信号会被拉高,此时若再进行一次写操作, full(满)信号就会被拉高,表示 FIFO 已被写满,在有数据被读出前,无法再写入数据了。如果在 full 信号拉高后执意要进行写操作, wr_ack 就会被拉低,表示此次数据写入失败,同时 overflow(满溢出)信号就会被拉高,表示 FIFO 存在溢出现象。
最后我们再来看下读操作的时序图,一个典型的读操作时序如下图所示:
只要 FIFO 中存有数据, empty(空)信号就会一直为低电平,表明 FIFO 中有数据可以进行读取。当 rd_en(读使能)信号使能时,会在 rd_clk(读时钟) 的下一个上升沿上发生读操作, FIFO 会在 dout(数据输出线) 上输出数据,并拉高 valid(读有效)信号,表示读操作成功。
当 FIFO 中还剩最后一个数据时,almost_empty(将空)信号会被拉高,此时若再进行一次读操作, empty(空)信号就会被拉高,表示 FIFO 已被读空,在 FIFO 中有存储数据前,读请求将被忽视。如果在 empty 信号拉高后执意要进行读操作, valid 信号就会被拉低,表示此次数据读出失败,同时 underflow(空溢出)信号就会被拉高,表示 FIFO 中已经没有可被读取的数据了。
3.2 顶层模块设计
本次实验的目的是为了将 Xilinx FIFO Generato IP 核配置成一个异步 FIFO 并对其进行读写操作,因此可以给模块命名为 ip_fifo;因为我们做的是异步 FIFO,所以我们需要一个 PLL IP 核来输出 50MHz 的写时钟和 100MHz 的读时钟,当然输出其它频率的时钟也是可以的;然后我们还需要一个写模块( fifo_wr)和一个读模块(fifo_rd),写模块通过 FIFO 的状态来判断是否给出写请求信号和写数据,读模块通过 FIFO 的状态来判断是否给出读请求信号,并接收从 FIFO 中读出的数据;系统时钟和系统复位是一个完整的工程中必不可少的输入端口信号,这里就不再赘述。
经过上述分析我们可以画出一个大致的模块框图,如下图所示:
模块端口与功能描述如下图所示:
3.2.1 代码编写
因为本次实验是使用 FIFO Generator IP 核来生成一个异步 FIFO,所以我们需要使用到 PLL IP 核来输出两路不同频率的时钟,除此之外我们还需要一个**读模块(fifo_rd)和一个写模块(fifo_wr)**来进行异步的读写操作,所以我们需要创建一个顶层模块来例化两个 IP 核与读/写模块, 这里我们将顶层模块命名为 ip_fifo, 代码如下:
module ip_fifo(
input sys_clk , // 系统时钟信号
input sys_rst_n // 系统复位信号
);
//wire define
wire clk_50m ; // 50M时钟
wire clk_100m ; // 100M时钟
wire locked ; // 时钟锁定信号
wire rst_n ; // 复位,低有效
wire wr_rst_busy ; // 写复位忙信号
wire rd_rst_busy ; // 读复位忙信号
wire fifo_wr_en ; // FIFO写使能信号
wire fifo_rd_en ; // FIFO读使能信号
wire [7:0] fifo_wr_data ; // 写入到FIFO的数据
wire [7:0] fifo_rd_data ; // 从FIFO读出的数据
wire almost_full ; // FIFO将满信号
wire almost_empty ; // FIFO将空信号
wire full ; // FIFO满信号
wire empty ; // FIFO空信号
wire [7:0] wr_data_count ; // FIFO写时钟域的数据计数
wire [7:0] rd_data_count ; // FIFO读时钟域的数据计数
//*****************************************************
//** main code
//*****************************************************
//通过系统复位信号和时钟锁定信号来产生一个新的复位信号
assign rst_n = sys_rst_n & locked;
//例化PLL IP核
clk_wiz_0 clk_wiz_0 (
// Clock out ports
.clk_out1(clk_50m ), // output clk_out1
.clk_out2(clk_100m), // output clk_out2
// Status and control signals
.locked (locked ), // output locked
// Clock in ports
.clk_in1 (sys_clk ) // input clk_in1
);
//例化FIFO IP核
fifo_generator_0 fifo_generator_0 (
.rst (~rst_n ), // input wire rst
.wr_clk (clk_50m ), // input wire wr_clk
.rd_clk (clk_100m ), // input wire rd_clk
.wr_en (fifo_wr_en ), // input wire wr_en
.rd_en (fifo_rd_en ), // input wire rd_en
.din (fifo_wr_data ), // input wire [7 : 0] din
.dout (fifo_rd_data ), // output wire [7 : 0] dout
.almost_full (almost_full ), // output wire almost_full
.almost_empty (almost_empty ), // output wire almost_empty
.full (full ), // output wire full
.empty (empty ), // output wire empty
.wr_data_count (wr_data_count), // output wire [7 : 0] wr_data_count
.rd_data_count (rd_data_count), // output wire [7 : 0] rd_data_count
.wr_rst_busy (wr_rst_busy ), // output wire wr_rst_busy
.rd_rst_busy (rd_rst_busy ) // output wire rd_rst_busy
);
//例化写FIFO模块
fifo_wr u_fifo_wr (
.wr_clk (clk_50m ), // 写时钟
.rst_n (rst_n ), // 复位信号
.wr_rst_busy (wr_rst_busy ), // 写复位忙信号
.fifo_wr_en (fifo_wr_en ), // fifo写请求
.fifo_wr_data (fifo_wr_data), // 写入FIFO的数据
.empty (empty ), // fifo空信号
.almost_full (almost_full ) // fifo将满信号
);
//例化读FIFO模块
fifo_rd u_fifo_rd (
.rd_clk (clk_100m ), // 读时钟
.rst_n (rst_n ), // 复位信号
.rd_rst_busy (rd_rst_busy ), // 读复位忙信号
.fifo_rd_en (fifo_rd_en ), // fifo读请求
.fifo_rd_data (fifo_rd_data), // 从FIFO输出的数据
.almost_empty (almost_empty), // fifo将空信号
.full (full ) // fifo满信号
);
//写时钟域下ila
ila_0 u_ila_wr (
.clk (clk_50m ), // input wire clk
.probe0 (fifo_wr_en ), // input wire [0:0] probe0
.probe1 (fifo_wr_data ), // input wire [7:0] probe1
.probe2 (almost_full ), // input wire [0:0] probe2
.probe3 (full ), // input wire [0:0] probe3
.probe4 (wr_data_count) // input wire [7:0] probe4
);
//读时钟域下ila
ila_1 u_ila_rd (
.clk (clk_100m ), // input wire clk
.probe0 (fifo_rd_en ), // input wire [0:0] probe0
.probe1 (fifo_rd_data ), // input wire [7:0] probe1
.probe2 (almost_empty ), // input wire [0:0] probe2
.probe3 (empty ), // input wire [0:0] probe3
.probe4 (rd_data_count) // input wire [7:0] probe4
);
endmodule
可以看出 ip_fifo 顶层模块只是例化了 FIFO IP 核(fifo_generator_0)、 PLL IP 核(clk_wiz_0)、读模块(fifo_rd)和写模块(fifo_wr),其中写模块负责产生 FIFO IP 核写操作所需的所有数据、写请求等信号;读模块负责产生 FIFO IP 核读操作所需读请求信号,并将读出的数据也连接至读模块。
因为读写模块的时钟皆来自 PLL IP 核,而 PLL IP 核需要一定的时间才能输出稳定的时钟,所以在第 29 行代码中我们通过系统复位和时钟锁定来产生一个信号复位信号,使读/写模块及 FIFO IP 核在时钟稳定后才进入工作状态。
3.3 FIFO 写模块设计
首先介绍下 FIFO 写模块的设计, 在 FIFO 写模块中, 我们的输入信号主要有系统时钟信号(写时钟域的时钟)、系统复位信号;因为 FIFO 的写操作需要在 FIFO 完成复位后进行,所以我们还需要输入 wr_rst_busy(写复位忙) 信号来判断 FIFO 是否结束了复位状态;实验任务中我们提到了 FIFO 为空时进行写操作,因此还需要引入一个空相关的信号,这里我们引入的是 empty(空)信号。
实验任务中我们还提到了写满了要停止写操作,所以这里我们引入了 almost_full(将满)信号,因为将满信号表示 FIFO 还能再进行最后一次写操作,使用这个信号的话我们正好可以在写入最后一次数据后关闭写使能,当然引入 full(满)信号也是可以,区别只是在于这么做会在写使能关断前执行一次无效的写操作。
输出信号有控制写 FIFO 所需的 fifo_wr_en(写端口使能) 和 **fifo_wr_data(写数据)**这两个信号。由上述分析我们绘制出如下图所示的模块框图:
模块端口与功能描述如下图所示:
3.3.1 绘制波形图
在编写代码前,我们先大致梳理一下模块的端口时序。 因为 empty(空) 是读时钟域下的信号,所以我们需要对其进行打两拍的操作,将其同步到写时钟域下,当检测到打拍后的 empty 信号为高时,说明 FIFO 为空, 此时我们可以开启写使能并向 FIFO 中写入数据,直到检测到 almost_full(将满)信号拉高时,将写使能关闭。
综上,我们绘制出如下所示的波形图:
由上图可知,当系统复位结束后, FIFO 还处于复位状态,我们需要等待 FIFO 写复位结束(即 wr_rst_busy 信号拉低)后,再对 FIFO 进行写操作。这里说明一下,因为 empty(空)信号是 FIFO 读时钟域的输出信号,对于写操作来说属于异步信号,所以这里我们通过打拍的方式将 empty 信号同步到写时钟域下。需要注意的是,当 FIFO 中有两个或两个以上的数据时,将空信号就会被拉低,但是因为是异步FIFO,所以状态信号的同步需要一定的时间,因此将空信号并不是在写入第二个数据后拉低的。
3.3.2 代码编写
fifo_wr 模块用于产生 FIFO 写操作所需的信号,其代码如下所示:
module fifo_wr(
//mudule clock
input wr_clk , // 时钟信号
input rst_n , // 复位信号
//FIFO interface
input wr_rst_busy , // 写复位忙信号
input empty , // FIFO空信号
input almost_full , // FIFO将满信号
output reg fifo_wr_en , // FIFO写使能
output reg [7:0] fifo_wr_data // 写入FIFO的数据
);
//reg define
reg empty_d0;
reg empty_d1;
//*****************************************************
//** main code
//*****************************************************
//因为empty信号是属于FIFO读时钟域的
//所以对empty打两拍同步到写时钟域下
always @(posedge wr_clk or negedge rst_n) begin
if(!rst_n) begin
empty_d0 <= 1'b0;
empty_d1 <= 1'b0;
end
else begin
empty_d0 <= empty;
empty_d1 <= empty_d0;
end
end
//对fifo_wr_en赋值,当FIFO为空时开始写入,写满后停止写
always @(posedge wr_clk or negedge rst_n) begin
if(!rst_n)
fifo_wr_en <= 1'b0;
else if(!wr_rst_busy) begin
if(empty_d1)
fifo_wr_en <= 1'b1;
else if(almost_full)
fifo_wr_en <= 1'b0;
end
else
fifo_wr_en <= 1'b0;
end
//对fifo_wr_data赋值,0~254
always @(posedge wr_clk or negedge rst_n) begin
if(!rst_n)
fifo_wr_data <= 8'b0;
else if(fifo_wr_en && fifo_wr_data < 8'd254)
fifo_wr_data <= fifo_wr_data + 8'b1;
else
fifo_wr_data <= 8'b0;
end
endmodule
因为我们设置的 FIFO 只能存入 255 个数据,而 0~ 254 就是 255 个数据,所以在代码的第 52 行中,我们设置了 FIFO 写使能时写数据的累加上限,即写数据的值只会在 0~254 之间循环计数。
3.4 FIFO 读模块设计
首先介绍下 FIFO 读模块的设计,在 FIFO 读模块中,我们的输入信号主要有系统时钟信号(读时钟域时钟) 和系统复位信号;因为 FIFO 的读操作需要在 FIFO 完成复位后进行,所以我们还需要输入 rd_rst_busy(读复位忙) 信号来判断 FIFO 是否结束了复位状态;实验任务中我们提到了 FIFO 为满时进行读操作,因此还需要引入一个满相关的信号,这里我们引入的是 full(满)信号。
实验任务中我们还提到了读空了要停止读操作,所以这里我们引入了 almost_empty(将空)信号,因为将空信号表示 FIFO 还能再进行最后一次读操作,使用这个信号的话我们正好可以在读出最后一个数据后关闭读使能,当然引入 empty(空)信号也是可以,区别只是在于这么做会在读使能关断前执行一次无效的读操作。
输出信号仅有控制写 FIFO 所需的 fifo_rd_en(读端口使能)信号。由上述分析我们绘制出如下图所示的模块框图:
模块端口与功能描述如下图所示:
3.4.1 绘制波形图
在编写代码前,我们先大致梳理一下模块的端口时序。 因为 full(满)信号是写时钟域下的信号,所以我们需要对其进行打两拍的操作,将其同步到读时钟域下,当检测到打拍后的 full 信号为高时,说明 FIFO 为满,此时我们可以开启读使能并读出 FIFO 中存储的数据,直到检测到 almost_empty(将空)信号拉高时,将写使能关闭。综上,我们绘制出如下所示的波形图:
由上图可知,当系统复位结束后, FIFO 还处于复位状态,我们需要等待 FIFO 读复位结束(即 wr_rst_busy 信号拉低)后,再对 FIFO 进行读操作。这里说明一下,因为 full(满)信号是 FIFO 写时钟域的输出信号,对于写操作来说属于异步信号,所以这里我们通过打拍的方式将 full 信号同步到写时钟域下。
需要注意的是,当 FIFO 中的数据量小于写深度 -1 时,将满信号就会被拉低,但是因为是异步 FIFO,所以状态信号的同步需要一定的时间,因此将空信号并不是在读出第二个数据后拉低的。
3.4.2 编写代码
fifo_rd 模块用于产生 FIFO 读操作所需的信号,其代码如下所示:
module fifo_rd(
//system clock
input rd_clk , //时钟信号
input rst_n , //复位信号
//FIFO interface
input rd_rst_busy , //读复位忙信号
input [7:0] fifo_rd_data, //从FIFO读出的数据
input full , //FIFO满信号
input almost_empty, //FIFO将空信号
output reg fifo_rd_en //FIFO读使能
);
//reg define
reg full_d0;
reg full_d1;
//*****************************************************
//** main code
//*****************************************************
//因为full信号是属于FIFO写时钟域的
//所以对full打两拍同步到读时钟域下
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n) begin
full_d0 <= 1'b0;
full_d1 <= 1'b0;
end
else begin
full_d0 <= full;
full_d1 <= full_d0;
end
end
//对fifo_rd_en进行赋值,FIFO写满之后开始读,读空之后停止读
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n)
fifo_rd_en <= 1'b0;
else if(!rd_rst_busy) begin
if(full_d1)
fifo_rd_en <= 1'b1;
else if(almost_empty)
fifo_rd_en <= 1'b0;
end
else
fifo_rd_en <= 1'b0;
end
endmodule
4 仿真验证
4.1 编写 TestBench
我们接下来先对代码进行仿真,因为本章实验我们只有系统时钟和系统复位这两个输入信号,所以仿真文件也只需要编写这两个信号的激励即可, TestBench 代码如下:
`timescale 1ns / 1ps //仿真单位/仿真精度
module tb_ip_fifo();
//parameter define
parameter CLK_PERIOD = 20; //时钟周期 20ns
//reg define
reg sys_clk;
reg sys_rst_n;
//信号初始化
initial begin
sys_clk = 1'b0;
sys_rst_n = 1'b0;
#200
sys_rst_n = 1'b1;
//模拟按下复位
#10000 ;
sys_rst_n = 0;
#160 ;
sys_rst_n = 1;
end
//产生时钟
always #(CLK_PERIOD/2) sys_clk = ~sys_clk;
ip_fifo u_ip_fifo (
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n)
);
endmodule
本次实验我们模拟了一次复位状态,因为复位信号至少要保持 8 个时钟周期(以慢时钟为准)的有效,而我们的慢时钟的频率为 50MHz,所以复位状态至少要保持 20ns,如代码第 20~22 行所示。
4.2 代码仿真
通过仿真我们得到了以下波形图:
可以看出 FIFO 是在读空时进行写操作,在写满时进行读操作,当复位信号来临时, FIFO 内的数据被清空,各信号恢复到初始状态。
接着我们展开波形图看一下各个状态的细节,首先是复位状态,其波形如下图所示:
可以看出当复位信号来临后经过一定的读/写时钟周期后, FIFO 进入复位状态,即读写复位忙标志拉高,同时数据及标志信号被重置到我们设置的初始状态。例如空满等标志信号在复位期间就一直处于我们设置的高电平状态。
写状态的波形如下所示:
由写状态仿真图可以看出写使能拉高后开始向 FIFO 中写入数据,当写完倒数第二个数时,将满信号被拉高,当写完最后一个数时,满信号拉高,写使能被拉低。另外,从图中我们可以发现 FIFO 写时钟域的数据计数器(wr_data_count)在写操作前并不是以 1 为步进递减的,这是 FIFO 的读写速率不同导致的。
而且 wr_data_count 也并没有递减到 0,这是因为读时钟域与写时钟域下的信号存在数个时钟周期的同步延迟,而我们通过检测空信号的方式在其延迟更新前就开启了写使能,所以我们没有看到 wr_data_count 更新到 0 的过程。
读状态的波形如下所示:
由读状态仿真图可以看出读使能拉高后开始从 FIFO 中读出数据,当读完倒数第二个数时,将空信号被拉高,当读完最后一个数时,空信号拉高,读使能被拉低。
FIFO 读时钟域的数据计数器(rd_data_count) 的变化与 wr_data_count 类似,这里就不再赘述了。
5 下载验证
5.1 引脚约束
在仿真验证完成后,接下来对引脚进行分配,并上板验证。 本实验中,系统时钟、按键复位的管脚分配如下图所示:
对应的 XDC 约束语句如下所示:
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]
Vivado 软件中 IO Planning 界面如下图所示:
5.2 添加 ila IP 核进行在线调试
接下来添加 ILA IP 核,将我们想要观察的信号添加至观察列表中,例如读写数据,空满信号等,添加 ILA IP 核的方法这里不再赘述,需要注意的是因为读写是在不同的时钟域下进行的,所以我们需要两个 ila 来分别观察读时钟域下的信号和写时钟域下信号,本例程是将 ILA 例化在了顶层模块(ip_fifo)中,例化代码如下所示:
//写时钟域下ila
ila_0 u_ila_wr (
.clk (clk_50m ), // input wire clk
.probe0 (fifo_wr_en ), // input wire [0:0] probe0
.probe1 (fifo_wr_data ), // input wire [7:0] probe1
.probe2 (almost_full ), // input wire [0:0] probe2
.probe3 (full ), // input wire [0:0] probe3
.probe4 (wr_data_count) // input wire [7:0] probe4
);
//读时钟域下ila
ila_1 u_ila_rd (
.clk (clk_100m ), // input wire clk
.probe0 (fifo_rd_en ), // input wire [0:0] probe0
.probe1 (fifo_rd_data ), // input wire [7:0] probe1
.probe2 (almost_empty ), // input wire [0:0] probe2
.probe3 (empty ), // input wire [0:0] probe3
.probe4 (rd_data_count) // input wire [7:0] probe4
);
5.3 上板验证
编译工程并生成比特流文件后,将下载器一端连接电脑,另一端与开发板上的 JTAG 下载口连接,连接电源线,并打开开发板的电源开关。
点击 Vivado 左侧 “Flow Navigator” 窗口最下面的 “Open Hardware Manager” ,如果此时 Vivado 软件识别到了下载器,则点击 “Hardware” 窗口中 “Progam Device” ,在弹出的界面中选择 “Program” 下载程序。
FIFO 写操作在 ILA 中观察到的波形如下图所示:
由上图可知,写使能拉高后,开始向 FIFO 中写入数据,当还能进行最后一次写操作时,将满信号(almost_full)拉高,当写入最后一个数据后,满信号(fifo_full)拉高,此时写使能关闭,等待读使能打开。
FIFO 读操作在 ILA 中观察到的波形如下图所示:
由上图可知,读使能拉高后,开始从 FIFO 中读出数据,当读出倒数第二个数后,将空信号(almost_empty)拉高,当读出最后一个数据后,空信号(fifo_empty)拉高,此时读使能关闭,等待写使能打开。 从图中我们也可以看出读使能拉高后,读数据会延后一拍输出,和我们的设置相符。
经过验证可以发现 ILA 波形符合我们设计,即 FIFO 读空了写,写满了读。至此 IP 核之 FIFO 读写实验验证成功!
6 总结
本博客我们详细讲解了 FIFO 的概念和使用场景,并介绍了 Vivado 软件中如何对 FIFO Generator IP 核 进行配置和调用(例化)。希望对您有所帮助,有兴趣的朋友可以进一步联系我交流。
微博:沂舟Ryan (@沂舟Ryan 的个人主页 - 微博 )
GitHub:ChinaRyan666
微信公众号:沂舟无限进步
如果对您有帮助的话请点赞支持下吧!文章来源:https://www.toymoban.com/news/detail-768856.html
集中一点,登峰造极。文章来源地址https://www.toymoban.com/news/detail-768856.html
到了这里,关于(四)零基础学懂FIFO——最详细的FIFO IP核应用教程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!