文章目录
- 一、什么是FIFO
- 二、为什么要用FIFO
- 三、什么时候用FIFO
- 四、FIFO分类
- 五、同步FIFO
-
- [1. 同步FIFO电路框图](#1. 同步FIFO电路框图)
- [2. 同步FIFO空满判断](#2. 同步FIFO空满判断)
- [3. 同步FIFO设计代码](#3. 同步FIFO设计代码)
- [4. 同步FIFO仿真结果](#4. 同步FIFO仿真结果)
- 六、异步FIFO
-
- 1、异步FIFO的电路框图
- [2 、亚稳态](#2 、亚稳态)
- 3、打两拍
- 4、格雷码
- 5、如何判断异步FIFO的空满
- 6、如何选择FIFO深度
- 7、异步FIFO的设计代码
- [8、 仿真](#8、 仿真)
- [七、FIFO应用实例 (ADC)](#七、FIFO应用实例 (ADC))
-
- 一、实验前提
- [二、FIFO IP调用](#二、FIFO IP调用)
- 原文链接
一、什么是FIFO
FIFO 是 First In First Out 的简称。
是指在FPGA内部用逻辑资源实现的能对数据的存储具有先进先出特性的一种缓存器。
FIFO 与 FPGA 内部的 RAM 和 ROM 的区别是 FIFO 没有外部读写地址线,采取顺序写入数据,顺序读出数据的方式,其数据地址由内部读写指针自动加1完成。
FIFO 使用起来简单方便,由此带来的缺点是不能像 RAM 和 ROM 那样可以由地址线决定读取或写入某个指定的地址。
二、为什么要用FIFO
FPGA内的程序实现的电路实际上是由一个个独立的功能模块组成的 ,各个模块又通过相关信号关联在一起,当存在模块间处理数据速度不同(有快有慢)时,处理得快的模块就需要等一等处理得慢的模块,这个等待其实就是缓存的实现。
我们可以采用FIFO来解决数据的缓存。
打个比方,就像水龙头放水慢(输入慢),但我们人提水的时候是一次处理一桶水(输出快),所以需要一个水桶作为缓存,等存满一桶水,再一次被人提走。
又或者像我们人喝水,一次接一杯水(输入快), 渴的时候喝两口(输出慢)。这里,杯子作为缓存。
另外,在现代集成电路芯片中,随着设计规模的不断扩大,一个系统中往往含有数个时钟,此时,异步时钟之间的接口电路的设计将成为关键。
而使用异步FIFO可以在两个不同时钟系统之间快速而方便地传输实时数据。
三、什么时候用FIFO
- 数据缓存、
- 协议处理、
- 串并转换、
- 跨时钟域数据处理。
四、FIFO分类
FIFO根据读写时钟是否为同一时钟分为同步FIFO和异步FIFO。
-
同步FIFO是指读时钟和写时钟为同一个时钟,在时钟沿来临时可同时发生读写操作。
-
异步FIFO是指读写时钟不一致,读写时钟是互相独立的2个时钟。
-
同步FIFO在实际应用中比较少见,常用的是异步FIFO,但基于学习的目的,下文对两种FIFO都进行讲解。
五、同步FIFO
1. 同步FIFO电路框图
简单来说,同步FIFO其实就是一个双口RAM加上两个读写控制模块。FIFO的常见参数和信号如下:
- FIFO的宽度:即FIFO一次读写操作的数据位;
- FIFO的深度:指的是FIFO可以存储多少个N位的数据(如果宽度为N)。
- 满标志:FIFO已满或将要满时由FIFO的状态电路送出的一个信号,以阻止FIFO的写操作继续向FIFO中写数据而造成溢出(overflow)。
- 空标志:FIFO已空或将要空时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从FIFO中读出数据而造成无效数据的读出(underflow)。
- 读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。(同步FIFO 读写只有一个时钟)
- 写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据。(异步FIFO读写时钟分开)
- 读指针:总是指向下一个将要被写入的单元,写完后自动加1,复位时,指向第1个单元(编号为0)。
- 写指针:总是指向下一个将要被读出的单元,读完后自动加1,复位时,指向第1个单元(编号为0)
其实可以把FIFO比作一个单向行驶的隧道,隧道两端都有一个门进行控制,FIFO宽度就是这个隧道单向有几个车道,FIFO的深度就是一个车道能容纳多少辆车,当隧道内停满车辆时,这就是FIFO的写满状态,当隧道内没有一辆车时,这便是FIFO的读空状态。
2. 同步FIFO空满判断
FIFO 的设计原则是任何时候都不能向满FIFO中写入数据 (写溢出),任何时候都不能从空FIFO中读取数据(读溢出)。
FIFO 设计的核心是空满判断。FIFO设置读,写地址指针,FIFO初始化的时候 读指针和写指针都指向地址为0的位置, 当往FIFO里面每写一个数据,写地址指针自动加1指向下一个要写入的地址。
当从FIFO里面每读一个数据,读地址指针自动加1指向下一个要读出的地址,最后通过比较读地址指针和写地址指针的大小来确定空满状态。
当读地址指针追上写地址指针,写地址指针跟读地址指针相等,此时FIFO是读空状态。
当写地址指针再次追上读地址指针,写指针跟读地址指针再次相等的时候,此时FIFO是写满状态。
可以设置一个计数器,当写使能有效的时候计数器加一;
当读使能有效的时候,计数器减一,将计数器与FIFO的size进行比较来判断FIFO的空满状态。
这种方法设计比较简单,但是需要的额外的计数器,就会产生额外的资源,而且当FIFO比较大时,会降低FIFO最终可以达到的速度。
3. 同步FIFO设计代码
同步FIFO基本接口:
信号 | 描述 |
---|---|
clk | 系统时钟 |
rstn | 系统复位信号 |
wr_en | 写使能端 |
wr_data | FIFO写数据 |
fifo_full | FIFO的满标志位 |
rd_en | 读使能端 |
rd_data | FIFO读数据 |
fifo_empty | FIFO的空标志位 |
同步FIFO实现代码如下:
module sync_fifo#(parameter BUF_SIZE=8, BUF_WIDTH=8) (
//FIFO的数据位宽默认为8bit
//FIFO深度默认为8
input i_clk,//输入时钟
input i_rst,//复位信号
input i_w_en,//写使能信号
input i_r_en,//读使能信号
input [BUF_WIDTH-1:0] i_data,//写入数据
output reg [BUF_WIDTH-1:0] o_data,//读出数据
output o_buf_empty,//FIFO空标志
output o_buf_full );//FIFO满标志
reg [3:0] fifo_cnt; //记录FIFO数据个数
reg [$clog2(BUF_SIZE)-1:0] r_ptr,w_ptr; //数据指针为3位宽度,0-7索引,8个数据深度,循环指针0-7-0-7
reg [BUF_WIDTH-1:0] buf_mem[0:BUF_SIZE-1]; //定义FIFO大小
//判断空满
assign o_buf_empty=(fifo_cnt==4'd0)?1'b1:1'b0;
assign o_buf_full=(fifo_cnt==4'd8)?1'b1:1'b0;
always@(posedge i_clk or posedge i_rst) //用于修改计数器
begin
if(i_rst)
fifo_cnt<=4'd0;
else if((!o_buf_full&&i_w_en)&&(!o_buf_empty&&i_r_en)) //同时读写,计数器不变
fifo_cnt<=fifo_cnt;
else if(!o_buf_full&&i_w_en) //写数据,计数器加1
fifo_cnt<=fifo_cnt+1;
else if(!o_buf_empty&&i_r_en) //读数据,计数器减1
fifo_cnt<=fifo_cnt-1;
else
fifo_cnt <= fifo_cnt; //其他情况,计数器不变
end
always@(posedge i_clk or posedge i_rst) //读数据
begin
if(i_rst)
o_data<=8'd0;
else if(!o_buf_empty&&i_r_en)
o_data<=buf_mem[r_ptr];
end
always@(posedge i_clk) //写数据
begin
if(!o_buf_full&&i_w_en)
buf_mem[w_ptr]<=i_data;
end
always@(posedge i_clk or posedge i_rst) //读写地址指针变化
begin
if(i_rst) begin
w_ptr <= 0;
r_ptr <= 0;
end
else begin
if(!o_buf_full&&i_w_en) // 写数据,地址加1,溢出后自动回到0开始
w_ptr <= w_ptr + 1;
if(!o_buf_empty&&i_r_en) // 读数据,地址加1,溢出后自动回到0开始
r_ptr <= r_ptr + 1;
end
end
endmodule
4. 同步FIFO仿真结果
同步FIFO仿真测试文件
`timescale 1ns/1ns
module sync_fifo_tb;
reg i_clk,i_rst;
reg i_w_en,i_r_en;
reg [7:0] i_data;
wire [7:0] o_data;
wire o_buf_empty,o_buf_full;
sync_fifo dut(
.i_clk(i_clk),
.i_rst(i_rst),
.i_data(i_data),
.i_w_en(i_w_en),
.i_r_en(i_r_en),
.o_buf_empty(o_buf_empty),
.o_buf_full(o_buf_full),
.o_data(o_data)
);
initial begin
#30;
forever #10 i_clk = ~i_clk; //时钟
end
reg [7:0] r_data=8'd0;
initial begin
i_clk=1'b0;
i_rst=1'b0;
i_w_en=1'b0;
i_r_en=1'b0;
i_data=8'd0;
#5 i_rst=1'b1;
#10 i_rst=1'b0;
push(1);
fork //同时执行push和pop
push(2);
pop(r_data);
join
push(3);
push(4);
push(5);
push(6);
push(7);
push(8);
push(9);
push(10);
push(11);
push(12);
push(13);
push(14);
push(15);
push(16);
push(17);
pop(r_data);
push(18);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
push(19);
pop(r_data);
push(20);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
push(21);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
#100 $stop;
end
task push (input [7:0] data);
if(o_buf_full)
$display("Cannot push %d: Buffer Full",data);
else begin
$display("Push",,data);
i_data=data;
i_w_en=1;
@(posedge i_clk) #4 i_w_en= 0; //时钟上升沿后4ns,写使能清零
end
endtask
task pop(output[7:0] data);
if(o_buf_empty)
$display("Cannot Pop: Buffer Empty");
else begin
i_r_en=1;
@(posedge i_clk) #4 i_r_en= 0; //时钟上升沿4ns后,读使能清零
data = o_data;
$display("Pop:",,data);
end
endtask
endmodule
采用Modelsim仿真得到如下波形:
可以在Modelsim的View------Transcript窗口看到有如下打印信息:
# run -all
# Push 1
# Push 2
# Pop: 1
# Push 3
# Push 4
# Push 5
# Push 6
# Push 7
# Push 8
# Push 9
# Cannot push 10: Buffer Full
# Cannot push 11: Buffer Full
# Cannot push 12: Buffer Full
# Cannot push 13: Buffer Full
# Cannot push 14: Buffer Full
# Cannot push 15: Buffer Full
# Cannot push 16: Buffer Full
# Cannot push 17: Buffer Full
# Pop: 2
# Push 18
# Pop: 3
# Pop: 4
# Pop: 5
# Pop: 6
# Push 19
# Pop: 7
# Push 20
# Pop: 8
# Pop: 9
# Pop: 18
# Pop: 19
# Pop: 20
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Push 21
# Pop: 21
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
六、异步FIFO
虽然各大厂商都有自己的现成 FIFO IP 可供调用, 而且自己设计异步FIFO是比较复杂的。
但是我们仍然需要学习FIFO的设计原理,这样我们在设计或移植的过程中查找问题起来将有据可循。
所以掌握异步FIFO设计原理是一名合格FPGA工程师的基本功。
1、异步FIFO的电路框图
异步FIFO有两个时钟信号,读和写接口分别采用不同时钟,这两个时钟可能时钟频率不同,也可能时钟相位不同,可能是同源时钟,也可能是不同源时钟。
下面是异步FIFO的系统框图:
可以看到异步FIFO实质上也是基于中间的双口RAM,外加一些读写控制电路组成的。
因为这里读写用的是两个不同的时钟。这将涉及到跨时钟域问题。跨时钟域的电路会带来亚稳态。
2 、亚稳态
外部电路对异步FIFO进行读写操作时,需要根据异步FIFO输出的空满信号来判断是否能继续对异步FIFO进行读或者写的操作。
那么FIFO是如何输出空满信号的呢?
异步FIFO跟同步FIFO一样设置读写指针, 同样也是当读地址指针追上写地址指针(写地址指针跟读地址指针相等),此时FIFO是读空状态。
当写地址指针再次追上读地址指针(写指针跟读地址指针再次相等的时候),此时FIFO是写满状态。
同步FIFO里面指针的比较直接采用了一个额外的计数器统计FIFO里面还剩多少数据。
但异步FIFO是读写时钟不同步的,只能将读时钟域的读地址指针传输到 写时钟域然后与写地址指针进行比较判断FIFO是否为满,将写时钟域的写地址指针传输到读时钟域然后与读地址指针进行比较判断FIFO是否为空。
这种跨时钟域的处理就会产生亚稳态。下面举个例子:
同步时钟:
假设数据从0跳变到1,一般数据的跳变不是立马跳变,而是有一个上升时间,有个斜坡。
如果是同步时钟采集数据则不会有什么影响。
第一个时钟周期采集到的是0, 第二个周期电平已经稳定到1。
异步时钟:
如果是异步时钟,比如数据跟clk1是同步的,第2个时钟比第1个时钟滞后一点点,那么第2个时钟在采集数据的时候有可能时钟上升沿正好对应在数据跳变的阶段,那此时读到的数据可能是0, 可能是1, 也可能输出中间级电平,或者是处于振荡状态。
这就是出现了亚稳态。这种不确定的电平输出会沿着信号通道上的电路继续传递下去,对电路造成很大危害,极有可能让整个系统挂死。
亚稳态不可完全避免, 只能通过一些手段如 引入同步机制(打2拍) 以及 格雷码等来降低亚稳态出现的机率。
3、打两拍
异步FIFO的跨时钟域处理所带来的亚稳态可以通过同步机制(打两拍)来降低亚稳态发生的概率。
如下图,A时钟域的数据Q1传递给B时钟域, 当B时钟上升沿来时,可能恰好数据Q1从0跳变到1,这样Q2极有可能出现亚稳态。
如果我们将Q2的值直接拿来用,将会导致亚稳态传播下去。
所以后面再设置一个D触发器继续对Q2进行采样得到Q3。
可能Q2会产生亚稳态,但等到Q3时候电平就会稳定到0或者1(也有可能继续是亚稳态,但一个电路出现亚稳态概率非常低, 然后连续两次出现亚稳态的概率更低, 低到我们可以忽略, 因此我们可以假设打两拍以后Q3 不存在亚稳态了,因此打两拍可以解决亚稳态传播的问题)。
Q1经过B时钟打两拍同步以后的数据Q3才能在B时钟域被使用。
当然,可能有人会问,如果Q1当时跳变为1时却被识别为0 ,对电路就没有影响吗?
答案是,如果只是一个地方判断错误不会有太大影响。怕就怕亚稳态一直被传播下去。
4、格雷码
格雷码是一种相邻数据只有1bit变化的码制。
十进制数 | 自然二进制码 | 格雷码 |
---|---|---|
0 | 0000 | 0000 |
1 | 0001 | 0001 |
2 | 0010 | 0011 |
3 | 0011 | 0010 |
4 | 0100 | 0110 |
5 | 0101 | 0111 |
6 | 0110 | 0101 |
7 | 0111 | 0100 |
8 | 1000 | 1100 |
9 | 1001 | 1101 |
10 | 1010 | 1111 |
11 | 1011 | 1110 |
12 | 1100 | 1010 |
13 | 1101 | 1011 |
14 | 1110 | 1001 |
15 | 1111 | 1000 |
如果地址采用二进制码,地址从3(0011)跳变到4(0100),有3个bit发生了变化, 每个bit 都有可能发生亚稳态,那么此时亚稳态出现的几率是1bit 跳变的3倍。
因为格雷码每次跳变只有一个bit,所以采用格雷码将大大降低了亚稳态发生的概率。
格雷码是二进制码右移1位再与原码相异或的结果。
二进制码转格雷码的Verilog代码实现如下:
graycode = (bincode>>1) ^ bincode;
如果二进制变化没有任何规律,那么采用格雷码也可能发生多 bit 的跳变,而 FIFO 设计中的读写地址都是连续变化的,因此格雷码适用于 FIFO 的地址处理。
5、如何判断异步FIFO的空满
(1)空判断
当读地址指针追上写地址指针,写地址指针跟读地址指针相等,此时FIFO是读空状态。
(2)满判断
当写地址指针再次追上读地址指针,写指针跟读地址指针再次相等的时候,此时FIFO是写满状态。
(3)虚空、虚满
当发现计数器不准。
当写地址同步到读时钟域时,这个地址需要在读时钟域打两拍,而这两拍的过程中写控制端还可以继续向FIFO里面写数据,如果此时判断FIFO为空的话,这个空属于虚空。
当读地址同步到写时钟域时 这个地址需要在写时钟域打两拍,而这两拍的过程中读控制端还可以继续从FIFO里面读取 数据,如果此时判断FIFO为满的话,这个满属于虚满。
虚空虚满不会产生错误, 只是影响FIFO 效率。 理解这些原理后,分析问题就知道去哪里分析。
6、如何选择FIFO深度
写比读快
第一种情况,已知连续写数据的长度(Burst Length),那么只需要考虑这段时间内最多会写进多少个数,以及会读走多少个数,二者只差就是FIFO的深度。
读比写快
FIFO的深度为1就可以了。
读写一样
FIFO的深度为1就可以了。
7、异步FIFO的设计代码
(1)顶层模块
module async_fifo#(parameter BUF_SIZE=8, BUF_WIDTH=8)
//FIFO深度默认为8
//FIFO的数据位宽默认为8bit
(
input [BUF_WIDTH-1:0] i_wdata,
input i_w_en, i_wclk, i_wrst_n, //写请求信号,写时钟,写复位
input i_r_en, i_rclk, i_rrst_n, //读请求信号,读时钟,读复位
output [BUF_WIDTH-1:0] o_rdata,
output o_buf_full,
output o_buf_empty
);
wire [$clog2(BUF_SIZE)-1:0] waddr, raddr;
wire [$clog2(BUF_SIZE):0] wptr, rptr, wq2_rptr, rq2_wptr;
/*在检测"满"或"空"状态之前,需要将指针同步到其它时钟域时,使用格雷码,可以降低同步过程中亚稳态出现的概率*/
sync_r2w I1_sync_r2w(
.wq2_rptr(wq2_rptr),
.rptr(rptr),
.wclk(i_wclk),
.wrst_n(i_wrst_n));
sync_w2r I2_sync_w2r (
.rq2_wptr(rq2_wptr),
.wptr(wptr),
.rclk(i_rclk),
.rrst_n(i_rrst_n));
/* DualRAM */
dualram #(BUF_WIDTH, BUF_SIZE) I3_DualRAM(
.rdata(o_rdata),
.wdata(i_wdata),
.waddr(waddr),
.raddr(raddr),
.wclken(i_w_en),
.wclk(i_wclk));
/*空、满比较逻辑*/
rptr_empty #(BUF_SIZE) I4_rptr_empty(
.rempty(o_buf_empty),
.raddr(raddr),
.rptr(rptr),
.rq2_wptr(rq2_wptr),
.rinc(i_r_en),
.rclk(i_rclk),
.rrst_n(i_rrst_n));
wptr_full #(BUF_SIZE) I5_wptr_full(
.wfull(o_buf_full),
.waddr(waddr),
.wptr(wptr),
.wq2_rptr(wq2_rptr),
.winc(i_w_en),
.wclk(i_wclk),
.wrst_n(i_wrst_n));
endmodule
(2)双端口RAM模块
双端口RAM模块用于存储数据。
module dualram
#(
parameter BUF_WIDTH = 8, // 数据位宽
parameter BUF_SIZE = 8 // FIFO深度
)
(
input wclken,wclk,
input [$clog2(BUF_SIZE)-1:0] raddr, //RAM 读地址
input [$clog2(BUF_SIZE)-1:0] waddr, //RAM 写地址
input [BUF_WIDTH-1:0] wdata, //写数据
output [BUF_WIDTH-1:0] rdata //读数据
);
reg [BUF_WIDTH-1:0] Mem[BUF_SIZE-1:0];
always@(posedge wclk)
begin
if(wclken)
Mem[waddr] <= wdata;
end
assign rdata = Mem[raddr];
endmodule
(3)同步模块1
sync_r2w 模块用于读地址同步到写控制端。
module sync_r2w
#(parameter BUF_SIZE = 8)
(
output reg [$clog2(BUF_SIZE):0] wq2_rptr,
input [$clog2(BUF_SIZE):0] rptr,
input wclk, wrst_n
);
reg [$clog2(BUF_SIZE):0] wq1_rptr;
always @(posedge wclk or negedge wrst_n) begin
if (!wrst_n)
{wq2_rptr,wq1_rptr} <= 0;
else
{wq2_rptr,wq1_rptr} <= {wq1_rptr,rptr};// 将写时钟域传过来的地址打两拍
end
endmodule
(4)同步模块2
sync_w2r模块用于写地址同步到读控制端。
module sync_w2r
#(parameter BUF_SIZE = 8)
(
output reg [$clog2(BUF_SIZE)+1:0] rq2_wptr,
input [$clog2(BUF_SIZE)+1:0] wptr,
input rclk, rrst_n
); reg [$clog2(BUF_SIZE)+1:0] rq1_wptr;
always @(posedge rclk or negedge rrst_n) begin
if (!rrst_n)
{rq2_wptr,rq1_wptr} <= 0;
else
{rq2_wptr,rq1_wptr} <= {rq1_wptr,wptr};
end
endmodule
(5)空判断模块
空判断模块用于判断是否可以读取数据。
读操作时,读使能rinc有效且FIFO未空。
module rptr_empty
#(parameter BUF_SIZE = 8)
(
output reg rempty, //输出空信号
output [$clog2(BUF_SIZE)-1:0] raddr, //输出读数据地址
output reg [$clog2(BUF_SIZE):0] rptr, //读数据指针
input [$clog2(BUF_SIZE):0] rq2_wptr, //写数据指针的格雷码经过打两拍后输入
input rinc, rclk, rrst_n);
reg [$clog2(BUF_SIZE):0] rbin;
wire [$clog2(BUF_SIZE):0] rgraynext, rbinnext;
wire rempty_val;
always @(posedge rclk or negedge rrst_n)
if (!rrst_n)
begin
rbin <= 0;
rptr <= 0;
end
else
begin
rbin <= rbinnext ;
rptr <= rgraynext;
end
// gray码计数逻辑
assign rbinnext = !rempty ? (rbin + rinc) : rbin; //如果为空,则指针不变,如果不为空,指针+1
assign rgraynext = (rbinnext>>1) ^ rbinnext; //二进制到gray码的转换
assign raddr = rbin[$clog2(BUF_SIZE)-1:0];
/*读指针是一个n位的gray码计数器,比FIFO寻址所需的位宽大一位
当系统复位或者读指针和同步过来的写指针完全相等时(包括MSB),说明二者折回次数一致,FIFO为空*/
assign rempty_val = (rgraynext == rq2_wptr);
always @(posedge rclk or negedge rrst_n)
if (!rrst_n)
rempty <= 1'b1;
else
rempty <= rempty_val;
endmodule
(6)满判断模块
满判断模块用于判断是否可以写入数据。
写操作时,写使能winc有效且FIFO未满。
module wptr_full
#(
parameter BUF_SIZE = 8
)
(
output reg wfull, //输出满信号
output [$clog2(BUF_SIZE)-1:0] waddr, //输出写地址
output reg [$clog2(BUF_SIZE):0] wptr, //输出写指针
input [$clog2(BUF_SIZE):0] wq2_rptr, //读指针的格雷码打两拍后输入
input winc, wclk, wrst_n);
reg [$clog2(BUF_SIZE):0] wbin;
wire [$clog2(BUF_SIZE):0] wgraynext, wbinnext;
wire wfull_val;
// GRAYSTYLE2 pointer
always @(posedge wclk or negedge wrst_n)
if (!wrst_n)
begin
wbin <= 0;
wptr <= 0;
end
else
begin
wbin <= wbinnext;
wptr <= wgraynext;
end
//gray 码计数逻辑
assign wbinnext = !wfull ? (wbin + winc) : wbin;
assign wgraynext = (wbinnext>>1) ^ wbinnext;
assign waddr = wbin[$clog2(BUF_SIZE)-1:0];
/*由于满标志在写时钟域产生,因此比较安全的做法是将读指针同步到写时钟域*/
assign wfull_val = (wgraynext=={~wq2_rptr[$clog2(BUF_SIZE):$clog2(BUF_SIZE)-1],
wq2_rptr[$clog2(BUF_SIZE)-2:0]});
always @(posedge wclk or negedge wrst_n)
if (!wrst_n)
wfull <= 1'b0;
else
wfull <= wfull_val;
endmodule
异步FIFO设计的整体RTL Viewer如下图所示:
8、 仿真
(1)异步FIFO仿真文件
`timescale 1 ps/ 1 ps
module async_fifo_vlg_tst();
reg i_r_en;
reg i_rclk;
reg i_rrst_n;
reg i_w_en;
reg i_wclk;
reg i_wrst_n;
reg [7:0] i_wdata;
wire o_buf_empty;
wire o_buf_full;
wire [7:0] o_rdata;
async_fifo i1 (
.i_r_en(i_r_en),
.i_rclk(i_rclk),
.i_rrst_n(i_rrst_n),
.i_w_en(i_w_en),
.i_wclk(i_wclk),
.i_wdata(i_wdata),
.i_wrst_n(i_wrst_n),
.o_buf_empty(o_buf_empty),
.o_buf_full(o_buf_full),
.o_rdata(o_rdata)
);
always #10 i_wclk = ~i_wclk;
always #5 i_rclk = ~i_rclk;
reg [7:0] r_data=8'd0;
initial begin
i_wclk=1'b0;
i_rclk=1'b0;
i_wrst_n=1'b1;
i_rrst_n=1'b1;
i_w_en=1'b0;
i_r_en=1'b0;
i_wdata=8'd0;
#1 i_wrst_n=1'b0;
i_rrst_n=1'b0;
#1 i_wrst_n=1'b1;
i_rrst_n=1'b1;
#20 push(1);
push(2);
//pop(r_data);
push(3);
push(4);
push(5);
push(6);
push(7);
push(8);
push(9);
pop(r_data);
push(10);
push(11);
push(12);
push(13);
push(14);
push(15);
push(16);
pop(r_data);
push(17);
pop(r_data);
push(18);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
push(19);
pop(r_data);
push(20);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
push(21);
pop(r_data);
pop(r_data);
pop(r_data);
pop(r_data);
#100 $stop;
end
task push (input [7:0] data);
if(o_buf_full)
$display("Cannot push %d: Buffer Full",data);
else begin
$display("Push",,data);
i_wdata=data;
i_w_en=1;
@(posedge i_wclk) #4 i_w_en= 0; //时钟上升沿后4ns,写使能清零
end
endtask
task pop(output[7:0] data);
if(o_buf_empty)
$display("Cannot Pop: Buffer Empty");
else begin
data = o_rdata;
$display("Pop:",,data);
i_r_en=1;
@(posedge i_rclk) #4 i_r_en= 0; //时钟上升沿4ns后,读使能清零
end
endtask
endmodule
这里选择的是读写频率相同,但读是在时钟下降沿, 写在时钟的上升沿。
(2)异步FIFO仿真结果
打开Quartus 的 菜单栏的 Tools------Run Simulation Tool------RTL Simulation看到波形如下:
当复位撤销(复位信号低有效)之后,在写使能 i_w_en 拉高有效之后,写数据也开始变化:
empty 空标记也开始在几拍之后变为非空(有一个写到读侧的异步转换,打了两拍):
当读使能i_ r_en 拉高有效之后,读数据在下一拍也开始变化:
可以在Modelsim的View------Transcript窗口看到有如下打印信息:
# run -all
# Push 1
# Push 2
# Push 3
# Push 4
# Push 5
# Push 6
# Push 7
# Push 8
# Cannot push 9: Buffer Full
# Pop: 1
# Cannot push 10: Buffer Full
# Cannot push 11: Buffer Full
# Cannot push 12: Buffer Full
# Cannot push 13: Buffer Full
# Cannot push 14: Buffer Full
# Cannot push 15: Buffer Full
# Cannot push 16: Buffer Full
# Pop: 2
# Cannot push 17: Buffer Full
# Pop: 3
# Cannot push 18: Buffer Full
# Pop: 4
# Pop: 5
# Pop: 6
# Pop: 7
# Push 19
# Pop: 8
# Push 20
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Push 21
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# Cannot Pop: Buffer Empty
# ** Note: $stop :
七、FIFO应用实例 (ADC)
一、实验前提
内容参考:LTC2308 ADC器件解读以及LTC2308控制器代码解读
二、FIFO IP调用
自己设计FIFO的目的一般是为了学习一下FIFO的结构,设计思路等,如果是一般的项目设计 ,建议可以直接调用厂商提供的FIFO IP 进行简单配置会不容易出错一点。
使用Quartus II软件提供的免费FIFO IP核,Quartus II软件为用户提供了友好的图形化界面方便用户对FIFO的各种参数和结构进行配置,生成的FIFO IP核针对Altera不同系列的器件,还可以实现结构上的优化。
Quartus 里面提供的FIFO可分为两种结构:单时钟FIFO(SCFIFO)和双时钟FIFO(DCFIFO), 本实验我们调用DCFIFO。