FPGA FIFO基本原理之同步FIFO
- FIFO释义
- FIFO(First In First Out,即先入先出),在FPGA/ASIC中作为一种先进先出的数据缓存器,与普通存储器的区别为没有外部读写地址,使用简单,缺点为只能顺序读写,数据地址由内部计数器计数使读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。
- FIFO的用途
- FIFO一般用于不同时钟域之间的数据传输,比如FIFO的一端是AD数据采集(采集的模拟电压转换为数字信号),另一端是计算机PCI总线;假设AD采集速率为16位100K SPS(Sample Per Second 每秒采样数),即在16位AD精度下每秒获得100K个采样值,bps = 100k * 16bit = 1.6Mbps,每秒的数据量就是1.6Mbps,PCI总线的速度为33MHz,总线宽度为32bit,其最大传输速率为33*32 = 1056Mbps;在两个不同的时钟域之间就可以采用FIFO来作为数据缓冲;对于不同宽度的数据接口也可以用FIFO,例如单片机8位数据输出,DSP可能是16位数据输入,在单片机与DSP进行接口连接时就可以使用FIFO来达到数据匹配的目的。总的来说就是做交互数据的一个缓冲,当数据发生突发写入(即数据写入过快,并且间隔时间长)时,通过设置一定深度的FIFO,可以起到数据暂存的功能,防止数据丢失且使得后续处理流程平滑;
- FIFO分类
- 根据FIFO工作时读写时钟是否为同一个时钟,可以将FIFO分为同步FIFO和异步FIFO。同步FIFO时指读写时钟为同一个时钟,在时钟沿来临时同时发生读写操作;异步FIFO是指读写时钟不一致,读写时钟相互独立。
- 若输入输出总线为同一时钟域,FIFO只是作为缓存使用,用同步FIFO即可,FIFO在同一时钟下工作,FIFO的写使能、读使能、满信号、空信号、输入输出数据等各种信号都在同一时钟沿输入或输出。
- 若输入输出为不同时钟域,FIFO做时钟协同作用,需要采用异步FIFO,FIFO的读写分别在各自时钟下工作,FIFO的写使能、写满信号、输入数据等各种输入信号都在同一输入时钟沿打入或输出。读使能、读空信号、输出数据等各种输出信号都在同一输出时钟沿打入或输出。
- FIFO设计
- FIFO的设计难点在于怎样判断FIFO的空/满状态,为了保证数据正确的写入或读出,而不发生溢出或读空的状态出现,必须保证FIFO在满的情况下不能进行写操作;在空的状态下不能进行读操作;
- 读写指针工作原理
- 写指针:总是指向下一个将要写入的地址,复位时指向第一个地址(序号为0)。
- 读指针:总是指向下一个时钟要读取的地址,复位时指向第一个地址(序号为0)。
Reset state,no data pushed to fifo: fifo empty | |||||||
RP,WP | |||||||
1 data pushed to fifo: | |||||||
RP | WP | ||||||
5 more data pushed to fifo: | |||||||
RP | WP | ||||||
6 data popped from fifo: fifo empty | |||||||
RP,WP | |||||||
5 data pushed to fifo: | |||||||
WP | RP | ||||||
3 data popped from fifo: | |||||||
RP | WP | ||||||
6 data pushed to fifo: fifo full | |||||||
RP,WP | |||||||
4 data popped from fifo: | |||||||
WP | RP | ||||||
1 data pushed to and popped from fifo at the same: no full and no empty | |||||||
WP | RP |
- 以一个深度为8的FIFO为例,读写指针的移动以及空满状态的转换如图所示
- FIFO的空满检测
- FIFO处于空或满的状态时,写指针和读指针总是指向同一个位置。换句话说,当写指针和读指针指向同一个位置时,FIFO必定处于空或者满的状态,具体是哪个状态我们还需要其他条件来判定。
- 复位时,读写指针都指向位置0,此时FIFO为空状态。
- 当FIFO中写入一些数据,但FIFO未处于满状态,再读出一些数据,让FIFO中剩一些数据,FIFO未处于空状态,此时写指针和读指针位于不同的位置;向FIFO中写入数据,此时写指针会逐渐递增,达到最大位置后跳到0位置,直到递增到和读指针的位置相同,停止写入数据,此时FIFO处于满状态。
- 当FIFO中写入一些数据,但FIFO未处于满状态,此时写指针位于某位置;从FIFO中读取数据,此时读指针会逐渐递增,直到递增到和写指针的位置相同,停止读取数据,此时FIFO处于空状态。
- RTL代码设计
Sync_FIFO.v
module Sync_FIFO
#(
parameter DATA_WIDTH = 8,//FIFO数据位宽
parameter FIFO_DEPTH = 8,//FIFO深度
parameter AFULL_DEPTH = FIFO_DEPTH - 1,//Almost full 深度
parameter AEMPTY_DEPTH = 1,//Almost empty 深度
parameter ADDR_WIDTH = 3,//FIFO地址的位宽
parameter RDATA_MODE = 0 //读FIFO中的数据的模式,组合逻辑0 or 时序逻辑1
)
(
input sys_clk ,
input sys_rst_n ,
input wr_en ,
input rd_en ,
input [DATA_WIDTH-1:0] wr_data ,
output [ADDR_WIDTH-1:0] usedw ,
output [DATA_WIDTH-1:0] rd_data ,
output full ,
output almost_full ,
output empty ,
output almost_empty ,
output reg overflow ,//上溢出,fifo满后继续写
output reg underflow //下溢出,fifo为空继续读
);
reg [ADDR_WIDTH-1 :0] wr_ptr ;//写指针用于产生写地址
reg [ADDR_WIDTH-1 :0] rd_ptr ;//读指针用于产生读地址
reg [ADDR_WIDTH :0] fifo_cnt ;//计算已使用的FIFO空间,即FIFO中数据个数
reg [DATA_WIDTH-1 :0] fifo_mem [0:FIFO_DEPTH-1] ;//FIFO缓冲器
reg [DATA_WIDTH-1 :0] rd_data_reg ;
wire[DATA_WIDTH-1 :0] rd_data_comb ;
integer i ;
//FIFO counter
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
fifo_cnt <= 'b0;
else if(wr_en && rd_en && !full && !empty)
fifo_cnt <= fifo_cnt;
else if(wr_en && !full)
fifo_cnt <= fifo_cnt + 1'b1;
else if(rd_en && !empty)
fifo_cnt <= fifo_cnt - 1'b1;
end
//write pointer address
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
wr_ptr <= 'b0;//wr_ptr <= {ADDR_WIDTH{1'b0}};
else if((wr_ptr == FIFO_DEPTH - 1'b1) && wr_en && !full)
wr_ptr <= 'b0;
else if(wr_en && !full)
wr_ptr <= wr_ptr + 1'b1;
else
wr_ptr <= wr_ptr;
end
//read pointer address
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
rd_ptr <= 'b0;
else if((rd_ptr == FIFO_DEPTH - 1'b1) && rd_en && !empty)
rd_ptr <= 'b0;
else if(rd_en && !empty)
rd_ptr <= rd_ptr + 1'b1;
else
rd_ptr <= rd_ptr;
end
//Write data to FIFO
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)begin
for(i=0;i<FIFO_DEPTH;i=i+1)
fifo_mem[i] <= 'b0;
end
else if(wr_en && !full)
fifo_mem[wr_ptr] <= wr_data;//0 1 2 3 ...... FIFO_DEPTH
end
//Read data from FIFO
//时序逻辑读取数据
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
rd_data_reg <= 'b0;
else if(rd_en && !empty)
rd_data_reg <= fifo_mem[rd_ptr];
else
rd_data_reg <= rd_data_reg;
end
//组合逻辑读取数据
assign rd_data_comb = (rd_en && !empty)?fifo_mem[rd_ptr]:rd_data_comb;
//overflow
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
overflow <= 1'b0;
else if(wr_en && full)
overflow <= 1'b1;
else
overflow <= 1'b0;
end
//underflow
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
underflow <= 1'b0;
else if(rd_en && empty)
underflow <= 1'b1;
else
underflow <= 1'b0;
end
//full & empty
assign full = (fifo_cnt == FIFO_DEPTH) ? 1'b1:1'b0;//注意 fifo_cnt是会计数到FIFO_DEPTH,而不是FIFO_DEPTH - 1’b1
assign empty = (fifo_cnt == 1'd0) ? 1'b1:1'b0;
//almost full & almost empty
assign almost_full = (fifo_cnt >= AFULL_DEPTH) ? 1'b1:1'b0;//fifo中数据个数大于某值时拉高
assign almost_empty = (fifo_cnt <= AEMPTY_DEPTH) ? 1'b1:1'b0;//fifo中数据个数小于某值时拉高
//rd_data
assign rd_data = RDATA_MODE?rd_data_reg:rd_data_comb;
//usedw
assign usedw = fifo_cnt;
endmodule
- 编写测试激励文件
tb_Sync_FIFO.v
`include "../rtl/Sync_FIFO.v"
`timescale 1ns/1ns
module tb_Sync_FIFO;
parameter DATA_WIDTH = 8; //FIFO数据位宽
parameter FIFO_DEPTH = 8; //FIFO深度
parameter AFULL_DEPTH = FIFO_DEPTH-1; //Almost full 深度
parameter AEMPTY_DEPTH = 1; //Almost empty 深度
parameter ADDR_WIDTH = 3; //FIFO地址的位宽
parameter RDATA_MODE = 1; //读FIFO中的数据的模式,组合逻辑0 or 时序逻辑1
reg sys_clk ;
reg sys_rst_n ;
reg wr_en ;
reg rd_en ;
reg [DATA_WIDTH-1:0] wr_data ;
wire [ADDR_WIDTH-1:0] usedw ;
wire [DATA_WIDTH-1:0] rd_data ;
wire full ;
wire almost_full ;
wire empty ;
wire almost_empty ;
wire overflow ;
wire underflow ;
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20 sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
initial begin
wr_en <= 1'b0;
rd_en <= 1'b0;
wr_data <= 'b0;
end
initial begin
#100 send_wr_en_data(1);
#100 send_wr_en_data(5);
#100 send_rd_en(6);
#100 send_wr_en_data(5);
#100 send_rd_en(3);
#100 send_wr_en_data(6);
#100 send_rd_en(4);
#100 send_wr_rd(1);
#100 $finish;
end
initial begin
$dumpfile("tb_Sync_FIFO.vcd");
$dumpvars();
end
task send_wr_en_data(
input [3:0] times
);
begin
repeat(times)begin
@(posedge sys_clk)begin
rd_en <= 1'b0;
wr_en <= 1'b1;
wr_data <= {$random};
end
@(posedge sys_clk)begin
wr_en <= 1'b0;
end
end
end
endtask
task send_rd_en(
input [3:0] times
);
begin
repeat(times)begin
@(posedge sys_clk)begin
rd_en <= 1'b1;
end
@(posedge sys_clk)begin
rd_en <= 1'b0;
end
end
end
endtask
task send_wr_rd(
input [3:0] times
);
begin
repeat(times)begin
@(posedge sys_clk)begin
wr_en <= 1'b1;
wr_data <= {$random};
rd_en <= 1'b1;
end
@(posedge sys_clk)begin
wr_en <= 1'b0;
rd_en <= 1'b0;
end
end
end
endtask
Sync_FIFO
#(
.DATA_WIDTH (DATA_WIDTH ),
.FIFO_DEPTH (FIFO_DEPTH ),
.AFULL_DEPTH (AFULL_DEPTH ),
.AEMPTY_DEPTH (AEMPTY_DEPTH ),
.ADDR_WIDTH (ADDR_WIDTH ),
.RDATA_MODE (RDATA_MODE )
)
Sync_FIFO_inst
(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.wr_en (wr_en ),
.rd_en (rd_en ),
.wr_data (wr_data ),
.usedw (usedw ),
.rd_data (rd_data ),
.full (full ),
.almost_full (almost_full ),
.empty (empty ),
.almost_empty (almost_empty ),
.overflow (overflow ),//上溢出,fifo满后继续写
.underflow (underflow ) //下溢出,fifo为空继续读
);
endmodule
- 仿真验证波形分析
- 我们的tb是仿照上面表格里的转移状况写的,能表现出各种状况。很遗憾,是在VScode中用WaveTrace看的波形图,因为是免费版,只能看8个信号,所以其他信号不能同时拉出来看,后面有补充一段其他信号的值,可以对照观看,发现其余功能也完全正确。
RDATA_MODE = 0时组合逻辑读出数据模式下的波形图,可以看到功能是完全正确的。前面有一段不定态是因为rd_en信号到后面才拉高,同时之前的rd_data是组合逻辑赋值,复位时没有给初始值,要rd_en有效才从FIFO中读取数据,所以rd_en未拉高之前都为不定态。
RDATA_MODE = 1时序逻辑读出数据模式下的波形图,可以看到功能是完全正确的。与组合逻辑读书数据模式相比前面就没有一段不定态,这是因为时序逻辑赋值复位时会赋初始值为0。
- 空满状态判定的另一种方法——高位扩展法
上面的方法是使用了一个计数器fifo_cnt判断FIFO中数据的存储,然后根据fifo_cnt的值去判定FIFO的满空状态。同步FIFO在网上还有一种所谓的高位扩展法来进行空满状态的判定。例如在深度为8的FIFO中,需要3bit的读写指针来分别指示读写地址3’b000-3’b111这8个地址。若将地址指针扩展1bit,则变成4bit的地址,而地址表示区间则变成了4’b0000-4’b1111。假设不看最高位的话,后面3位的表示区间仍然是3’b000-3’b111,也就意味着最高位可以拿来作为指示位。
Reset state,no data pushed to fifo: fifo empty wr_ptr/rd_ptr: 0000/0000 | |||||||
RP,WP | |||||||
1 data pushed to fifo: wr_ptr+1/rd_ptr: 0001/0000 | |||||||
RP | WP | ||||||
5 more data pushed to fifo: wr_ptr+5/rd_ptr: 0110/0000 | |||||||
RP | WP | ||||||
6 data popped from fifo: fifo empty wr_ptr/rd_ptr+6: 0110/0110 | |||||||
RP,WP | |||||||
5 data pushed to fifo: wr_ptr+5/rd_ptr: 1011/0110 | |||||||
WP | RP | ||||||
3 data popped from fifo: wr_ptr/rd_ptr+3: 1011/1001 | |||||||
RP | WP | ||||||
6 data pushed to fifo: fifo full wr_ptr+6/rd_ptr: 0001/1001 | |||||||
RP,WP | |||||||
4 data popped from fifo: wr_ptr/rd_ptr+4: 0001/1101 | |||||||
WP | RP | ||||||
1 data pushed to and popped from fifo at the same: no full and no empty wr_ptr+1/rd_ptr+1: 0010/1110 |
|||||||
WP | RP |
- 当最高位和其他位都相同,即1xxx 和 0xxx, xxx相同,这种情况只可能是1.写指针未超过最大位置,读指针追上了写指针,两者位置相同,fifo empty , 如表里所示6 data popped from fifo: fifo empty wr_ptr/rd_ptr+6: 0110/0110.2.写指针位置超过了最大值,然后读指针增加,位置超过了最大值从位置0开始递增,追上了写指针,此时两个指针所处的位置相同,表里 5 data pushed to fifo: wr_ptr/rd_ptr: 1011/0110时,再让读指针+5,此时 fifo empty wr_ptr+5/rd_ptr: 1011/1011,fifo empty;
- 当最高位不同,且其他位相同时,只可能是1.写指针追上了读指针,写指针已经转了一圈了,越过了最大位置,从位置0开始追上了读指针 如6 data pushed to fifo: fifo full wr_ptr+6/rd_ptr: 0001/1001 2.当4 data popped from fifo: wr_ptr/rd_ptr: 0001/1101时,再让写指针+4, wr_ptr+4/rd_ptr: 0101/1101,此时写指针追上了读指针,fifo full。这两种情况都意味着FIFO写满了。
- 高位扩展法来判定FIFO空满状态的同步FIFO设计
- 这种方法可以少使用一个fifo_cnt计数器,利用扩展高位位宽后的读写指针来判定空满状态,同时里面有一个系统函数$clog2();其实就是求对数,FIFO深度DATA_DEPTH和PTR_WIDTH满足2PTR_WIDTH = DATA_DEPTH的关系,PTR_WIDTH叫做以2为底DATA_DEPTH的对数,记做PTR_WIDTH=log2DATA_DEPTH,这么一看$clog2(DATA_DEPTH)就很清楚是在干嘛了,说白了就是在求满足DATA_DEPTH的FIFO深度,需要多大的位宽,2位宽 = FIFO 深度
sync_fifo2
`timescale 1ns/1ps
module sync_fifo2 #(
parameter DATA_WIDTH = 32,
parameter DATA_DEPTH = 8,
parameter PTR_WIDTH = $clog2(DATA_DEPTH)
)(
input wire clk_i,
input wire rst_n_i,
// write interface
input wire wr_en_i,
input wire [DATA_WIDTH-1:0] wr_data_i,
// read interface
input wire rd_en_i,
output reg [DATA_WIDTH-1:0] rd_data_o,
//Flags_o
output wire full_o,
output wire empty_o
);
reg [DATA_WIDTH-1:0] FIFO[0:DATA_DEPTH-1];
reg [PTR_WIDTH:0] wr_ptr;
reg [PTR_WIDTH:0] rd_ptr;
wire wr_ptr_msb;
wire rd_ptr_msb;
wire [PTR_WIDTH-1:0] wr_ptr_true;
wire [PTR_WIDTH-1:0] rd_ptr_true;
/*---------------------------------------------------\
------------- generate the flags -----------------
\---------------------------------------------------*/
assign full_o = (rd_ptr=={~wr_ptr_msb, wr_ptr_true}) ? 1'b1 : 1'b0;
assign empty_o = (rd_ptr==wr_ptr) ? 1'b1 : 1'b0;
/*---------------------------------------------------\
-------------------- read data -------------------
\---------------------------------------------------*/
always @(posedge clk_i or negedge rst_n_i) begin
if(!rst_n_i)
rd_ptr <= 3'b0;
else if(rd_ptr == DATA_DEPTH-1 && rd_en_i==1'b1 && empty_o==1'b0)
rd_ptr <= 3'b0;
else if(rd_en_i==1'b1 && empty_o==1'b0)
rd_ptr <= rd_ptr + 3'b1;
else
rd_ptr <= rd_ptr;
end
assign {rd_ptr_msb, rd_ptr_true} = rd_ptr;
always @(posedge clk_i or negedge rst_n_i) begin
if(!rst_n_i)
rd_data_o <= 32'b0;
else
if(rd_en_i==1'b1 && empty_o==1'b0)
rd_data_o <= FIFO[rd_ptr_true];
end
/*---------------------------------------------------\
-------------------- write data ------------------
\---------------------------------------------------*/
always @(posedge clk_i or negedge rst_n_i) begin
if(!rst_n_i)
wr_ptr <= 3'b0;
else if(wr_ptr == DATA_DEPTH-1 && wr_en_i==1'b1 && full_o==1'b0)
wr_ptr <= 3'b0;
else if(wr_en_i==1'b1 && full_o==1'b0)
wr_ptr <= wr_ptr + 3'b1;
else
wr_ptr <= wr_ptr;
end
assign {wr_ptr_msb, wr_ptr_true} = wr_ptr;
integer i;
always @(posedge clk_i or negedge rst_n_i) begin
if(!rst_n_i)
for(i=0;i<DATA_DEPTH;i=i+1)
FIFO[i] <= 32'b0;
else
if(wr_en_i==1'b1 && full_o==1'b0)
FIFO[wr_ptr_true] <= wr_data_i;
end
endmodule
同步FIFO设计参考了下列文章。
https://zhuanlan.zhihu.com/p/404518524