FIFO学习:
1、参考网站 :https://blog.csdn.net/alangaixiaoxiao?type=blog
2、FIFO https://blog.csdn.net/alangaixiaoxiao/article/details/81432144?spm=1001.2014.3001.5502
3、Verilog 有符号数与无符号数运算 https://blog.csdn.net/alangaixiaoxiao/article/details/106209193?spm=1001.2014.3001.5502
4、HLS实现YOLO神经网络系列(一)https://blog.csdn.net/alangaixiaoxiao/article/details/104634089?spm=1001.2014.3001.5502
5、yolov3从头实现(五)– yolov3网络块https://blog.csdn.net/m0_43609475/article/details/107984633
一、FIFO简介
FIFO是英文First In First Out 的缩写,是一种先进先出的数据缓存器,它与普通存储器的区别是没有外部读写地址线,这样使用起来非常简单,但缺点就是只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。
用途1:
异步FIFO读写分别采用相互异步的不同时钟。在现代集成电路芯片中,随着设计规模的不断扩大,一个系统中往往含有数个时钟,多时钟域带来的一个问题就是,如何设计异步时钟之间的接口电路。异步FIFO是这个问题的一种简便、快捷的解决方案,使用异步FIFO可以在两个不同时钟系统之间快速而方便地传输实时数据。
用途2:
对于不同宽度的数据接口也可以用FIFO,例如单片机位8位数据输出,而DSP可能是16位数据输入,在单片机与DSP连接时就可以使用FIFO来达到数据匹配的目的。
二、分类
同步FIFO是指读时钟和写时钟为同一个时钟,在时钟沿来临时同时发生读写操作;
异步FIFO是指读写时钟不一致,读写时钟是互相独立的。
三、FIFO的常见参数
- FIFO的宽度:即FIFO一次读写操作的数据位;
- FIFO的深度:指的是FIFO可以存储多少个N位的数据(如果宽度为N)。
- 满标志:FIFO已满或将要满时由FIFO的状态电路送出的一个信号,以阻止FIFO的写操作继续向FIFO中写数据而造成溢出(overflow)。
- 空标志:FIFO已空或将要空时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从FIFO中读出数据而造成无效数据的读出(underflow)。
- 读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。
- 写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据。
- 读写指针的工作原理
读指针:总是指向下一个将要被写入的单元,复位时,指向第1个单元(编号为0)。
写指针:总是指向当前要被读出的数据,复位时,指向第1个单元(编号为0)
- FIFO的“空”/“满”检测
FIFO设计的关键:产生可靠的FIFO读写指针和生成FIFO“空”/“满”状态标志。
当读写指针相等时,表明FIFO为空,这种情况发生在复位操作时,或者当读指针读出FIFO中最后一个字后,追赶上了写指针时
当读写指针再次相等时,表明FIFO为满,这种情况发生在,当写指针转了一圈,折回来(wrapped around)又追上了读指针
为了区分到底是满状态还是空状态,可以采用以下方法:
方法1:在指针中添加一个额外的位(extra bit),当写指针增加并越过最后一个FIFO地址时,就将写指针这个未用的MSB加1,其它位回零。对读指针也进行同样的操作。此时,对于深度为2n的FIFO,需要的读/写指针位宽为(n+1)位,如对于深度为8的FIFO,需要采用4bit的计数器,0000~1000、1001~1111,MSB作为折回标志位,而低3位作为地址指针。
- 如果两个指针的MSB不同,说明写指针比读指针多折回了一次;如r_addr=0000,而w_addr = 1000,为满。
- 如果两个指针的MSB相同,则说明两个指针折回的次数相等。其余位相等,说明FIFO为空;
tb测试文件
module tb_fifo_top;
//inputs
reg clk;
reg rst;
reg [7:0] din;
reg wr_en;
reg rd_en;
//outputs
wire [7:0] dout;
wire full;
wire almost_full;
wire empty;
wire almost_empty;
wire [3:0] data_count;
wire prog_full;
wire prog_empty;
// Instantiate the Unit Under Test (UUT)
fifo_top uut (
.clk(clk),
.rst(rst),
.din(din),
.wr_en(wr_en),
.rd_en(rd_en),
.dout(dout),
.full(full),
.almost_full(almost_full),
.empty(empty),
.almost_empty(almost_empty),
.data_count(data_count),
.prog_full(prog_full),
.prog_empty(prog_empty) );
initial begin
// Initialize Inputs
clk = 0;
rst = 0;
din = 0;
wr_en = 0;
rd_en = 0;
// Wait 100 ns for global reset to finish
#100;
// Add stimulus here
end
always #10 clk = ~clk ;
endmodule
仿真分析:
在 100 ns 时刻后,empty 信号 和 almost_empty 信号因为 FIFO 为空,所以为高电平有效。但我们可以观察到 full 以及 almost full 信号确仍然保持高电平,实际上此时,FIFO 显然没有满,所以这两个信号是不正确的。他们需要一段时间,也就是直到 260 ns 时刻,恢复到正常的低电平,这说明这两个状态信号在复位后需要一段时间才能恢复正常。
接下来我们依次向 FIFO 写入 16 个数据,再依次读取。FIFO 的深度为 16。我们通过编写 testbench,连续产生 16 次 wr_en 写有效信号,并每次 wr_en 写有效时,写入数据加一。延迟一段时候后,再连续产生 16 次 rd_en 读有效信号,将之前写入的数据全数读取出来。
首先来看三个空状态信号。
第一个空状态信号,在第一个 wr_en 信号结束后的第一个时钟上升沿置低。almost_empty 信号在第二个写使能信号后的时钟上升沿置低,代表此时 FIFO 中已经有超过一个数据。而 prog_empty 我们自定义的“几乎”空信号,在写入三个数据后置低,因为我们设置的自定义阈值是 2,FIFO 中有超过两个数据后信号不再有效。不过我们可以观察到可编程信号和原生信号相比有一个周期的延时,如果对周期敏感的应用应当注意到这个小小的周期时延。
full 满信号和 empty 信号的特性完全相同,我们来看下 full 信号的置高与置低的过程。在复位一段时间后,full 信号恢复正常。当写入第 14 个数据后,prog_full 信号置起。写入第15 个数据后,在写有效信号高电平之后的第一个上升沿,almost_full 信号置起。最后是 full 信号,prog_full 信号仍然有一个时钟的延迟。
FIFO 提供了一组接口用于显示当前 FIFO 中的数据个数。在第一个数据写入后,data_count 就变化为 1,之后每写入一个数据增长 1 。在某些情况下,我们需要记录写入 FIFO 的数据数量,比如我们需要在 FIFO 中缓存一帧 16 byte 长的数据,我们的 FIFO 出于多帧数据缓冲的需求,深度肯定远大于一帧数据的长度,那么我们显然无法依靠空,满信号进行判断。一方面可以自己使用逻辑对写使能进行计数,或者我们可以使用 FIFO 核提供的计数功能,该功能我没有验证过,但在同步的情况下,数据计数应该是完全准确的。
值得注意的是在写入第 16 个数据后,计数输出变为 0 ,这是个小失误,因为我们的四位计数值显然在记录 16 时溢出了,因此我们一般需要 log2(深度) + 1 位宽的计数值。
读延迟与 First Word Fall Through 特性
第一行是读取的数据,第二行是读使能信号,最后一行是时钟。我们从第二个读使能信号来看会比较清晰,因为数据通道的复位值是 0x0,但第一个写入的数据也是 0x0,所以第一个读使能信号看不太清晰。第二个读使能信号在黄线处的时钟上升沿置起,直到下一个时钟上升沿,数据 0x01 才会出现在数据线上,这就是读信号时的一个时钟延迟,一个时钟的长度是相对于读使能有效的第一个时钟上升沿而言。
1个时钟的固定延迟对于简单的同步系统来说问题不大,只要在读信号有效之后固定延迟一个周期再读取或者使用读数据线上的数据即可。比如使用状态机时,在上一个状态置起读有效,等到下一个状态再读取数据。
那么有没有办法消除这个延迟,这就又要说说我们上篇中配置 ip 核时见到的 First Word Fall Through 特性。
该特性的主要功能是,哪怕你还没送出读使能信号,我就把FIFO 中下一个数据准备到数据线上。比如 FIFO 中有两个数据 0x1,0x2,当你什么都还没做时,读数据线上已经是 0x1了,当你读取一个数据后,数据线上就变成了 0x2 ——下一个等待读取的数据。但注意到为了实现这个特性,FIFO 真正的深度已经扩展为 18 位。(那么计满,计空是按照 18 位还是 16 位呢?)。
当我们写溢出会怎样,是抛弃最早的数据还是无视最新的数据?
FIFO 使用中最需要注意的问题在于溢出,我们需要借助空/满信号来判定 FIFO 的状态,尽量避免 FIFO 的读写溢出。但如果我们写溢出了会怎样?
我们向深度为 16 (开启 First Word Fall Through 特性后实际深度为 18 )的FIFO ,一口气写 20 个数据和使能信号。当我们读取 20 个信号时,我们是会读到前 20 个,还是后 20个数据?
答案是前 18 个数据,读取到的最后一个数据是 0x66 ,在 0x66 之后的两个写入数据 0x00 和 0x78 并没有进入 FIFO。
所以结论是 FIFO 在写满之后,会保证之前写入的数据,而拒绝新写入的数据。另外,能够容纳的数据并不是名义上的 FIFO 深度,而是 IP 核配置界面显示的实际深度,本例中是 18 。
数据因为 FIFO 写满而丢失,很有可能造成严重的系统问题,需要认真选取合适的 FIFO 深度。
当我们的 FIFO 没有数据,但头铁硬要读取会怎样?
在写入 16 个数据后,我们闲来无事,决定读取个 20 次数据。
你读取到了 16 个数据,并没有什么特别的事情发生。
当我们同时读写会怎样?
当 FIFO 没有数据时,在开启 Fall Through 的情况下,同时读取和写入数据。
可以发现,这种情况下存在问题:
在前三个读使能周期,读取到的都是 FIFO 中的初始值 0x00,直到第 3 个读使能信号,才读取到 FIFO 中的第一个数据 0x80,最终 16 个读使能信号实际上只读到了 14 个有效数据。
但如果先写入 3 个数据后,再同时读写
此时就不会出现问题,所以开启 Fall Through 的情况下,前 2 个周期是无法读取数据的,但在之后的时钟中,同时读取也是不会有问题的。
没有开启Fall Through 的情况下,第一个读使能会因为一个周期的读延迟无法读到数据。也就是说会少读取一个数据。