目录
Vivado 下 IP核之FIFO 实验
1、FIFO IP 核简介
2、实验任务
3、程序设计
3.1、FIFO IP 核配置
3.1.1、“Basic” 选项卡下各参数配置
3.1.2、“Native Ports”选项卡下各参数配置
3.1.3、“Status Flags” 选项卡下各参数配置
3.1.4、“Data Counts(数据计数)”选项卡下各参数配置
3.2、时序图讲解
3.3、顶层模块设计
3.3.1、顶层模块 ip_fifo.v 代码
3.4、FIFO 写模块设计
3.4.1、绘制波形图
3.4.2、fifo_wr 模块代码
3.5、FIFO 读模块设计
3.5.1、绘制波形图
3.5.2、fifo_rd 模块代码
4、仿真验证
4.1、编写 TB 文件
4.2、仿真验证
5、下载验证
5.1、引脚约束--约束文件 ip_fifo.xdc
5.2、添加 ILA IP 核进行在线调试(调试完成后,ILA代码需删除)
6、本章总结
FIFO(First In First Out,即先进先出),是一种数据缓存器,用来实现数据先进先出的读写方式。在FPGA 或者 ASIC 中使用到的 FIFO 一般指的是对数据的存储具有先进先出特性的缓存器,常被用于数据的缓存、多比特数据跨时钟域的转换、读写数据带宽不同步等场合,或者高速异步数据的交互也即所谓的跨时钟域信号传递。 它与 FPGA 内部的 RAM 和 ROM 的区别是没有外部读写地址线,采取顺序写入数据,顺序读出数据的方式, 使用起来简单方便,由此带来的缺点就是不能像 RAM 和 ROM 那样可以由地址线决定读取或写入某个指定 的地址。本节将对 Vivado 软件生成的 FIFO IP 核进行读写测试,来向大家介绍 Xilinx FIFO IP 核的使用 方法。
FIFO 本质上是由 RAM 加读写控制逻辑构成的一种先进先出的数据缓冲器,其与普通存储器 RAM 的 区别在于 FIFO 没有外部读写地址线,使用起来非常简单,但 FIFO 只能顺序写入数据,并按顺序读出数据,其数据地址由内部读写指针自动加 1 完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址,不过也正是因为这个特性,使得 FIFO 在使用时并不存在像 RAM 那样的读写冲突问题。
根据 FIFO 工作的时钟域,可以将 FIFO 分为同步 FIFO 和异步 FIFO。同步 FIFO 是指读时钟和写时钟为同一个时钟,在时钟沿来临时同时发生读写操作,常用于两边数据处理带宽不一致的临时缓冲。异步 FIFO是指读写时钟不一致,读写时钟是互相独立的,一般用于数据信号跨时钟阈处理。
从上图中可以看到,单时钟 FIFO 具有一个独立的时钟端口 clock,因此所有的输入输出信号都同步于 clock 信号。而在双时钟FIFO 结构中,写端口和读端口分别有独立的时钟,所有与写相关的信号都是同步于写时钟 wrclk,所有与读相关的信号都是同步于读时钟 rdclk。在双时钟 FIFO 的符号图中,位于图中上侧部分的以“data”和 “wr”开头的信号为与写相关的所有信号,位于中间部分的“q”和以“rd”开头的信号为与读相关的所有信号,位于底部的为异步清零信号。
对于 FIFO 我们还需要了解一些常见参数:
这里还有两点需要大家注意:
1、“almost_empty” 和 “almost_full” 这两个信号分别被看作 “empty” 和 “full” 的警告信号,他们相对于真正的空(empty)和满(full)都会提前一个时钟周期拉高。
2、FIFO 中,先写入的数据被置于高位,后写入的数据被置于低位,由于其先入先出的特性,所以读出的数据也是高位在前,低位在后。这一点在读写数据位宽不对等时尤为重要,例如我们写数据为 8,读数据位宽为 2,当写入的数据为 11000111 时,读出的数据依次为 11、00、01、11,如下图所示:
读位宽大于写位宽时,原理是相同的,其示意图如下:
在逻辑设计的时候,尤其是在 FPGA 设计中,FIFO 都有哪些实现方法:
Xilinx 的 FIFO IP 核可以被配置为同步 FIFO 或异步 FIFO,其信号框图如下图所示。从图中可以了解到,当被配置为同步 FIFO 时,只使用 wr_clk,所有的输入输出信号都同步于 wr_clk 信号。而当被配置为异步 FIFO时,写端口和读端口分别有独立的时钟,所有与写相关的信号都是同步于写时钟 wr_clk,所有与读相关的信号都是同步于读时钟 rd_clk。
首先说明下,上图中黑色箭头表示此信号为必要信号;蓝色箭头表示此信号为可选信号;灰色箭头表示此信号为可选的边带信号。
从图中我们可以了解到,当被配置为同步 FIFO 时,只使用 wr_clk,所有的输入输出信号都同步于 wr_clk 信号。而当被配置为异步 FIFO 时,写端口和读端口分别有独立的时钟,所有与写相关的信号都是同步于写时钟 wr_clk,所有与读相关的信号都是同步于读时钟 rd_clk。
这里我们对框图中的常用信号端口做一下讲解,其他很少用到的信号如果大家感兴趣的话也可以在课 后打开 IP 核的数据手册进行学习,各常用端口的功能描述如下:
需要注意的是在使用 Vivado 的 FIFO Generato IP 核时,输入的读写时钟频率不能超过 500MHz。
本节的实验任务是使用 Vivado 生成一个异步 FIFO,并实现以下功能:当 FIFO 为空时,向 FIFO 中写入数据,直至将 FIFO 写满后停止写操作;当 FIFO 为满时,从 FIFO 中读出数据,直到 FIFO 被读空后停止读操作,以此向大家详细介绍一下 FIFO IP 核的使用方法。
根据实验任务要求和模块化设计的思想,我们需要如下 4 个模块:fifo IP 核、写 fifo 模块、读 fifo 模块以及顶层例化模块实现前三个模块的信号交互。由于 FIFO 多用于跨时钟域信号的处理,所以本实验我们使用异步 FIFO 来向大家详细介绍双时钟 FIFO IP 核的创建和使用。为了方便大家理解,这里我们将读/写时钟都用系统时钟来驱动。系统的功能框图如下图所示:
首先我们创建一个名为 “ip_fifo” 的空白工程,然后点击 Vivado 软件左侧 “Flow Navigator” 栏中的 “IP Catalog”,在弹出的 “IP Catalog” 窗口的搜索栏中输入“fifo”关键字后,我们找到 “FIFO Generator” 如下图所示:
双击 “FIFO Generator” 后弹出 IP 核的配置界面,接着我们就可以对 BMG IP 核进行配置了, “Basic” 选项卡配置界面如下图所示。
最上面的“Component Name”一栏可以设置该 IP 元件的名称,这里我们保持默认命名,当然也可以命名为其它方便自己一眼看出其功能的名称。
接着目光回到 “Basic” 选项卡上,该选项卡下各参数(我们重点关注(1)和(2)中的内容)含义如下:
(1)、“lnterface Type(接口模式)”:有三种接口模式可选,分别为 Native(常规)接口、AXI Memory Mapped(内存映射)接口和 AXI Stream(流)接口。其中 AXI Memory Mapped 接口一般用于与PS 端进行数据交互;AXI Stream 接口一般应用于高速信号处理场景中,例如光口通信;通常情况下我们一般采用 Native 模式,所以本次实验我们选择 Native 模式。
(2)、“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。
(3)、“synchronization Stages(同步阶段)”:定义跨交叉时钟域逻辑的同步器级数,即设置读写状态信号的输出延迟。保持默认设置 2 即可。
(4)、“FIFO Implementation Options(FIFO 实现方案)”:此处的表格将实现 FIFO 的七种方案的特征都一一列出了,当我们不清楚自己的 FIFO 设计该使用哪种方案实现时,可以看下此处的表格。
接下来我们对 “Native Ports” 选项卡进行配置,如下图所示:
“Native Ports”选项卡下各参数(我们重点关注(1)、(2)和(4)中的内容)含义如下: 、(1)、“Read Mode(读取模式)”:有 “Standard FIFO(标准 FIFO)” 和 “First Word Fall Through (首字直通,简称 FWFT 模式,即预读模式)” 两种可选,需要注意的是标准模式的数据输出会比读使能延迟一拍,预读模式的数据与读使能同时输出,这里我们选择默认的标准模式。
(2)、“Data Port Parameters(数据端口参数)”:用于设置 FIFO 的读写数据位宽和读写深度,其中写数据位宽可在 1~1024 内任意设置;写深度的可支持参数配置我们可以通过下拉来查看,这里我们设置为 256,需要注意的是,虽然我们设置的深度为 256,但实际深度只有 255;读数据位宽支持 1:8~8:1 之间的偶数比,这里我们保持默认的 1:1 比例,即读数据位宽为 8;读深度是我们设置完读写数据位宽和写深度后自动确定的,无需我们进行设置。
这里有一点需要我们注意,在实际应用中,FIFO 的读写数据位宽和深度在满足设计需求的情况下要尽量设置的小一点,因为 FIFO 使用的是片上 BRAM 资源,而 FPGA 内部的片上 BRAM 资源是有限的,所以大家不要将位宽和深度设置成远远超过实际需求的值,造成 BRAM 资源的过度浪费。
(3)、“ECC,Output Register and Power Gating Options(ECC、输出寄存器和电源选通选项)”, 其下各配置如下:
(4)、“Initialization(初始化)”,也就是设置复位相关的参数,详情如下:
(5)、“Read Latency(读延迟)”,可以在此处看出经过以上设置后,输出被延迟了几拍。因为我们选择的读取模式是标准模式,且没有启用任何输出寄存器,所以输出延迟了一拍。
接下来我们对“Status Flags”选项卡进行配置,如下图所示:
(5)、“Read Latency(读延迟)”,可以在此处看出经过以上设置后,输出被延迟了几拍。因为我们选择的读取模式是标准模式,且没有启用任何输出寄存器,所以输出延迟了一拍。
接下来我们对 “Status Flags” 选项卡进行配置,如下图所示:
“Status Flags”选项卡下各参数(我们重点关注(1)中的内容)含义如下:
(1)、“Optional Flags(可选标准)”,可勾选是否输出 Almost Full Flag(将满信号)和 Almost Empty Flag(将空信号),两个信号皆为高有效。其中将满信号是在 FIFO 内部写数据个数>=FIFO 深度-1 之后的第一个写时钟上升沿置高,直到 FIFO 内部写数据个数
(2)、“Handshaking Options(握手选项)”,可用于配置读写端口的握手机制,这里我们简单的介绍下各个配置含义:
(3)、“Programmable Flags(可编程标志)”有六个可配置参数,这里的参数配置将影响到可编程空满信号(prog_empty 和 prog_full 信号)在何时使能,各参数含义如下:
接下来我们对 “Data Counts” 选项卡进行配置,如下图所示:
“Data Counts(数据计数)”选项卡下各参数(我们重点关注(1)中的内容)含义如下:
(1)、“More Accurate Data Counts(更精确的数据计数)”,该功能只有选择使用块 RAM 或者分布式 RAM 来实现 FIFO 时,将读取模式设置为预读模式才可进行配置和使用。
(2)、“Data Counts(数据计数)”,当使用非 Builtin FIFO 资源来实现同步 FIFO 时可进行设置,可用于跟踪 FIFO 中的字数(数据个数),我们可以指定其计数总线的宽度,最小为 1,最大宽度为 log2(FIFO 深度),如果指定的宽度小于最大宽度,则低位会被截断。例如数据深度为 16,则 Data Counts 的最大位宽为 4,如果我们设置为 3,那么 FIFO 中的数据量=2 时,Data Counts 才会加 1。
(3)、“Write Data Count(写数据计数)”,与写时钟同步。当使用非 Builtin FIFO 资源来实现异步FIFO 时可进行设置,可用于跟踪写 FIFO 中的字数(数据个数),我们可以指定其计数总线的宽度,最小 为 1,最大宽度为 log2(写 FIFO 深度),如果指定的宽度小于最大宽度,则低位会被截断。
(4)、“Read Data Count(读数据计数)”,与读时钟同步。当使用非 Builtin FIFO 资源来实现异步FIFO 时可进行设置,可用于跟踪读 FIFO 中的字数(数据个数),我们可以指定其计数总线的宽度,最小为 1,最大宽度为 log2(读 FIFO 深度),如果指定的宽度小于最大宽度,则低位会被截断。
3.1.5、“Summary” 选项卡下各参数配置
最后一个是 “Summary” 选项卡,该界面显示了我们配置的存储器的类型,消耗的 BRAM 资源等信息, 我们直接点击“OK”按钮完成 FIFO Generator IP 核的配置,如下图所示:
可以看到“Summary”选项卡中有一条警告时建议我们启用安全电路,我们在之前的配置中已经启用了,所以不用理会。之后的几步操作是生成 IP 核时的必要操作,在之前的几篇 IP 核相关的文档中已经讲解过数次了,想必大家已经很熟悉了,这里就不再赘述了。
无论是同步 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 的复位信号,其时序图如下所示:
由上图可总结出以下几点注意事项:
1、建议在安全电路下的异步 FIFO 的复位信号(RST)至少要保持八个时钟周期(以慢时钟为准)的有效。
2、在 RST 上升沿时期,经过 7 个 WR_CLK(写时钟)周期后 WR_RST_BUSY(写复位忙)信号拉高, FULL(满)信号拉高,此时的 WR_EN(写使能)信号应该拉低。
3、在 RST 上升沿时期,经过 7 个 RD_CLK(读时钟)周期后 RD_RST_BUSY(读复位忙)信号拉高,EMPTY(空)信号拉高,此时的 RD_EN(读使能)信号应该拉低,而且此时的数据输出端口均为无效。
4、图中建议我们至少在复位结束后,经过 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 中已经没有可被读取的数据了。
本次实验的目的是为了将 Xilinx FIFO Generato IP 核配置成一个异步 FIFO 并对其进行读写操作,因此可以给模块命名为 ip_fifo;因为我们做的是异步 FIFO,所以我们需要一个 PLL IP 核来输出 50MHz 的写时钟和 100MHz 的读时钟,当然输出其它频率的时钟也是可以的;然后我们还需要一个写模块(fifo_wr)和一个读模块(fifo_rd),写模块负责给出写请求信号和写数据,读模块负责给出读请求信号,并接收从FIFO 中读出的数据;系统时钟和系统复位是一个完整的工程中必不可少的输入端口信号,这里就不再多讲了。经过上述分析我们可以画出一个大致的模块框图,如下图所示:
模块端口与功能描述如下表所示:
因为本次实验是使用 FIFO Generator IP 核来生成一个异步 FIFO,所以我们需要使用到 PLL IP 核来输出两路不同频率的时钟,除此之外我们还需要一个读模块(fifo_rd)和一个写模块(fifo_wr)来进行异步的读写操作,所以我们需要创建一个顶层模块来例化两个 IP 核与读/写模块,这里我们将顶层模块命名为 ip_fifo,代码如下:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2023/06/12 15:52:44
// Design Name:
// Module Name: ip_fifo
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
//实验任务:
//本节的实验任务是使用 Vivado 生成一个异步 FIFO,并实现以下功能:当 FIFO 为空时,
//向 FIFO 中写入数据,直至将 FIFO 写满后停止写操作;
//当 FIFO 为满时,从 FIFO 中读出数据,直到 FIFO 被读空后停止读操作,
//以此向大家详细介绍一下 FIFO IP 核的使用方法。
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_din; //写入到FIFO的数据
wire [7:0] fifo_dout; //从FIFO读出的数据
wire almost_full; //FIFO将满信号
wire almost_empty; //FIFO将空信号
wire fifo_full; //FIFO满信号
wire fifo_empty; //FIFO空信号
wire [7:0] fifo_wr_data_count; // FIFO 写时钟域的数据计数
wire [7:0] fifo_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 signal
.locked (locked ), //output locked
//clock in port
.clk_in1 (sys_clk ) //input clk_in1
);
//例化 FIFO IP 核
fifo_generator_0 fifo_generator_0(
.rst (~sys_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_din ), //input wire [7:0] din
.dout (fifo_dout ), //output wire [7:0] dout
.almost_full (almost_full ), //output wire almost_full
.almost_empty (almost_empty ), //output wire almost_empty
.full (fifo_full ), //output wire full
.empty (fifo_empty ), //output wire empty
.wr_data_count (fifo_wr_data_cout ), //output wire [7:0] wr_data_cout
.rd_data_count (fifo_rd_data_cout ), //output wire [7:0] rd_data_cout
.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(
.clk (clk_50m ), //写时钟
.rst_n (rst_n ), //复位信号
.wr_rst_busy (wr_rst_busy ), //写复位忙信号
.fifo_wr_en (fifo_wr_en ), //fifo 写请求
.fifo_wr_data (fifo_din ), //写入FIFO的数据
.almost_empty (almost_empty ), //fifo 空信号
.almost_full (almost_full ) //fifo 满信号
);
//例化读 FIFO 模块
fifo_rd u_fifo_rd (
.clk (clk_100m ), // 读时钟
.rst_n (rst_n ), // 复位信号
.rd_rst_busy (rd_rst_busy ), // 读复位忙信号
.fifo_rd_en (fifo_rd_en ), // fifo 读请求
.fifo_dout (fifo_dout ), // 从 FIFO 输出的数据
.almost_empty (almost_empty ), // fifo 空信号
.almost_full (almost_full ) // fifo 满信号
);
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 核需要一定的时间才能输出稳定的时钟,所以在代码中(第56 行)我们通过系统复位和时钟锁定来产生一个信号复位信号,使读/写模块在时钟稳定后才进入工作状态。
首先介绍下 FIFO 写模块的设计,在 FIFO 写模块中,我们的输入信号主要有系统时钟信号、系统复位信号;因为 FIFO 的写操作需要在 FIFO 完成复位后进行,所以我们还需要输入 wr_rst_busy(写复位忙)来作为判断 FIFO 是否结束了复位状态;实验任务中我们提到了 FIFO 为空时进行写操作,因此还需要引入一个空相关的信号,这里我们引入的是 almost_empty(将空)信号,当然引入 empty(空)信号也是可以的;实验任务中我们还提到了写满了要停止写操作,所以这里我们引入了 almost_full(将满)信号,因为将满信号表示 FIFO 还能再进行最后一次写操作,使用这个信号的话我们正好可以在写入最后一次数据后关闭写使能,当然引入 full(满)信号也是可以,区别只是在于这么做会在写使能关断前执行一次无效的写操作。
输出信号有控制写 FIFO 所需的 fifo_wr_en(写端口使能)和 fifo_wr_data(写数据)这两个信号。由上述分析绘制出如下图所示的模块框图:
模块端口与功能描述如下表所示:
在编写代码前,我们先大致梳理一下模块的端口时序,并绘制出如下波形图:
由上图可知,当系统复位结束后,FIFO 还处于复位状态,我们需要等待 FIFO 写复位结束(即 wr_rst_busy 信号拉低)后,再对 FIFO 进行写操作。这里说明一下,因为 almost_empty(将空)信号是FIFO 读时钟域的输出信号,对于写操作来说属于异步信号,所以这里我们引入了 almost_empty_syn 信号,通过打拍的方式将 almost_empty 信号同步到写时钟域下。需要注意的是,当 FIFO 中有两个或两个以上的数据时,将空信号就会被拉低,但是因为是异步 FIFO,所以状态信号的同步需要一定的时间,因此将空信号并不是在写入第二个数据后拉低的。
写模块的运行是通过一个小的状态机(state)实现的,当 FIFO 复位结束后,在 state=0(空闲状态)时,若检测到 almost_empty_syn 信号为高,则跳转到延迟状态(state=1);延迟状态用于延迟 10 拍(dly_cnt 计数实现),其目的是等待 FIFO 的内部状态信号更新完成,在第 10 拍时,打开fifo_wr_en(写使能)信号,清空 dly_cnt 计数器并跳转到写操作状态(state=2);写操作状态时,向 FIFO 中写入从 0 开始的累加数据,直至检测到 almost_full(将满)信号时,关闭写使能,将写数据清零并跳转回空闲状态(state=0),等待下一次的写操作。
fifo_wr 模块用于产生 FIFO 写操作所需的信号,其代码如下所示:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2023/06/12 20:08:10
// Design Name:
// Module Name: fifo_wr
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
//写模块的运行是通过一个小的状态机(state)实现的,当 FIFO 复位结束后,
//在 state=0(空闲状态)时,若检测到 almost_empty_syn 信号为高,则跳转到延迟状态(state=1);
//延迟状态用于延迟 10 拍(dly_cnt 计数实现),其目的是等待 FIFO 的内部状态信号更新完成,
//在第 10 拍时,打开 fifo_wr_en(写使能)信号,清空 dly_cnt 计数器并跳转到写操作状态(state=2);
//写操作状态时,向 FIFO 中写入从 0 开始的累加数据,直至检测到 almost_full(将满)信号时,
//关闭写使能,将写数据清零并跳转回空闲状态(state=0),等待下一次的写操作。
//fifo_wr 模块用于产生 FIFO 写操作所需的信号
module fifo_wr(
//module clock
input clk, //时钟信号
input rst_n, //复位信号
//FIFO interface
input wr_rst_busy, //写复位忙信号
input almost_empty, //FIFO 将空信号
input almost_full, //FIFO 将满信号
output reg fifo_wr_en, //FIFO 写使能
output reg [7:0] fifo_wr_data //写入FIFO 的数据
);
//reg define
reg [1:0] state; //动作转态
reg almost_empty_d0; //almost_empty 延迟一拍
reg almost_empty_syn; //almost_empty 延迟两拍
reg [3:0] dly_cnt; //延迟计数器
//********************************************************
//** main code
//********************************************************
//因为 almost_empty 信号是属于FIFO 读时钟域的
//所以要将其同步到写时钟域中
always @(posedge clk) begin
if(!rst_n) begin
almost_empty_d0 <= 1'b0;
almost_empty_syn <= 1'b0;
end
else begin
almost_empty_d0 <= almost_empty;
almost_empty_syn <= almost_empty_d0;
end
end
//向FIFO中写入数据
always @(posedge clk) begin
if(!rst_n) begin
fifo_wr_en <= 1'b0;
fifo_wr_data <= 8'd0;
state <= 2'd0;
dly_cnt <= 4'd0;
end
//当写复位处于空闲状态时
else if(!wr_rst_busy) begin
case(state)
2'd0: begin
if(almost_empty_syn) begin //如果检测到FIFO 将被读空
state <= 2'd1; //就进入延时状态
end
else
state <= state;
end
2'd1: begin
//原因是 FIFO IP 核内部状态信号的更新存在延时 //延迟 10 拍以等待状态信号更新完毕
if(dly_cnt==4'd10) begin //延时10拍
dly_cnt <= 4'd0;
state <= 2'd2; //开始写操作
fifo_wr_en <= 1'b1; //打开写使能
end
else
dly_cnt <= dly_cnt + 4'd1;
end
2'd2: begin
if(almost_full) begin //等待FIFO将被写满
fifo_wr_en <= 1'b0; //关闭写使能
fifo_wr_data <= 8'd0;
state <= 2'd0; //回到第一个状态
end
else begin //如果FIFO没有被写满
fifo_wr_en <= 1'b1; //则持续打开写使能
fifo_wr_data <= fifo_wr_data + 1'd1; //且写数据持续累加
end
end
default: state <= 2'd0;
endcase
end
else begin
fifo_wr_en <= 1'b0;
fifo_wr_data <= 8'd0;
state <= 2'd0;
dly_cnt <= 4'd0;
end
end
endmodule
fifo_wr 模块的核心部分是一个不断进行状态循环的小状态机,如果检测到 FIFO 为空,则先延时 10 拍,这里注意,由于 FIFO 的内部信号的更新比实际的数据读/写操作有所延时,所以延时 10 拍的目的是等待 FIFO 的空/满状态信号、数据计数信号等信号的更新完毕之后再进行 FIFO 写操作,如果写满,则回到状态 0,即等待 FIFO 被读空,以进行下一轮的写操作。
首先介绍下 FIFO 读模块的设计,在 FIFO 读模块中,我们的输入信号主要有系统时钟信号、系统复位信号;因为 FIFO 的读操作需要在 FIFO 完成复位后进行,所以我们还需要输入 rd_rst_busy(读复位忙)来作为判断 FIFO 是否结束了复位状态;实验任务中我们提到了 FIFO 为满时进行读操作,因此还需要引入一个满相关的信号,这里我们引入的是 almost_full(将满)信号,当然引入 full(满)信号也是可以的;实验任务中我们还提到了读空了要停止读操作,所以这里我们引入了 almost_empty(将空)信号,因为将空信号表示 FIFO 还能再进行最后一次读操作,使用这个信号的话我们正好可以在读出最后一个数据后关闭读使能,当然引入 empty(空)信号也是可以,区别只是在于这么做会在读使能关断前执行一次无效的读操作。
输出信号有控制写 FIFO 所需的 fifo_rd_en(读端口使能)信号。由上述分析绘制出如下图所示的模块框图:
模块端口与功能描述如下表所示:
在编写代码前,我们先大致梳理一下模块的端口时序,并绘制出如下波形图:
由上图可知,当系统复位结束后,FIFO 还处于复位状态,我们需要等待 FIFO 读复位结束(即 wr_rst_busy 信号拉低)后,再对 FIFO 进行读操作。这里说明一下,因为 almost_full(将满)信号是 FIFO 写时钟域的输出信号,对于读操作来说属于异步信号,所以这里我们引入了 almost_full_syn 信号,通过打拍的方式将 almost_full 信号同步到读时钟域下。需要注意的是,当 FIFO 中的数据量小于写深度-1 时,将满信号就会被拉低,但是因为是异步 FIFO,所以状态信号的同步需要一定的时间,因此将满信号并不是在读出第二个数据后拉低的。
读模块的运行同样是通过一个小的状态机(state)实现的,当 FIFO 复位结束后,在 state=0(空闲状态)时,若检测到 almost_full_syn 信号为高,则跳转到延迟状态(state=1);延迟状态用于延迟 10 拍(dly_cnt 计数实现),其目的是等待 FIFO 的内部状态信号更新完成,在第 10 拍时,打开 fifo_rd_en(读使能)信号,清空 dly_cnt 计数器并跳转到读操作状态(state=2);读操作状态时,模块接收从 FIFO 中读出的数据,直至检测到 almost_empty(将空)信号时,关闭读使能并跳转回空闲状态(state=0),等待下一次的读操作。
fifo_rd 模块用于产生 FIFO 读操作所需的信号,其代码如下所示:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2023/06/13 16:57:14
// Design Name:
// Module Name: fifo_rd
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
//读模块的运行同样是通过一个小的状态机(state)实现的,当 FIFO 复位结束后,
//在 state=0(空闲状态)时,若检测到 almost_full_syn 信号为高,则跳转到延迟状态(state=1);
//延迟状态用于延迟 10 拍(dly_cnt 计数实现),其目的是等待 FIFO 的内部状态信号更新完成,
//在第 10 拍时,打开 fifo_rd_en(读使能)信号,清空 dly_cnt 计数器并跳转到读操作状态(state=2);
//读操作状态时,模块接收从 FIFO 中读出的数据,直至检测到 almost_empty(将空)信号时,
//关闭读使能并跳转回空闲状态(state=0),等待下一次的读操作。
//读 FIFO 模块 fifo_rd.v
//fifo_rd 模块用于产生 FIFO 读操作所需的信号
module fifo_rd(
//system clock
input clk, //时钟信号
input rst_n, //复位信号
//FIFO interface
input rd_rst_busy, //读复位忙信号
input [7:0] fifo_dout, //从FIFO读出数据
input almost_full, //FIFO将满信号
input almost_empty, //FIFO将空信号
output reg fifo_rd_en //FIFO读使能
);
//reg define
reg [1:0] state; //动作状态
reg almost_full_d0; //fifo_full 延迟一拍
reg almost_full_syn; //fifo_full 延迟两拍
reg [3:0] dly_cnt; //延迟计数器
//*****************************************************
//** main code
//*****************************************************
//因为 fifo_full 信号是属于 FIFO 写时钟域的
//所以要将其同步到读时钟域中
always@( posedge clk ) begin
if( !rst_n ) begin
almost_full_d0 <= 1'b0 ;
almost_full_syn <= 1'b0 ;
end
else begin
almost_full_d0 <= almost_full ;
almost_full_syn <= almost_full_d0 ;
end
end
//读出 FIFO 的数据
always @(posedge clk ) begin
if(!rst_n) begin
fifo_rd_en <= 1'b0;
state <= 2'd0;
dly_cnt <= 4'd0;
end
//当读复位处于空闲状态时
else if(!rd_rst_busy) begin
case(state)
2'd0: begin
if(almost_full_syn) //如果检测到 FIFO 将被写满
state <= 2'd1; //就进入延时状态
else
state <= state;
end
2'd1: begin
if(dly_cnt == 4'd10) begin //延时 10 拍
//原因是 FIFO IP 核内部状态信号的更新存在延时
//延迟 10 拍以等待状态信号更新完毕
dly_cnt <= 4'd0;
state <= 2'd2; //开始读操作
fifo_rd_en <= 1'b1; //打开读使能
end
else
dly_cnt <= dly_cnt + 4'd1;
end
2'd2: begin
if(almost_empty) begin //等待 FIFO 将被读空
fifo_rd_en <= 1'b0; //关闭读使能
state <= 2'd0; //回到第一个状态
end
else //如果 FIFO 没有被读空
fifo_rd_en <= 1'b1; //则持续打开读使能
end
default : state <= 2'd0;
endcase
end
else begin
fifo_rd_en <= 1'b0;
state <= 2'd0;
dly_cnt <= 4'd0;
end
end
endmodule
//读模块的代码结构与写模块几乎一样,也是使用一个不断进行状态循环的小状态机来控制操作过程。
读模块的代码结构与写模块几乎一样,也是使用一个不断进行状态循环的小状态机来控制操作过程。
我们接下来先对代码进行仿真,因为本章实验我们只有系统时钟和系统复位这两个输入信号,所以仿真文件也只需要编写这两个信号的激励即可,TestBench 代码如下:
`timescale 1ns / 1ps //仿真单位/仿真精度
//
// Company:
// Engineer:
//
// Create Date: 2023/06/13 21:30:18
// Design Name:
// Module Name: tb_ip_fifo
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
//对代码进行仿真,因为本章实验我们只有系统时钟和系统复位这两个输入信号,
//所以仿真文件也只需要编写这两个信号的激励即可。
module tb_ip_fifo();
//parameter define
parameter CLK_PERIOD = 20; //50MHz系统时钟(一个周期是20ns:1/50MHz=0.02us=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。
本次实验我们模拟了一次复位状态,因为复位信号至少要保持 8 个时钟周期(以慢时钟为准)的有效,而我们的慢时钟的频率为 50MHz,所以复位状态至少要保持 20ns。
通过仿真我们得到了以下波形图:
可以看出 FIFO 是在读空时进行写操作,在写满时进行读操作,当复位信号来临时,FIFO 内的数据被清空,各信号恢复到初始状态。
接着我们展开波形图看一下各个状态的细节,首先是复位状态,其波形如下图所示:
写使能拉高后开始向 FIFO 中写入数据,当写完倒数第二个数时,将满信号被拉高,当写完最后一个数时,满信号拉高,写使能被拉低。这里我们也可以看出,虽然我们将写 FIFO 的深度为 256, 但写 FIFO 的实际深度确实只有 255,与 IP 核设置界面的提示是一致的,这是 IP 核的设计问题,我们在使用时只需要注意一下就可以了。
读使能拉高后开始从 FIFO 中读出数据,当读完倒数第二个数时,将空信号被拉高,当读完最后一个数时,空信号拉高,读使能被拉低。
在仿真验证完成后,接下来对引脚进行分配,并上板验证。本节实验只用到了输入的时钟信号和按键复位信号,没有用到其它硬件外设。
本实验中,各端口信号的管脚分配如下表所示:
对应的 XDC 约束语句如下所示:
set_property -dict {PACKAGE_PIN R4 IOSTANDARD LVCMOS15} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN U7 IOSTANDARD LVCMOS15} [get_ports sys_rst_n]
接下来添加 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_din ), // input wire [7:0] probe1
.probe2 (almost_full ), // input wire [0:0] probe2
.probe3 (fifo_full ), // input wire [0:0] probe3
.probe4 (fifo_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_dout ), // input wire [7:0] probe1
.probe2 (almost_empty ), // input wire [0:0] probe2
.probe3 (fifo_empty ), // input wire [0:0] probe3
.probe4 (fifo_rd_data_count ) // input wire [7:0] probe4
);
本章节我们主要讲解了 FIFO 的概念和使用场景,并介绍了 Vivado 软件中如何对 FIFO Generator IP 核进行配置和调用(例化),其例化语句如下所示:
//例化 FIFO IP 核
fifo_generator_0 fifo_generator_0(
.rst (~sys_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_din ), //input wire [7:0] din
.dout (fifo_dout ), //output wire [7:0] dout
.almost_full (almost_full ), //output wire almost_full
.almost_empty (almost_empty ), //output wire almost_empty
.full (fifo_full ), //output wire full
.empty (fifo_empty ), //output wire empty
.wr_data_count (fifo_wr_data_cout ), //output wire [7:0] wr_data_cout
.rd_data_count (fifo_rd_data_cout ), //output wire [7:0] rd_data_cout
.wr_rst_busy (wr_rst_busy ), //output wire wr_rst_busy
.rd_rst_busy (rd_rst_busy ) //output wire rd_rst_busy
);