FPGA入门笔记013——嵌入式块RAM使用之FIFO
1、FIFO概述
FIFO(First In First Out),即先进先出。FPGA 或者 ASIC 中使用到的 FIFO 一般指的是对数据的存储具有先进先出特性的一个缓存器,常被用于数据的缓存或者高速异步数据的交互。它与普通存储器的区别是没有外部读写地址线,这样使用起来相对简单,但缺点就是只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加 1 完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。
2、FIFO相关知识
2.1、FIFO结构
FIFO 从大的情况来分,有两类结构:单时钟 FIFO(SCFIFO)和双时钟 FIFO(DCFIFO), 其中双时钟 FIFO 又可以分为普通双时钟( DCFIFO )和混合宽度双时钟 FIFO (DCFIFO_MIXED_WIDTHS)。
图 1 为单时钟 FIFO 和双时钟 FIFO 的符号图。从图中可以看到,单时钟 FIFO 具有一个独立的时钟端口 Clock,因此所有输入信号的读取都是在 Clock 的上升沿进行的,所有 输出信号的变化也是在 Clock 信号的上升沿的控制下进行的,即单时钟 FIFO 的所有输入输出信号都是同步于 Clock 信号的。而在双时钟 FIFO 结构中,写端口和读端口分别有独立的时钟,所有与写相关的信号都是同步于写时钟 wrclk 的,所有与读相关的信号都是同步于读时钟 rdclk 的。在双时钟 FIFO 的符号图中,位于上部分的为与写相关的所有信号,位于中间部分的为与读相关的所有信号,位于下部的为异步清零信号。
2.2、单时钟 FIFO应用场景
单时钟 FIFO 常用于片内数据交互。例如,在 FPGA 的控制下从外部传感器读取到的一 连串传感器数据,首先被写入 FIFO 中,然后再以 UART 串口波特率将数据依次发送出去。 由于传感器的单次读取数据可能很快,但并不是时刻都需要采集数据,例如某传感器使用 SPI 接口的协议,FPGA 以 2M 的 SPI 数据速率从该传感器中读取 20 个数据,然后以 9600 的波特率通过串口发送出去。因为 2M 的数据速率远高于串口 9600 的波特率,因此需要将 从传感器中采集到的数据首先用 FIFO 缓存起来,然后再以串口的数据速率缓慢发送出去。 这里,由于传感器数据的读取和串口数据的发送都是可以同步于同一个时钟的,因此可以使 用单时钟结构的 FIFO 来实现此功能。
2.3、FIFO常见参数
- FIFO 的宽度:即 FIFO 一次读写操作的数据位;
- FIFO 的深度:指的是 FIFO 可以存储多少个 N 位的数据(如果宽度为 N)。
- 满标志:FIFO 已满或将要满时由 FIFO 的状态电路送出的一个信号,以阻止 FIFO 的写操作继续向 FIFO 中写数据而造成溢出。
- 空标志:FIFO 已空或将要空时由 FIFO 的状态电路送出的一个信号,以阻止 FIFO 的读操作继续从 FIFO 中读出数据而造成无效数据的读出。
- 读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。
- 写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据。
3、IP 核使用之 FIFO
3.1、单时钟 FIFO 实现
(1)新建一个以名为 fifo 的工程保存在 prj 下,然后单击 Tools→MegaWizard Plug-In Manager 按前面的方法搜索出 FIFO,并将输出目录修改为工程文件夹下的 ip 文件夹,并以 myscfifo 命名保存,单击 Next。
(2)这里首先创建一个单时钟 FIFO 也就是 SCFIFO,数据位宽为 16bits,数据深度为 256words。这里修改数据位宽及深度,观察界面左下角的资源使用情况,如下图所示。
(3)在 IP 核 FIFO 中提供了很多接口,如下图,这里仅选择满、空、接近满、接近空以及异步清零。Almost_full:当 usedw 大于等于设置的值时该信号为高电平,是 full 的提前提示信号。Almost_empty:当 usedw 小于等于设置的值时该信号为低电平,是 empty 的提前 提示信号
(4)读操作请求确认信号,有两种模式:普通模式与前显(show-ahead)模式,如下图所示。
(5)选择器件优化方式是面积优先还是速度优先,更换可以看到资源占用率是不一样的, 选择速度优先必然使用资源会上升,如下图所示。
然后确定配置信息后即可点击 Finish 完成配置。将生成的 IP 核加入工程,即可在工程 下看到加入的 IP 核文件并设置为顶层文件。
3.2、单时钟 FIFO 测试
(1)为了测试仿真编写测试激励文件,新建 myscfifo_tb.v 文件并输入以下内容再次进行分析和综合直至没有错误以及警告,保存到 testbench 文件夹下。除了实现例化需要仿真的文件以及时钟创建,还需实现写入 256 个数据后并依次读出。
`timescale 1ns/1ns
`define clk_period 20
module myscfifo_tb;
reg Clk;
reg[15:0] data;
reg rdreq;
reg sclr;
reg wrreq;
wire almost_empty;
wire almost_full;
wire empty;
wire full;
wire[15:0] q;
wire[7:0] usedw;
integer i;
myscfifo myscfifo(
.clock(Clk)
.data(data)
.rdreq(rdreq)
.sclr(sclr)
.wrreq(wrreq)
.almost_empty(almost_empty)
.almost_full(almost_full)
.empty(empty)
.full(full)
.q(q)
.usedw(usedw)
);
initial Clk = 1;
always#(`clk_period/2) Clk = ~Clk;
initial begin
//初始化
data = 0;
rdreq = 0;
wrreq = 0;
sclr = 0;
#(`clk_period * 20 + 1);
for(i = 0;i < 256;i = i + 1)begin
wrreq = 1;
data = i;
#`clk_period;
end
wrreq = 0;
#(`clk_period * 20);
for(i = 0;i < 256;i = i + 1)begin
rdreq = 1;
#`clk_period;
end
rdreq = 0;
$stop;
end
endmodule
(2)设置好仿真脚本后进行功能仿真,可以看到如下图所示的波形文件。可以看出 实现了数据的写入与读出。
(3)放大写数据部分,可以看到每当时钟上升沿时均会写入对应的数据,且 empty 信号在写入一个数据后即置低,almost_empty 信号在写入两个数据后即置低。符合前面有关的配置,如下图所示。
(4)在数据持续写入 254 个后 almost_full 信号置高,在写入 256 个后 full 信号置高。在 读取使能信号有效后,读出一个数据后 full 信号置低同样读出 2 个数据后 almost_full 信号置 低,如下图所示。
(5)需指出这里 usedw 在写入 255 个数据后再写入第 256 个数据之所以变为 0,是因为 在前面设置中此数据的位宽是 8 位,因此最大计数到 255。这里可以同时判断 full 的状态来 判断实际剩余数据个数。同时选中 full 与 usedw,右键选择 Combine Signals,在弹出的界面 中将合并后的信号命名为 usedw2,如下图所示。
(6)这样重启仿真即可看到下图所示的波形,这时 usedw2 即为真实 FIFO 中数据个数。
(7)在读取 254 个信号后 almost_empty 信号置高,继续读取一个数据后 empty 信号置高。 如下图所示。
这样就完成了一个单时钟 FIFO 的配置以及仿真。
3.3、双时钟 FIFO 实现
(1)现在再新建一个 FIFO 并以 mydcfifo 保存。配置双时钟 FIFO 也就是 DCFIFO,且写入读取宽度分别为 16、8bits,如下图所示。
(2)输入信号接口选择 empty 与 usedw,输出信号端口选择 full 与 usedw,这里可根据实际需求情况进行选择,如下图所示。
(3)下图显示为 DCFIFO 的读请求信号 rdreq 的作为和存储器的实现方式。选则第一种普通方式则 rdreq 信号作为读请求信号,当该信号有效时 FIFO 中的控制逻辑从存储器中读取一个数据输出到 q 端。如果选中 Show-ahead 方式,则 rdreq 信号作为读应答信号,即 rdreq 还没有有效时,q 端口上已经输出了一个有效的数据,rdreq 信号有效的时 候则相当于通知 FIFO 内部的控制逻辑 q 端口上的数据已经被读取,则 FIFO 内部的逻辑会 从 RAM 中再取出一个新的数据,在下一个时钟周期输出到 q 端口上。该模式在实际中应用也非常的普遍,因为 q 端口上的数据 与 rdreq 同时有效,没有读潜伏期。
(4)下图为 FIFO 的溢出控制选项,可以设置忽略满溢出,忽略空溢出等。关于该设置的具体特性,可以通过阅读 IP 手册加仿真的方式验证。
然后预览汇总信息,确认后点击 Finish 来完成此 IP 的配置并将其加入工程中设置为顶层文件。
3.4、双时钟 FIFO 测试
为了测试仿真编写测试激励文件,新建 mydcfifo_tb.v 文件并输入以下内容再次进行分析和综合直至没有错误以及警告,保存到 testbench 文件夹下。这里首先定义产生了不同的读写时钟频率且读时钟频率为写时钟频率的两倍,然后写使能即每当写时钟到来时就依次写入数据,写入 256 个数据后写失能,开始读使能即每当读时钟到来时依次读出数据,读出 512 个数据后读失能。这里由于写的数据为 16 位 256 个,因此也就相当于读出 8 位 512 个。
在写入数据时人为加十进制数 1024,这样写入数据实际就是两个八进制数 0400、0401、 0402(MSB:先读低位,再读高位),读取时就是 2534\2544\2554。且读取 empty 正常显示。
`timescale 1ns/1ns
`define wrclk_period 20
`define rdclk_period 10
module mydcfifo_tb;
reg[15:0] data;
reg rdclk;
reg rdreq;
reg wrclk;
reg wrreq;
wire[7:0] q;
wire rdempty;
wire[8:0] rdusedw;
wire wrfull;
wire[7:0] wrusedw;
integer i;
mydcfifo mydcfifo(
.data(data),
.rdclk(rdclk),
.rdreq(rdreq),
.wrclk(wrclk),
.wrreq(wrreq),
.q(q),
.rdempty(rdempty),
.rdusedw(rdusedw),
.wrfull(wrfull),
.wrusedw(wrusedw)
);
initial wrclk = 1;
always#(`wrclk_period/2) wrclk = ~wrclk;
initial rdclk = 1;
always#(`rdclk_period/2) rdclk = ~rdclk;
initial begin
//初始化
data = 0;
rdreq = 0;
wrreq = 0;
#(`wrclk_period * 20 + 1);
for(i = 0; i < 256; i = i + 1)begin
wrreq = 1;
data = i + 1024;
#`wrclk_period;
end
wrreq = 0;
#(`rdclk_period * 20);
for(i = 0; i < 512; i = i + 1)begin
rdreq = 1;
#`wrclk_period;
end
rdreq = 0;
#(`rdclk_period * 20);
$stop;
end
endmodule
设置好仿真脚本后进行功能仿真,可以看到如图 2 和图 3 所示的波形。可以看出:向 fifo 中写入的数据为 16 位,每写入一个 16 位数据,当其被读出时由于限定其位宽 是 8 位,所以只能分为两个 8 位的数据读出,相应读出时地址每次加 2 而不是加 1,与前面解释一致 。
从图 3 可以看到,如果写入数据是 16’h04ff 分成两个 8 位十六进制数即为 8’h04、 8’hff,低位在前,符合设计。同样可以分析出读出数据规则。
通过对以上的波形可以看出 FIFO 的工作状态正常。