文章目录
- 前言
- 一、FIFO
- 1、区别
- 2、分类
- 二、单时钟&多时钟FIFO框图
- 三、FIFO IP 核配置
- 四、源码
- 1、fifo_wr(写模块)
- 2、fifo_rd(读模块)
- 3、ip_fifo(顶层文件)
- 五、仿真
- 1、仿真文件
- 2、波形分析
- 六、SignalTap II在线验证
- 七、总结
- 八、参考资料
前言
环境:
1、Quartus18.0
2、vscode
3、板子型号:原子哥开拓者2(EP4CE10F17C8)
要求:
实现当 FIFO 为空时就开始向 FIFO 中写入数据,直到 FIFO 写满为止;当 FIFO 为满时则开始从 FIFO 中读出数据,直到 FIFO 读空为止的功能。
一、FIFO
FIFO 的英文全称是 First In First Out,即先进先出。FPGA 使用的 FIFO 一般指的是对数据的存储具有先进先出特性的一个缓存器,常被用于数据的缓存或者高速异步数据的交互,也即所谓的跨时钟域信号传递。
1、区别
与RAM、ROM的相比,FIFO没有外部读写地址线,采用的是顺序写入、顺序读出数据的方式,缺点就是不能像RAM、ROM那样由地址线决定读取或写入的地址。
2、分类
- 单时钟 FIFO:
单时钟 FIFO 具有一个独立的时钟端口 clock,因此所有的输入输出信号都同步于 clock 信号。
- 作用:
单时钟 FIFO 常用于同步时钟的数据缓存 - 双时钟 FIFO:
从输出数据的位宽的角度分为普通双时钟和混合宽度双时钟。
在双时钟FIFO 结构中,写端口和读端口分别有独立的时钟,所有与写相关的信号都是同步于写时钟 wrclk,所有与读相关的信号都是同步于读时钟 rdclk。
- 作用:
双时钟 FIFO 常用于跨时钟域的数据信号的传递,例如时钟域 A 下的数据 data1 传递给异步时钟域 B,当 data1 为连续变化信号时,如果直接传递给时钟域 B 则可能会导致收非所送的情况,即在采集过程中会出现包括亚稳态问题在内的一系列问题,使用双时钟 FIFO 能够将不同时钟域中的数据同步到所需的时钟域中。
二、单时钟&多时钟FIFO框图
- 模块框图:
- 端口框图:
- 参数描述:
FIFO 的宽度:FIFO 一次读写操作的数据位 N;
FIFO 的深度:FIFO 可以存储多少个宽度为 N 位的数据。
空标志:对于双时钟 FIFO 又分为读空标志 rdempty 和写空标志 wrempty。FIFO 已空或将要空时由FIFO 的状态电路送出的一个信号,以阻止 FIFO 的读操作继续从FIFO 中读出数据而造成无效数据的读出。
满标志:对于双时钟 FIFO 又分为读满标志 rdfull 和写满标志 wrfull。FIFO 已满或将要写满时由 FIFO的状态电路送出的一个信号,以阻止 FIFO 的写操作继续向 FIFO 中写数据而造成溢出。
读时钟:读 FIFO 时所遵循的时钟,在每个时钟的上升沿触发。
写时钟:写 FIFO 时所遵循的时钟,在每个时钟的上升沿触发。
三、FIFO IP 核配置
- 查找FIFO IP:
- 双击过后:
选择路径、语言。
- 选择位宽、深度、时钟:
- 选择中等优化:
1、最低延迟,但要求同步时钟: 此选项使用一个同步阶段,没有亚稳态保护,适用于同步时钟。它是最小尺寸,提供良好的 Fmax。
2、异步时钟时的最小设置: 这个选项使用两个同步阶段,具有良好的亚稳态保护。它是中等尺寸,提供良好的 Fmax。
3、异步时钟时最好的亚稳态保护,最好的Fmax,不同步: 这个选项使用三个或更多的同步阶段,具有最好的亚稳态保护。它是最大尺寸,给出了最好的 Fmax。
我们一般会根据工程需求以及资源来选择合适的优化。
- 选择输出控制信号:
这里我们选择读空、读满、读侧数据量和写空、写满信号、写侧数据量
- 选择输出模式和存储器类型:
输出模式:
1、正常模式:FIFO 将端口 rdreq 看做正常的读请求并在该端口信号为高电平进行读操作。
2、前显模式:FIFO 将端口 rdreq 看做读确认信号,将 rdreq 信号置为高电平时将输出 FIFO 中的下一个数据字(如果存在)。
- 上溢检测与下溢检测:
这里的上下溢检测默认是打开的,如果不需要的话可以进行勾选来禁止,以此来节省资源,提高FIFO性能。
- 添加仿真库(默认存在):
- 勾选inst文件:
- 点击finish后点击YES添加到工程:
- 添加成功效果:
四、源码
1、fifo_wr(写模块)
module fifo_wr(
input clk ,
input rst_n ,
input wrempty ,//写空信号
input wrfull ,//写满信号
output reg [7:0] data ,//写数据
output reg wrreq //写请求
);
reg [1:0] flow_cnt;//状态计数器
/***************
写 fifo 模块主要完成向 fifo 中写入数据的功能,当 fifo 为空时,向 fifo 中写入数据;
当 fifo 写满之后,停止写入数据,然后重新判断 fifo 是否为空。
***************/
//写数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
wrreq <= 1'b0;
data <= 8'd0;
flow_cnt <= 2'd0;
end
else begin
case(flow_cnt)
2'd0: begin
if(wrempty) begin//写空间为空,写请求使能,进入下一个状态
wrreq <= 1'b1;
flow_cnt <= flow_cnt + 1'b1;
end
else
flow_cnt <= flow_cnt;//其实最多就计到1,其余状态不变,在写满后置0
end
2'd1: begin
if(wrfull) begin
wrreq <= 1'b0;
data <= 8'd0;
flow_cnt <= 2'd0;
end
else begin
wrreq <= 1'b1;//保持写请求为有效状态
data <= data + 1'd1;//写数据加1
end
end
default: flow_cnt <= 2'd0;
endcase
end
end
endmodule
2、fifo_rd(读模块)
module fifo_rd(
input clk ,
input rst_n ,
input [7:0] data ,
input rdfull ,
input rdempty ,
output reg rdreq
);
reg [7:0] data_fifo;//读取的数据
reg [1:0] flow_cnt;//状态计数器
/***************
读 fifo 模块主要完成向 fifo 中读出数据的功能,当 fifo 数据为满的状态时,
开始向 fifo 中读出数据;当 fifo 读空之后,停止读数据,然后重新判断 fifo 是否为满的状态。
***************/
//读取数据
always @(posedge clk or negedge rst_n)begin
if(!rst_n) begin
rdreq <= 1'b0;
data_fifo <= 8'd0;
end
else begin
case(flow_cnt)
2'd0: begin
if(rdfull) begin//与写不同,读是满的时候才使能
rdreq <= 1'b1;
flow_cnt <= flow_cnt + 1'b1;
end
else
flow_cnt <= flow_cnt;
end
2'd1: begin
if(rdempty) begin
rdreq <= 1'b0;
data_fifo <= 8'd0;
flow_cnt <= 2'd0;
end
else begin
rdreq <= 1'b1;
data_fifo <= data;
end
end
default: flow_cnt <= 2'd0;
endcase
end
end
endmodule
3、ip_fifo(顶层文件)
module ip_fifo(
input sys_clk ,
input sys_rst_n
);
//写数据
wire wrreq ;
wire [7:0] data ;
wire wrempty ;
wire wrfull ;
wire wrusedw;//写侧FIFO中的数据量
//读数据
wire rdreq ;
wire [7:0] q ;//FIFO读出的数据
wire rdempty ;
wire rdfull ;
wire rdusedw;//读侧FIFO中的数据量
//fifo IP核
fifo fifo_inst(
.wrclk ( sys_clk ), // 写时钟
.wrreq ( wrreq ), // 写请求
.data ( data ), // 写入 FIFO 的数据
.wrempty ( wrempty ), // 写空信号
.wrfull ( wrfull ), // 写满信号
.wrusedw ( wrusedw ), // 写侧数据量
.rdclk ( sys_clk ), // 读时钟
.rdreq ( rdreq ), // 读请求
.q ( q ), // 从 FIFO 输出的数据
.rdempty ( rdempty ), // 读空信号
.rdfull ( rdfull ), // 读满信号
.rdusedw ( rdusedw ) // 读侧数据量
);
//写模块
fifo_wr fifo_wr_inst(
.clk (sys_clk),
.rst_n (sys_rst_n),
.wrempty (wrempty),//写空信号
.wrfull (wrfull),//写满信号
.data (data),//写数据
.wrreq (wrreq) //写请求
);
//读模块
fifo_rd fifo_rd_inst(
.clk (sys_clk),
.rst_n (sys_rst_n),
.data (q),
.rdfull (rdfull),
.rdempty (rdempty),
.rdreq (rdreq)
);
endmodule
五、仿真
1、仿真文件
`timescale 1ns/1ns
module ip_fifo_tb();
parameter T = 20;
reg sys_clk;
reg sys_rst_n;
initial begin
sys_clk <= 1'b0;
sys_rst_n <= 1'b0;
#(T + 1)
sys_rst_n <= 1'b1;
#(3000*4)
$stop;
end
always #(T) sys_clk = ~sys_clk;
ip_fifo ip_fifo_inst(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n)
);
endmodule
2、波形分析
- 说明:
这里为了我们理解,使用的是同步FIFO,即读写使用的都是系统时钟。意味着读和写操作不能同时进行。只有在写操作完成后,才能进行读操作。
当有数据写入FIFO时,写操作将会阻塞,直到有读操作将数据读取走。同样地,当FIFO已满时,写操作也会阻塞,直到有读操作将数据读取走腾出空间。这确保了数据的顺序性:先进先出。
- 波形分析:
在波形写满过后,且经过三个时钟周期后,读满信号 rdfull 才有效,并且在读请求信号rereq 拉高后的第 3 个时钟周期写满信号 wrfull 才拉低。写完才进行的读,且是从0开始读的,符合先进先出。
六、SignalTap II在线验证
- 读状态:
- 分析:
在写满过后,写使能置0,无法进行写入,隔三个时钟周期读满信号有效,读使能有效,从0开始读出数据,我们开始写入也是从0开始的,符合先进先出。
- 写状态:
- 分析:
在读空三个周期过后,写使能有效,开始从0进行写入操作。因为这里使用的是同一时钟,所以在写的时候会堵塞,写满后才能读取。
七、总结
写到这一篇IP核的系列文章就结束了,还有许多的IP核等待我们的学习,只是这几个是比较常用的先进行一个学习。与RAM不同,FIFO是先进先出,不能随意对某一个地址进行读写,只能按照顺序读写。
八、参考资料
以上资料均来自正点原子的教学视频或开拓者2开发教程:原子官方