FIFO IP 核简介
根据 FIFO
工作的时钟域,可以将
FIFO
分为同步
FIFO
和异步
FIFO
。同步
FIFO
是指读时钟和写时钟为同一个时钟,在时钟沿来临时同时发生读写操作。异步 FIFO
是指读写时钟不一致,读写时钟是互相独立。Xilinx
的
FIFO IP
核可以被配置为同步
FIFO
或异步
FIFO
,其信号框图如下图所示。从图中可以了解到,当被配置为同步 FIFO
时,只使用
wr_clk,
所有的输入输出信号都同步于
wr_clk
信号。而当被配置为异步
FIFO时,写端口和读端口分别有独立的时钟,所有与写相关的信号都是同步于写时钟 wr_clk
,所有与读相关的信号都是同步于读时钟 rd_clk
。
对于 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
时所遵循的时钟,在每个时钟的上升沿触发。
这里请注意,“almost_empty”
和
“almost_full”
这两个信号分别被看作
“empty”
和
“full”
的警告信号,他们相对于真正的空(empty
)和满(
full
)都会提前一个时钟周期拉高。
对于 FIFO 的基本知识先了解这些就足够了,可能有人会好奇为什么会有同步
FIFO
和异步
FIFO
,它们各自的用途是什么。之所以有同步 FIFO
和异步
FIFO
是因为各自的作用不同。同步
FIFO
常用于同步时钟的数据缓存,异步 FIFO
常用于跨时钟域的数据信号的传递,例如时钟域
A
下的数据
data1
传递给异步时钟域 B
,当
data1
为连续变化信号时,如果直接传递给时钟域
B
则可能会导致收非所送的情况,即在采集过程中会出现包括亚稳态问题在内的一系列问题,使用异步 FIFO
能够将不同时钟域中的数据同步到所需的时钟域中。
实验任务
本节的实验任务是使用 Vivado
生成
FIFO IP
核,并实现以下功能:当
FIFO
为空时,向
FIFO
中写入数 ,写入的数据量和 FIFO
深度一致,即
FIFO
被写满;然后从
FIFO
中读出数据,直到
FIFO
被读空为止,以此向大家详细介绍一下 FIFO IP
核的使用方法。
硬件设计
本章实验只用到了输入的时钟信号和按键复位信号,没有用到其它硬件外设。
本实验中,各端口信号的管脚分配如下表所示。
对应的 XDC 约束语句如下所示:
create_clock -period 20.000 -name clk [get_ports sys_clk]
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]
程序设计
根据实验任务要求和模块化设计的思想,我们需要如下 4 个模块:fifo IP 核、写 fifo 模块、读 fifo 模块以及顶层例化模块实现前三个模块的信号交互。由于 FIFO 多用于跨时钟域信号的处理,所以本实验我们使用异步 FIFO 来向大家详细介绍双时钟 FIFO IP 核的创建和使用。为了方便大家理解,这里我们将读/写时钟都用系统时钟来驱动。系统的功能框图如下图所示:
首先创建一个名为 ip_fifo
的工程,接下来我们创建
fifo IP
核。在
Vivado
软件的左侧“
Flow Navigator
” 栏中单击“IP Catalog
”,“
IP Catalog
”按钮以及单击后弹出的“
IP Catalog”窗口如下图所示。
在“IP Catalog”窗口中,在搜索栏中输入“fifo”关键字,这时 Vivado 会自动查找出与关键字匹配的 IP 核名称,我们双击“FIFO Generator”,如下图所示。
弹出“Customize IP”窗口,如下图所示。
下来就是配置 IP 核的时钟参数的过程。
最上面的“Component Name
”一栏设置该
IP
元件的名称,这里保持默认即可。在第一个
“Basic”
选项卡中,“Interface Type”
选项用于选择
FIFO
接口的类型,这里我们选择默认的“
Native
”,即传统意义上的
FIFO 接口。“Fifo Implementation
”选项用于选择我们想要实现的是同步
FIFO
还是异步
FIFO 以及使用哪种资源实现 FIFO,这里我们选择“Independent Clocks Block RAM”,即使用块 RAM 来实现的异步 FIFO。如下图所示。
接下来是“Native Ports”选项卡,用于设置 FIFO 端口的参数。“Read Mode”选项用于设置读 FIFO时的读模式,这里我们选择默认的“Standard FIFO”。“Data Port Parameters”一栏用于设置读写端口的数据总线的宽度以及 FIFO 的深度,写宽度“Write Width”我们设置为 8 位,写深度“Write Depth”我们设置为 256,注意此时 FIFO IP 核所能实现的实际深度却是 255;虽然读宽度“Read Width”能够设置成和写宽度不一样的位宽,且此时读深度“Read Depth”会根据上面三个参数被动地自动设置成相应的值;但是我们还是将读宽度“Read Width”设置成和写宽度“Write Width”一样的位宽,这也是在实际应用中最常用的情况。由于我们只是观察 FIFO 的读写,所以最下面的“Reset Pin”选项我们可以不使用,把它取消勾选。其他设置保持默认即可,如下图所示。
“Status Flags”选项卡,用于设置用户自定义接口或者用于设定专用的输入口。这里我们使用“即将写满”和“即将读空”这两个信号,所以我们把它们勾选上,其他保持默认即可,如下图所示。
“Data Counts”选项卡用于设置 FIFO 内数据计数的输出信号,此信号表示当前在 FIFO 内存在多少个有效数据。为了更加方便地观察读/写过程,这里我们把读/写端口的数据计数都打开,且计数值总线的位宽设置为满位宽,即 8 位,如下图所示。
最后的“Summary”选项卡是对前面所有配置的一个总结,在这里我们直接点击“OK”按钮即可,如 下图所示。
接着就弹出了“Genarate Output Products”窗口,我们直接点击“Generate”即可,如下图所示。
之后我们就可以在“Design Run”窗口的“Out-of-Context Module Runs”一栏中出现了该 IP 核对应的run“fifo_generator_0_synth_1”,其综合过程独立于顶层设计的综合,所以在我们可以看到其正在综合,如下图所示。
在其 Out-of-Context 综合的过程中,我们就可以进行 RTL 编码了。首先打开 IP 核的例化模板,在“Source” 窗口中的“IP Sources”选项卡中,依次用鼠标单击展开“IP”-“fifo_generator _0”-“Instantitation Template”,我们可以看到“fifo_generator_0.veo”文件,它是由 IP 核自动生成的只读的 verilog 例化模板文件,双击就可以打开它,如下图所示。
我们创建一个 verilog 源文件,其名称为 ip_fifo.v,作为顶层模块,其代码如下:、
module ip_fifo(
input sys_clk , // 时钟信号
input sys_rst_n // 复位信号
);
//wire define
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
//*****************************************************
//例化FIFO IP核
fifo_generator_0 fifo_generator_0 (
.wr_clk ( sys_clk ), // input wire wr_clk
.rd_clk ( sys_clk ), // 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_count ), // output wire [7 : 0] wr_data_count
.rd_data_count ( fifo_rd_data_count ) // output wire [7 : 0] rd_data_count
);
//例化写FIFO模块
fifo_wr u_fifo_wr(
.clk ( sys_clk ), // 写时钟
.rst_n ( sys_rst_n ), // 复位信号
.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 ( sys_clk ), // 读时钟
.rst_n ( sys_rst_n ), // 复位信号
.fifo_rd_en ( fifo_rd_en ), // fifo读请求
.fifo_dout ( fifo_dout ), // 从FIFO输出的数据
.almost_empty ( almost_empty ), // fifo空信号
.almost_full ( almost_full ) // fifo满信号
);
//例化ILA IP核
ila_0 ila_0 (
.clk ( sys_clk ), // input wire clk
.probe0 ( fifo_wr_en ), // input wire [0:0] probe0
.probe1 ( fifo_rd_en ), // input wire [0:0] probe1
.probe2 ( fifo_din ), // input wire [7:0] probe2
.probe3 ( fifo_dout ), // input wire [7:0] probe3
.probe4 ( fifo_empty ), // input wire [0:0] probe4
.probe5 ( almost_empty ), // input wire [0:0] probe5
.probe6 ( fifo_full ), // input wire [0:0] probe6
.probe7 ( almost_full ), // input wire [0:0] probe7
.probe8 ( fifo_wr_data_count ), // input wire [7:0] probe8
.probe9( fifo_rd_data_count ) // input wire [7:0] probe9
);
endmodule
顶层模块主要是对 FIFO IP
核、写
FIFO
模块、读
FIFO
模块进行例化,除此之外本实验还生成并例化了一个 ILA IP
核,用于对顶层模块信号的进行在线捕获观察。
写 FIFO
模块
fifo_wr.v
源文件的代码如下:
module fifo_wr(
//mudule clock
input clk , // 时钟信号
input rst_n , // 复位信号
//FIFO interface
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 begin
case(state)
2'd0: begin
if(almost_empty_syn) begin //如果检测到FIFO将被读空
state <= 2'd1; //就进入延时状态
end
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_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
end
endmodule
fifo_wr 模块的核心部分是一个不断进行状态循环的小状态机,如果检测到
FIFO
为空,则先延时
10 拍,这里注意,由于 FIFO
的内部信号的更新比实际的数据读
/
写操作有所延时,所以延时
10
拍的目的是等待 FIFO
的空
/
满状态信号、数据计数信号等信号的更新完毕之后再进行
FIFO
写操作,如果写满,则回到状态 0
,即等待
FIFO
被读空,以进行下一轮的写操作。
读 FIFO
模块
fifo_rd.v
源文件的代码如下:
module fifo_rd(
//system clock
input clk , // 时钟信号
input rst_n , // 复位信号
//FIFO interface
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 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; //开始读操作
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
end
endmodule
读模块的代码结构与写模块几乎一样,也是使用一个不断进行状态循环的小的状态机来控制操作过程,读者参考着代码应该很容易能够理解,这里就不再赘述。
我们对代码进行仿真,TestBench
中只要送出时钟的复位信号即可。
TB
文件如下:
module tb_ip_fifo( );
// Inputs
reg sys_clk;
reg sys_rst_n;
// Instantiate the Unit Under Test (UUT)
ip_fifo u_ip_fifo (
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n)
);
//Genarate the clk
parameter PERIOD = 20;
always begin
sys_clk = 1'b0;
#(PERIOD/2) sys_clk = 1'b1;
#(PERIOD/2);
end
initial begin
// Initialize Inputs
sys_rst_n = 0;
// Wait 100 ns for global reset to finish
#100 ;
sys_rst_n = 1;
// Add stimulus here
end
endmodule
写满后转为读的仿真波形图如下图所示:
由波形图可知,当写满 255 个数据后,fifo_full 满信号就会拉高。经过延时之后,fifo_rd_en 写使能信号拉高,经过一拍之后就开始将 fifo 中的数据送到 fifo_dout 端口上。写满后转为读的仿真波形图如下图所示:
由波形图可知,当读完 255 个数据后,fifo_empty 空信号就会拉高。经过延时之后,fifo_wr_en 写使能信号拉高,经过一拍之后就开始向 fifo 中继续写入数据。
下载验证
编译工程并生成比特流.bit
文件,将比特流
.bit
文件下载到
Zynq
中。
下载完成后,接下来在 Vivado
中会自动出现“
hw_ila_1
”
Dashboard
窗口。如下图所示: