Verilog实现FIFO

1. FIFO简介

FIFO是一种先进先出数据缓存器,它与普通存储器的区别是没有外部读写地址线,使用起来非常简单,缺点是只能顺序读写,而不能随机读写。

2. 使用场景

  1. 数据缓冲:也就是数据写入过快,并且间隔时间长,也就是突发写入。那么通过设置一定深度的FIFO,可以起到数据暂存的功能,且使得后续处理流程平滑。
  2. 时钟域的隔离:主要用异步FIFO。对于不同时钟域的数据传输,可以通过FIFO进行隔离,避免跨时钟域的数据传输带来的设计和约束上的复杂度。比如FIFO的一端是AD,另一端是PCI;AD的采集速率是16位100KSPS,每秒的数据量是1.6Mbps。而PCI总线的速度是33MHz,总线宽度是32位
  3. 用于不同宽度的数据接口。例如单片机是8位,DSP是16。

3. 分类

同步FIFO:指读时钟和写时钟是同一个时钟
异步FIFO:指读写时钟是不同的时钟。

4. FIFO的常见参数

FIFO的宽度:即FIFO一次读写操作的数据位;
FIFO的深度:指的是FIFO可以存储多少个N位的数据(如果宽度为N)。
满标志:FIFO已满或将要满时由FIFO的状态电路送出的一个信号,以阻止FIFO的写操作继续向FIFO中写数据而造成溢出(overflow)。
空标志:FIFO已空或将要空时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从FIFO中读出数据而造成无效数据的读出(underflow)。
读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。
写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据。

4. FIFO设计

  1. FIFO设计的关键是产生可靠的FIFO读写指针和生成FIFO空/满状态标志。
    当读写指针相等时,表明FIFO为空,这种情况发生在复位操作时;或者当读指针读出FIFO中最后一个字后,追赶上了写指针时,这时FIFO处于满的状态。
    为了区分到底是满状态还是空状态,可以采用以下方法:
    方法1:在指针中添加一个额外的位(extra bit),当写指针增加并越过最后一个FIFO地址时,就将写指针这个未用的MSB加1,其它位回零。对读指针也进行同样的操作。此时,对于深度为2^n的FIFO,需要的读/写指针位宽为(n+1)位,如对于深度为8的FIFO,需要采用4bit的计数器,0000~1000、1001~1111,MSB作为折回标志位,而低3位作为地址指针。
  • 如果两个指针的MSB不同,说明写指针比读指针多折回了一次;如r_addr=0000,而w_addr = 1000,为满。
  • 如果两个指针的MSB相同,则说明两个指针折回的次数相等。其余位相等,说明FIFO为空;
  1. 异步FIFO的设计还要注意跨时钟域同步问题
    将一个二进制的计数值从一个时钟域同步到另一个时钟域的时候很容易出现问题,因为采用二进制计数器时所有位都可能同时变化,在同一个时钟沿同步多个信号的变化会产生亚稳态问题。而使用格雷码只有一位变化,因此在两个时钟域间同步多个位不会产生问题。所以需要一个二进制到gray码的转换电路,将地址值转换为相应的gray码,然后将该gray码同步到另一个时钟域进行对比,作为空满状态的检测
  2. 使用gray带来了新的问题,如何判断空满
    对于“空”的判断:依然依据二者完全相等(包括MSB);
    对于“满”的判断:如下图,由于gray码除了MSB外,具有镜像对称的特点,当读指针指向7,写指针指向8时,除了MSB,其余位皆相同,不能说它为满。因此不能单纯的只检测最高位了,在gray码上判断为满必须同时满足以下3条:
  • wptr和同步过来的rptr的MSB不相等,因为wptr必须比rptr多折回一次。
  • wptr与rptr的次高位不相等,如上图位置7和位置15,转化为二进制对应的是0111和1111,MSB不同说明多折回一次,111相同代表同一位置。
  • 剩下的其余位完全相等

5. 同步FIFO实现

image.png

https://blog.csdn.net/HengZo/article/details/49683707
代码的核心部分主要是data_count,并且full 信号是当data_count == DATA_DEPTH时拉高,除了data_count之外,还可以同过判断r_ptr和w_ptr两个指针是否相等来判断空,满信号
ssign empty = (w_ptr == r_ptr) ? 1 : 0;
assign full = (w_ptr[2:0] == r_ptr[2:0] && (w_ptr[3] == ~r_ptr[3])) ? 1 : 0;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
module syn_fifo(
    clk,
    rst_n,
    data_in,
    w_en,
    full,
    data_out,
    r_en,
    empty);

parameter DATA_WIDTH = 8;
parameter DATA_DEPTH = 16;
parameter ADDR_WIDTH = 4;

input wire clk, rst_n;
input wire [DATA_WIDTH-1:0] data_in;
input wire w_en, r_en;

output wire empty, full;
output reg [DATA_WIDTH-1:0] data_out;

reg [ADDR_WIDTH   : 0] data_count;
reg [ADDR_WIDTH-1 : 0] w_ptr, r_ptr;
reg [DATA_WIDTH-1 : 0] mem[0 : DATA_DEPTH-1];

assign empty = (data_count == 'd0) ? 1 : 0;
assign full = (data_count == DATA_DEPTH) ? 1 : 0; //data_count == DATA_DEPTH

always @ (posedge clk or negedge rst_n) begin
    if (!rst_n)
        data_count <= 'd0;
    else if (w_en && r_en)
        data_count <= data_count;
    else if (!full && w_en)
        data_count <= data_count + 1'b1;
    else if (!empty && r_en)
        data_count <= data_count - 1'b1;
    else
        data_count <= data_count;
end

always @ (posedge clk  or negedge rst_n) begin
    if (!rst_n)
        mem[w_ptr] <= 'd0;
    else if (!full && w_en)
        mem[w_ptr] <= data_in;
end

always @ (posedge clk or negedge rst_n) begin
    if (!rst_n)
        w_ptr <= 'd0;
    else if (w_en && !full)
        w_ptr <= w_ptr + 1'b1;
    else
        w_ptr <= w_ptr;
end

always @ (posedge clk or negedge rst_n) begin
    if (!rst_n)
        r_ptr <= 'd0;
    else if (r_en && !empty)
        r_ptr <= r_ptr + 1'b1;
    else
        r_ptr <= r_ptr;
end

always @ (posedge clk  or negedge rst_n) begin
    if (!rst_n)
        data_out <= 'd0;
    else if (!empty && r_en)
        data_out <= mem[r_ptr];
end

endmodule

6. 异步FIFO实现

设计难点:

  1. 跨时钟域数据比较,需要用到同步器,减少亚稳态的传递
  2. 用到gray码,进一步减少亚稳态的产生
  3. gray码相等信号的比较 空:两个gray码相等 满:高两位相反,其余位相同。
  4. 指针计数需要比ADDR的位宽多一位,这一点和同步FIFO的设计是一样的。


    image.png

https://www.cnblogs.com/BitArt/archive/2013/04/10/3010073.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
module asyn_fifo(
    clk_w,
    clk_r,
    rst_n,
    r_en,
    w_en,
    data_in,
    data_out,
    full,
    empty
    );

input wire clk_r, clk_w, rst_n;

input wire r_en, w_en;

input wire [7:0] data_in;

output wire full, empty;
output reg [7:0] data_out;

parameter DATA_DEPTH = 8;
parameter DATA_WIDTH = 8;

parameter ADDR_WIDTH = 3;

reg [3:0] w_ptr, r_ptr;

reg [7:0] mem[DATA_DEPTH-1 : 0];


always @ (posedge clk_w or negedge rst_n) begin
    if (~rst_n)
        w_ptr <= 'd0;
    else if (w_en && !full)
        w_ptr <= w_ptr + 1'b1;
    else
        w_ptr <= w_ptr;
end
wire [3:0] w_ptr_gray, r_ptr_gray;

assign w_ptr_gray = w_ptr ^ (w_ptr >> 1);
assign r_ptr_gray = r_ptr ^ (r_ptr >> 1);

//
reg [ADDR_WIDTH:0] rd1_wp, rd2_wp;
always @ (posedge clk_r or negedge rst_n) begin
    if (!rst_n) begin
        rd1_wp <= 'd0;
        rd2_wp <= 'd0;
    end else begin
        rd1_wp <= w_ptr_gray;
        rd2_wp <= rd1_wp;
    end
end

assign empty = (rd2_wp == r_ptr_gray) ? 1 : 0;


always @ (posedge clk_r or negedge rst_n) begin
    if (~rst_n)
        r_ptr <= 'd0;
    else if (r_en && !empty)
        r_ptr <= r_ptr + 1'b1;
    else
        r_ptr <= r_ptr;
end
//wire [ADDR_WIDTH:0] r_ptr_gray;

assign r_ptr_gray = r_ptr ^ (r_ptr >> 1);

reg [ADDR_WIDTH:0] wd1_rp, wd2_rp;
always @ (posedge clk_w or negedge rst_n) begin
    if (~rst_n) begin
        wd1_rp <= 'd0;
        wd2_rp <= 'd0;
    end
    else begin
        wd1_rp <= r_ptr_gray;
        wd2_rp <= wd1_rp;
    end
end

assign full = ({(~wd2_rp[ADDR_WIDTH:ADDR_WIDTH-1]),wd2_rp[ADDR_WIDTH-2:0]} == w_ptr_gray) ? 1:0;

always @ (posedge clk_w or negedge rst_n) begin
    if (~rst_n)
        mem[w_ptr] <= 'd0;
    else
        mem[w_ptr] <= data_in;
end

always @ (posedge clk_r or negedge rst_n) begin
    if (~rst_n)
        data_out <= 'd0;
    else
        data_out <= mem[r_ptr];
end
endmodule

7. 关于异步FIFO最小深度的计算

FIFO仅在数据突发时才有效,不使用与连续的数据输出和输入。如果存在连续的数据流,那么所需要的FIFO大小因该是无限的。因此需要知道突发速率,突发大小,频率等,才能确定FIFO的深度。
最小深度的计算流程

  1. 确定读时钟fr和写时钟的频率fw, 一般情况fw>fr的
  2. 根据fr和fw计算读写一次数据的周期Tr 和Tw,根据T= 1/f
  3. 根据突发写长度的大小,计算这么多数据需要写多少时间 tw = Tw*len
  4. 根据写的时间tw计算读了多少数据 n = tw/Tr
  5. FIFO的最小深度等于 len-n

方案1:写时钟快于读时钟,写和读的过程中没有空闲周期


image.png

分析过程:
写时钟周期Tw = 1000/80 ns = 12.5ns;同理读时钟周期为20ns;
突发写长度为120个数据,写120个数据耗时120*12.5 = 1500ns;
1500ns时间内读出数据1500/20ns = 75个;
故最小FIFO深度为120 - 75 = 45;

方案2:写时钟频率大于读时钟频率,但在读写的过程中存在空闲周期

image.png

分析:
写时钟周期T_A = 12.5ns,读时钟周期为T_B = 20ns;
两个写时钟写一个数据,也就是写一个数据需要时间2T_A = 25ns,那么由于突发写数据个数为120个,写这么多数据需要时间12025ns = 3000ns;
4个读时钟周期读一个数据,因此读一个数据需要时间80ns,3000ns读了3000/80 = 37.5个数据(0.5不算一个数据,没读完整),约等于37个数据。
所以,FIFO的最小深度为120 - 37 = 83;
方案3:写时钟慢于读时钟,且读写过程中没有空闲周期

image.png

分析:
这种情况下永远也不会发生数据丢失的情况;
fifo的深度为1
方案4:写时钟频率小于读时钟频率,但读写过程中存在空闲周期

image.png

分析:
写时钟周期1000/30 ns = 100/3 ns;读时钟周期 20ns;
写一个数据需要2个时钟,也就是200/3 ns;读一个数据需要4个时钟,也就是80 ns;
写120个数据需要时间8000ns,这段时间内读出数据8000/80 = 100个;
因此,FIFO的最小深度为120 - 100 = 20;
方案5:读写时钟速率相同,且无空闲时钟

image.png

分析:
如果读写时钟之间没有相位差,则不需要FIFO就可以进行读写;
如果二者存在相位差,只需要FIFO的深度为1即可。
方案6:读写时钟频率一致,但在读写过程中存在空闲周期

image.png

分析:
两个时钟写一个数据,需要时间40ns;
4个时钟读一个数据,需要80ns;
由于突发长度为120,需要12040 = 4800ns写完;这段时间读出数据个数:4800/80 = 60;
所以,FIFO最小深度为120 - 60 = 60;
方案7:特定条件下,最坏情况分析FIFO最小深度

image.png

首先,从条件可知,写频率等于读频率;
其次,读写可以在如下限制下的任意时刻发生:

image.png

image.png

为了获得更安全的FIFO深度,我们需要考虑最坏的情况,以防数据丢失;
对于最坏的情况,写入和读取之间的数据速率之间的差异应该是最大的。 因此,对于写操作,应考虑最大数据速率,对于读操作,应考虑最小数据速率。从上表可以看出,最快的写数据速率应该为第4种情况,写操作在最小的时间内完成;
由于突发写长度为160,所以160个时钟写160个数据;
由于读速度为10个时钟读8个数据,因此一个数据需要10/8个时钟;
所以160个时钟读了160
8/10 = 128个数据;
所以FIFO的最小深度为160-128=32.
方案8:条件拐弯抹角的给出,需要自己提取关键信息

image.png

假如clkA = 25MHz,则CLKB = 100MHz;

TA= 40ns, TB = 10ns;
en_B = 100*40 = 4000ns;占空比为1/4;
我们认为B为写时钟,写使能时间为4000/4 = 1000ns,则突发写长度为1000/10 = 100个数据;
在1000ns内读出数据为1000/40 = 25个数据,所以FIFO最小深度为100 - 25 = 75

8. Vivado FIFO IP核使用

  1. 在IP Catalog中搜索FIFO,会出现各种各样的FIFO,一般选择FIFO generator。


    image.png

  2. 点击IP之后,会出现FIFO配置的一些选项,包括Basic Native ports, flag等


    image.png

  3. 在Basic中我们可以控制FIFO的接口形式和FIFO的类型
  • FIFO 的接口分为两类,一类是 Native 接口,这类接口使用比较简单,另一类是 AXI 协议接口,这类协议口线比较多,操作相对复杂。
  • FIFO 的类型主要区别:1.读写是否使用一个时钟 2.使用何种硬件资源
    其中区别1主要是通过common clk和 independent clk来确定,也就是同步FIFO和异步FIFO
    区别2硬件资源:分为3种。BRAM:即块RAM资源,这是FPGA内嵌的一种重要的专用RAM资源,可以在读写两端使用不同的数据宽度,可以使用 ECC (一种数据校验特性),支持 First-World Fall Through ,以及支持动态错误注入。;分布式RAM:Distributed RAM,即将FPGA中的LUT用作RAM,仅支持 First-World Fall Through 功能;专用FIFO,专用FIFO会提供很小的延迟。BRAM 是一种比较重要的资源,如果设计的 FIFO 对延时不敏感,可以使用分布式的 RAM 以节约 BRAM 资源。
  1. Stand FIFO 和 First Word Fall Through的区别
  • standard FIFO 读取数据时会延迟一个周期,也即会在使能信号拉高后延迟一个周期才有数据输出,而First word fall through会和使能信号同时输出。造成这种区别的原因在于FWFT模式下,第一个写入的数据将从RAM中提前读出到数据线。
  • FIFO 的复位使用的高电平复位,如果设计中系统复位信号是低电平有效的,那么不要忘记要将系统复位电平取反后再接入 FIFO 复位电平。一般我们在同步系统设计中使用异步复位。


    image.png

  1. status flag
  • 包括 almost Full/Empty 信号,这两个信号,顾名思义,就是在 FIFO 几乎要满或者几乎要空的情况下置起,所谓的“几乎“就是指还差一个数据满或者空
  • 这个页面上还提供握手选项,但一般我们在初级设计中不会需要 FIFO 具有这种“交互”特性,实质上 AXI 协议接口也会提供握手特性。
  • 第四个页面 Data Count,顾名思义就是提供一个信号来表示当前 FIFO 中的数据总数


    image.png

  1. 在顶层文件实例化IP
  • 在IP Source中打开Instation Template目录下的veo文件,里面就有实例化的例子


    image.png

9. FIFO IP使用注意事项

  1. 如果读写位宽不一样的情况,比如写位宽8, 读位宽32,那么当写入三次是, empty信号仍然为高电平,也就意味着是读不出数据的。
  2. FIFO的复位信号是高电平有效
  3. standard FIFO 和FWFT的区别就是读的时候需要延时一个周期和不需要延时
  4. output register:嵌入式输出寄存器可用于增加性能并向宏添加流水线寄存器,主要用于改善时序情况,但是只对Standard FIFO模式有用,添加output register, 读延时会增加1个周期

参考链接:
https://blog.csdn.net/Reborn_Lee/article/details/100127937
[https://hardwaregeeksblog.files.wordpress.com/2016/12/fifodepthcalculationmadeeasy2.pdf]
https://zhuanlan.zhihu.com/p/47847664