一文理解同步FIFO
文章目录
1、FIFO简介
FIFO即First In First Out,是一种先进先出数据存储、缓冲器,我们知道一般的存储器是用外部的读写地址来进行读写,而FIFO这种存储器的结构并不需要外部的读写地址而是通过自动的加一操作来控制读写,这也就决定了FIFO只能顺序的读写数据。其实FIFO的流程就像跑圈,写满的时候就等于写信号套了读信号一圈,而读空的时候,等于读信号又追上了写信号。
FIFO是一种先进先出数据缓存器,它与普通存储器的区别是没有外部读写地址线,使用起来非常简单,缺点是只能顺序读写,而不能随机读写。
2、使用场景
- 数据缓冲:也就是数据写入过快,并且间隔时间长,也就是突发写入。那么通过设置一定深度的FIFO,可以起到数据暂存的功能,且使得后续处理流程平滑。
- 时钟域的隔离:主要用异步FIFO。对于不同时钟域的数据传输,可以通过FIFO进行隔离,避免跨时钟域的数据传输带来的设计和约束上的复杂度。比如FIFO的一端是AD,另一端是PCI;AD的采集速率是16位100KSPS,每秒的数据量是1.6Mbps。而PCI总线的速度是33MHz,总线宽度是32位
- 用于不同宽度的数据接口。例如单片机是8位,DSP是16。
3、FIFO分类
同步FIFO,读和写应用同一个时钟。它的作用一般是做交互数据的一个缓冲,也就是说它的主要作用就是一个buffer。
异步FIFO,读写应用不同的时钟,它有两个主要的作用,一个是实现数据在不同时钟域进行传递,另一个作用就是实现不同数据宽度的数据接口。
4、 同步FIFO
同步fifo指的是写时钟和读时钟同频同相,也即同步fifo读写工作在同一个时钟下。
4.1 同步fifo的三部分:
- fifo写控制逻辑:产生写地址(决定往哪写,从0开始写)、写有效信号、写满(决定是否还能写)等信号;
- fifo读控制逻辑:产生读地址(决定从哪读,从0开始读)、读有效信号、读满(决定是否还能读)等信号;
- fifo的存储实体(reg、memory)
4.2 模块图
通过读写指针产生各自的读写地址,写指针指向下一个要写入的地址,读指针指向下一个要读的地址。有效写使能是写指针递增,有效的读使能是读指针递增。
双端口存储器(DPRAM)可以同步读取或异步读取。可以读、写独立进行。也可以同时读写
对于同步读操作:应该在数据在FIFO输出端有效前提供明确的读信号。
4.3 FIFO工作模式
◆工作流程
复位之后,在写时钟和状态信号的控制下,数据写入 FIFO 中。RAM 的写地址从 0 开始,每写一次数据写地址指针加一,指向下一个存储单元。当 FIFO 写满后,数据将不能再写入,否则数据会因覆盖而丢失。
FIFO 数据为非空、或满状态时,在读时钟和状态信号的控制下,可以将数据从 FIFO 中读出。RAM 的读地址从 0 开始,每读一次数据读地址指针加一,指向下一个存储单元。当 FIFO 读空后,就不能再读数据,否则读出的数据将是错误的。
FIFO 的存储结构为双口 RAM,所以允许读写同时进行。典型异步 FIFO 结构图如下所示。端口及内部信号将在代码编写时进行说明。
◆读写时刻
关于写时刻,只要 FIFO 中数据为非满状态,就可以进行写操作;如果 FIFO 为满状态,则禁止再写数据。
关于读时刻,只要 FIFO 中数据为非空状态,就可以进行读操作;如果 FIFO 为空状态,则禁止再读数据。
不管怎样,一段正常读写 FIFO 的时间段,如果读写同时进行,则要求写 FIFO 速率不能大于读速率。
◆读空状态
开始复位时,FIFO 没有数据,空状态信号是有效的。当 FIFO 中被写入数据后,空状态信号拉低无效。当读数据地址追赶上写地址,即读写地址都相等时,FIFO 为空状态。
因为是异步 FIFO,所以读写地址进行比较时,需要同步打拍逻辑,就需要耗费一定的时间。所以空状态的指示信号不是实时的,会有一定的延时。如果在这段延迟时间内又有新的数据写入 FIFO,就会出现空状态指示信号有效,但是 FIFO 中其实存在数据的现象。
严格来讲该空状态指示是错误的。但是产生空状态的意义在于防止读操作对空状态的 FIFO 进行数据读取。产生空状态信号时,实际 FIFO 中有数据,相当于提前判断了空状态信号,此时不再进行读 FIFO 数据操作也是安全的。所以,该设计从应用上来说是没有问题的。
◆写满状态
开始复位时,FIFO 没有数据,满信号是无效的。当 FIFO 中被写入数据后,此时读操作不进行或读速率相对较慢,只要写数据地址超过读数据地址一个 FIFO 深度时,便会产生满状态信号。此时写地址和读地址也是相等的,但是意义是不一样的。
此时经常使用多余的 1bit 分别当做读写地址的拓展位,来区分读写地址相同的时候,FIFO 的状态是空还是满状态。当读写地址与拓展位均相同的时候,表明读写数据的数量是一致的,则此时 FIFO 是空状态。如果读写地址相同,拓展位为相反数,表明写数据的数量已经超过读数据数量的一个 FIFO 深度了,此时 FIFO 是满状态。当然,此条件成立的前提是空状态禁止读操作、满状态禁止写操作。
同理,由于异步延迟逻辑的存在,满状态信号也不是实时的。但是也相当于提前判断了满状态信号,此时不再进行写 FIFO 操作也不会影响应用的正确性。
5、同步FIFO的Verilog实现
表1-1 同步FIFO端口
端口 | 属性 |
---|---|
clk | 时钟 |
rstn | 复位 |
wr_reg | 写请求 |
data_in | 写数据,本次为8bit |
rd_reg | 读请求 |
data_out | 读数据,同写数据,为8bit |
empty | fifo空标识 |
full | fifo满标识 |
wr_usedw | fifo当前数据量,本次使用8words ,指FIFO中还剩多少数据未被读取 |
almost_empty | 近空,usedw大于等于某个值时 |
almost_full | 近满,usedw小于某个值时 |
当然fifo内部还有写地址,读地址,写使能,读使能。
这里要注意请求信号和使能信号的关系,为保护fifo之前的数据不出错,请求信号进来了但是不满足fifo读写条件,如fifo空或者fifo满,前者fifo读使能会拉低,后者写使能会拉低。
至于usedw,在altera的IP表征着fifo中还有多少个数据,如果数据输入是8bit,且fifo中写入了100个8bit数据,则usedw则为100,同时也是判断almost_empty和almost_full的标志。
5.1 存储模块的实现
存储模块的实现可以用双口RAM资源,在仿真的时候直接使用reg模拟其行为即可;同时,需要实现RAM的深度可配置,因此要加入一个parameter控制深度。
在实现这个模块的过程中,要考虑清楚深度和地址位宽的关系,因为不同的深度对应的地址位宽也不同。
深度 | 地址范围 | 地址位宽 |
---|---|---|
64 | 0-63 | 6 |
32 | 0-31 | 5 |
16 | 0-15 | 4 |
8 | 0-7 | 3 |
4 | 0-3 | 2 |
2 | 0-1 | 1 |
这里就用 RAM来模拟
reg [DATA_WIDTH-1:0]mem[DATA_WIDTH-1:0];//深度8 数据位宽10
5.2 如何判断读空/写满
还记得刚刚说的吗?其实FIFO的工作流程就像跑圈,写满的时候就等于写信号套了读信号一圈,而读空的时候,等于读信号又追上了写信号。
如何判断读空:按照FIFO的工作原理,显而易见,当读空的时候,已经没有数据可供读取,所以wr_addr的地址和rd_addr的地址应该是一样的。
如何判断写满呢: 按照FIFO的工作原理,由于写信号套了读信号一圈,那么我们可以设个奇偶校验位(也可以理解为进位),通过奇偶校验来判断是否实现了“套圈”,奇偶校验位每跳转1次,就代表写信号或读信号跑完了一圈。注意哦,写信号是永远领先的,不可能落后读信号的。因此同时写满的时候wr_addr的地址和rd_addr的地址应该是一样的。
assign empty = ({wr_flag,wr_addr} == {rd_flag,rd_addr}) ? 1'b1:1'b0;
assign full = ((wr_addr == rd_addr) && (wr_flag != rd_flag)) ? 1'b1:1'b0;
5.3 如何计算FIFO当前数据量(还剩多少数据未被读取)
还是套圈理念,因为无论何时。写信号都是领先读信号的,所以FIFO中当前的数据量一定是**(写地址— 读地址)**来得到的。那么你可能会问当写地址大于读地址,毫无疑问,这肯定是成立的,但写地址小于读地址时还成立吗?
毫无疑问,当然成立,还记得定义的那个奇偶校验位吗?通过奇偶校验位,你就不会存在上面的问题。细品下述代码。
assign wr_usedw = {wr_flag,wr_addr} - {rd_flag,rd_addr};
5.4 源码
module synchronize_fifo #(
parameter [3:0] DATA_DEPTH = 4'd8, //深度 指FIFO中存了几个数
parameter [3:0] DATA_WIDTH = 4'd8, //宽度 每个数的位宽
parameter [1:0] ADDR_WIDTH = 2'd3, //FIFO地址(000——111) 2^ADDR_WIDTH = DATA_DEPTH
parameter [1:0] almost_empty_ref = 2'd2,
parameter [2:0] almost_full_ref = 3'd6
)(
input wire clk,
input wire rstn,
input wire wr_reg, //写请求
input wire rd_reg, //读请求
input wire [DATA_WIDTH-1:0] data_in,//数据写入
output reg [DATA_WIDTH-1:0] data_out,//数据读取
output wire empty,full,
output wire almost_empty,almost_full, //接近写满or读空
output wire [ADDR_WIDTH :0] wr_usedw // FIFO中还剩多少数据未被读取
);
wire wr_en,rd_en; //读使能,写使能
reg [{ADDR_WIDTH-1}:0] wr_addr,rd_addr; //读地址,写地址
reg [DATA_WIDTH-1:0]mem[DATA_DEPTH-1:0];//深度8 数据位宽10
reg wr_flag,rd_flag; //奇偶校验位 根据最高位地址判断是否读满or写空
//写地址
integer i;
assign wr_en = (!full) & wr_reg; //如果写满 则停止写操作
assign rd_en = (!empty) & rd_reg; //如果读空 则停止读操作
always @(posedge clk or negedge rstn)
if(!rstn)begin
wr_addr <= 3'd0;
wr_flag <= 1'd0;
end else if(wr_en)
{wr_flag,wr_addr} <= {wr_flag,wr_addr} +1'b1;
else
{wr_flag,wr_addr} <= {wr_flag,wr_addr};
//写数据
always @(posedge clk or negedge rstn)
if(!rstn)begin
for(i=0;i<8;i=i+1)
mem[i] <= 8'd0;
end else if(wr_en)
mem[wr_addr] <= data_in;
else
mem[wr_addr] <= mem[wr_addr];
//读地址
always @(posedge clk or negedge rstn)
if(!rstn)begin
rd_addr <= 3'd0;
rd_flag <= 1'd0;
end else if(rd_en)
{rd_flag,rd_addr} <= {rd_flag,rd_addr} + 1'b1;
else
{rd_flag,rd_addr} <= {rd_flag,rd_addr};
//读数据
always @(posedge clk or negedge rstn)
if(!rstn)
data_out <= 8'd0;
else if(rd_en)
data_out <= mem[rd_addr];
else
data_out <= data_out;
// 判断空or满
assign wr_usedw = {wr_flag,wr_addr} - {rd_flag,rd_addr};
assign almost_empty = (wr_usedw < almost_empty_ref) ? 1'b1:1'b0;
assign almost_full = (wr_usedw > almost_full_ref ) ? 1'b1:1'b0;
assign empty = ({wr_flag,wr_addr} == {rd_flag,rd_addr}) ? 1'b1:1'b0;
//assign full = ((wr_addr == rd_addr) && (wr_flag != rd_flag)) ? 1'b1:1'b0; //FIFO满时wr_addr = 000 rd_addr=000
assign full = ({~wr_flag,wr_addr} == {rd_flag,rd_addr}) ? 1'b1:1'b0; //FIFO满时wr_addr = 000 rd_addr=000
endmodule //synchronize_fifo
6、TestBench 仿真
这个testbench是先往FIFO中写数据,然后读数据,最后再写数据。 读出的数据是存入的3-10。
因此看wr_usedw 先从0-8, 再从8-0 ,然后又从0-8。
而wr_flag信号也跳变两次,说明写满两次。rd_flag信号跳变一次,说明读空一次。