一、FIFO****介绍
1、FIFO的分类
FIFO ( First In First Out)先进先出存储器。根据接入的时钟信号,可以分为同步 FIFO 和异步 FIFO 。
FIFO 底层是基于双口 RAM ,同步 FIFO 的读写时钟一致,异步 FIFO 读时钟和写时钟不同。
作用:
同步 FIFO:主要用于数据缓冲,类似于乒乓缓存思想,可以让后级不必等待前级过多时间。
异步 FIFO : a. 跨时钟域传输数据 b. 不同位宽的数据接口
2、FIFO 的常见参数
FIFO 的宽度:即 FIFO 一次读写操作的数据位;
FIFO 的深度:指的是 FIFO 可以存储多少个 N 位的数据(如果宽度为 N)。
满标志: FIFO 已满或将要满时由 FIFO 的状态电路送出的一个信号,以阻止FIFO 的写操作继续向 FIFO 中写数据而造成溢出( overflow)。
空标志: FIFO 已空或将要空时由 FIFO 的状态电路送出的一个信号,以阻止FIFO 的读操作继续从 FIFO 中读出数据而造成无效数据的读出( underflow)。
读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。
写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据
读使能:写使能信号有效时写入数据。
写使能:都市能信号有效时读出数据。
二、同步FIFO实现
在同步FIFO的实现中我们完全不用考虑不同的时钟和跨时钟的问题,因为在同步FIFO中读写都使用同一个时钟资源。
三、异步FIFO实现
异步 FIFO 实质上是基于中间的双口 RAM,外加一些读写控制电路组成的,主要是实现不同时钟域之间的数据交互。异步 FIFO 读/写操作在两个不同的时钟域,这个过程会设计到跨时钟域处理,所以需要考虑跨时钟域会产生亚稳态的问题。另外,异步 FIFO 也需要通过空满标志去衡量存储器的使用情况,空满标志的产生同样也需要考虑读 /写时钟域,其产生的条件和方式也是需要重点考虑的。以下为具有同步指针的异步 FIFO 示意图。
**1、读/**写指针
写指针:总是指向下一次要写的数据地址,写完后自动加 1 ;系统复位后( FIFO为空),写指针指向 0 地址。
读指针:总是指向下一次要读的数据地址,读完后自动加 1 ;系统复位后(FIFO为空),写指针指向 0 地址。
异步 FIFO 中的指针因设计需要,位宽比地址多 1 位。此处 FIFO 的地址对应 FIFO 的存储单元。如深度为 128 的 FIFO,理论上指针位需要[6:0],但为了判断空满都 需要将指针拓展到[7:0]。
**2、**空满判断
外部电路对异步 FIFO 进行读写操作时,需要根据异步 FIFO 输出的空满信号来判断是否能继续对异步 FIFO 进行读或者写的操作。
空标志:读指针追上写指针,即指针相等。
满标志:写指针追上读指针,即写指针与读指针再次相等,读 /写指针最高位不同即说明再次追上。
异步 FIFO 的读写时钟不同,判断时需要将写指针同步到读时钟域或将读指针同 步到写时钟域进行判断。
跨时钟域传输数据,则有可能会出现亚稳态。亚稳态无法完全避免,但可以通过引入同步机制(打两拍)和格雷码来降低亚稳态出现的概率。
**3、**亚稳态(打两拍)
上图位亚稳态为打两拍降低亚稳态的示意图。
当 A 时钟域下的 Din 传递到 B 时钟域,而当 B 时钟上升沿来临时,恰好 Din 数据发生跳变,则 Ds 极有可能出现亚稳态,若使用 Ds,则会导致亚稳态逐级传播下去。固可以再用以及触发器寄存,此时 Dout 的电平就稳定。(有一定可能仍是亚稳态,但概率极低,不做考虑。)单比特信号直接打两拍降低亚稳态的效果较好,但是多比特传输,若多位发生变化时,变化的位都有可能产生亚稳态,所以多比特不直接用打拍的方式进行同步。异步 FIFO 的地址指针每次变化都是加 1,将指针转化为格雷码后(相邻两位格雷码只有 1 位二进制发生变化),可直接进行打两拍来降低亚稳态。
**4、**格雷码与二进制相互转换
二进制转格雷码
格雷码转二进制
**5、**代码实现思路
(1 )输入输出端口定义
(2)位宽和深度参数化
(3)如何定义一个内存块?
方法一:
定义一个一维数组。将内存定义为一个 reg 类型的一维数组,这个数组中
的任何一个单元都可以通过一个下标去访问。
如: reg [ 7 : 0 ] data [ 255 : 0 ];
其中 [7:0] 表示一维数组中的每个元素的位宽大小,而在变量后面的
[255:0] ,表示的却不是位宽大小,它表示的是所创建的数组的深度,也就
是一维数组中的元素大小,也可以称作为数组的容量大小。
初始化可用 for 循环清零
方法二:
调用一个双端口的 RAM IP 。
4 wrusedw 与 rdusedw 怎么求取?写指针领先于读指针的,即写指针减去
读指针即可,异步 FIFO 此处要考虑不同时钟域。
四、代码实现
1、设计文件的编写
新建一个async_fifo.v文件,如下:
cpp
//---------<模块及端口声名>------------------------------------------------------
module async_fifo #(parameter FIFO_WIDTH = 8 ,//FIFO输入数据位宽
FIFO_DEPTH = 128 //FIFO深度
)(
//Write clock domain
wrclk ,//写时钟
wrrst_n ,//写侧复位,异步复位,低有效
wren ,//写使能
wrdata ,//写数据输入
wrempty ,//写侧空标志
wrfull ,//写侧满标志
wrusedw ,//写时钟域下可读数据量
//Read clock domain
rdclk ,//读时钟
rdrst_n ,//读侧复位,异步复位,低有效
rden ,//读使能
rddata ,//读数据输出
rdempty ,//读侧空标志
rdfull ,//读侧满标志
rdusedw //读时钟域下可读数据量
);
//参数声明
localparam ADDR_W = log2b(FIFO_DEPTH),//指针位宽
DATA_W = FIFO_WIDTH;//FIFO深度
//function声明
/************* 用取对数的方法计算地址指针的位宽 ************************/
function integer log2b(input integer data);
begin
for(log2b=0;data>0;log2b=log2b+1)begin
data = data>>1;
end
log2b = log2b - 1;
end
endfunction
//---------<内部信号定义>-----------------------------------------------------
//端口声明
//Write clock domain
input wrclk ;//写时钟
input wrrst_n ;//写侧复位,异步复位,低有效
input wren ;//写使能
input [DATA_W-1:0] wrdata ;//写数据输入
output wrempty ;//写侧空标志
output wrfull ;//写侧满标志
output [ADDR_W-1:0] wrusedw ;//写时钟域下可读数据量
//Read clock domain
input rdclk ;//读时钟
input rdrst_n ;//读侧复位,异步复位,低有效
input rden ;//读使能
output [DATA_W-1:0] rddata ;//读数据输出
output rdempty ;//读侧空标志
output rdfull ;//读侧满标志
output [ADDR_W-1:0] rdusedw ;//读时钟域下可读数据量
reg [DATA_W-1:0] fifo_mem[FIFO_DEPTH - 1:0] ;//FIFO存储阵列
// reg [ADDR_W-1:0] fifo_mem[0:(1'b1<<ADDR_W)-1] ;//两种写法皆可
wire [ADDR_W-1:0] wr_addr ;//写地址
wire [ADDR_W-1:0] rd_addr ;//读地址
reg [ADDR_W:0] wr_ptr ;//二进制写指针
reg [ADDR_W:0] rd_ptr ;//二进制读指针
wire [ADDR_W:0] wr_ptr_gray ;//格雷码写指针
reg [ADDR_W:0] wr_ptr_gray1;//打2拍,写指针同步寄存器
reg [ADDR_W:0] wr_ptr_gray2;
wire [ADDR_W:0] rd_ptr_gray ;//格雷码读指针
reg [ADDR_W:0] rd_ptr_gray1;//打2拍,读指针同步寄存器
reg [ADDR_W:0] rd_ptr_gray2;
reg [ADDR_W:0] wr_gray2_bin;//将同步至读时钟域的格雷码写指针转换为二进制
reg [ADDR_W:0] rd_gray2_bin;//将同步至写时钟域的格雷码读指针转换为二进制
reg [DATA_W-1:0] rd_data_r ;//读出数据输出寄存器
reg [ADDR_W-1:0] wr_usedw_r ;//写时钟域下可读数据量寄存器
reg [ADDR_W-1:0] rd_usedw_r ;//读时钟域下可读数据量寄存器
integer i;
//****************************************************************
//--wr_ptr、rd_ptr
//****************************************************************
//写指针
always @(posedge wrclk or negedge wrrst_n)begin
if(!wrrst_n)begin
wr_ptr <= 'd0;
end
else if(wren && ~wrfull)begin
wr_ptr <= wr_ptr + 1'b1;
end
end
//读指针
always @(posedge rdclk or negedge rdrst_n)begin
if(!rdrst_n)begin
rd_ptr <= 'd0;
end
else if(rden && ~rdempty)begin
rd_ptr <= rd_ptr + 1'b1;
end
end
//****************************************************************
//--wr_addr、rd_addr
//****************************************************************
assign wr_addr = wr_ptr[ADDR_W-1:0];//数据写入地址
assign rd_addr = rd_ptr[ADDR_W-1:0];//数据读出地址
//****************************************************************
//--写入数据、读出数据
//****************************************************************
//写入数据
always @(posedge wrclk or negedge wrrst_n)begin
if(!wrrst_n)begin
for (i=0;i<(1'b1<<ADDR_W);i=i+1) begin //利用for循环循环清零fifo_ram
fifo_mem[i] <= 'd0;
end
end
else if(wren && ~wrfull)begin //只要写使能有效就一直写入数据,数据数据量超过fifo深度,则会重新覆盖
fifo_mem[wr_addr] <= wrdata;
end
end
//读出数据
always @(posedge rdclk or negedge rdrst_n)begin
if(!rdrst_n)begin
rd_data_r <= 'd0;
end
else if(rden & !rdempty)begin
rd_data_r <= fifo_mem[rd_addr];
end
end
//****************************************************************
//--二进制转格雷码
//****************************************************************
assign wr_ptr_gray = wr_ptr^(wr_ptr>>1);//写指针格雷码
assign rd_ptr_gray = rd_ptr^(rd_ptr>>1);//读指针格雷码
//****************************************************************
//--格雷码同步
//****************************************************************
//将写指针格雷码同步到读时钟域
always @(posedge rdclk or negedge rdrst_n)begin
if(!rdrst_n)begin
wr_ptr_gray1 <= 'd0;
wr_ptr_gray2 <= 'd0;
end
else begin
wr_ptr_gray1 <= wr_ptr_gray;
wr_ptr_gray2 <= wr_ptr_gray1;
end
end
//将读指针格雷码同步到写时钟域
always @(posedge wrclk or negedge wrrst_n)begin
if(!wrrst_n)begin
rd_ptr_gray1 <= 'd0;
rd_ptr_gray2 <= 'd0;
end
else begin
rd_ptr_gray1 <= rd_ptr_gray;
rd_ptr_gray2 <= rd_ptr_gray1;
end
end
//****************************************************************
//--格雷码转二进制
//****************************************************************
/*
格雷码转二进制:格雷码的最高位作为二进制的最高位,二进制次高位产生过程是
使用二进制的高位和格雷码次高位相异或得到
*/
//将同步至读时钟域的写指针格雷码转换为二进制
always @(*)begin
wr_gray2_bin[ADDR_W] = wr_ptr_gray2[ADDR_W];
for (i=ADDR_W-1;i>=0;i=i-1) begin
wr_gray2_bin[i] = wr_gray2_bin[i+1]^wr_ptr_gray2[i];
end
end
//将同步至写时钟域的格雷码读指针转换为二进制
always @(*)begin
rd_gray2_bin[ADDR_W] = rd_ptr_gray2[ADDR_W];
for (i=ADDR_W-1;i>=0;i=i-1) begin
rd_gray2_bin[i] = rd_gray2_bin[i+1]^rd_ptr_gray2[i];
end
end
//****************************************************************
//--输出
//****************************************************************
//空标志
assign wrempty = wr_ptr == rd_gray2_bin;
assign rdempty = rd_ptr == wr_gray2_bin;
//满标志
assign wrfull = (wr_ptr != rd_gray2_bin) && (wr_ptr[ADDR_W-1:0] == rd_gray2_bin[ADDR_W-1:0]);
assign rdfull = (rd_ptr != wr_gray2_bin) && (rd_ptr[ADDR_W-1:0] == wr_gray2_bin[ADDR_W-1:0]);
//读出数据
assign rddata = rd_data_r;
//可读数据量
always @(posedge wrclk or negedge wrrst_n)begin
if(!wrrst_n)begin
wr_usedw_r <= 'd0;
end
else begin
wr_usedw_r <= wr_ptr - rd_gray2_bin;
end
end
always @(posedge rdclk or negedge rdrst_n)begin
if(!rdrst_n)begin
rd_usedw_r <= 'd0;
end
else begin
rd_usedw_r <= wr_gray2_bin - rd_ptr;
end
end
assign wrusedw = wr_usedw_r;
assign rdusedw = rd_usedw_r;
endmodule
2、测试文件的编写
新建一个tb_async_fifo.v文件,如下:
cpp
`timescale 1ns/1ns
module tb_async_fifo();
//激励信号定义
reg tb_clk ;
reg tb_rst_n ;
reg wren ;
reg [7:0] wrdata ;
reg rden ;
//输出信号定义
wire wrempty ;
wire wrfull ;
wire [6:0] wrusedw ;
wire [7:0] rddata ;
wire rdempty ;
wire rdfull ;
wire [6:0] rdusedw ;
//时钟周期参数定义
parameter CLOCK_CYCLE = 20;
//模块例化
async_fifo #(.FIFO_WIDTH(8),.FIFO_DEPTH(128)) async_fifo_inst(
//Write clock domain
/*input */.wrclk (tb_clk ),//写时钟
/*input */.wrrst_n (tb_rst_n ),//写侧复位,异步复位,低有效
/*input */.wren (wren ),//写使能
/*input [FIFO_WIDTH-1:0] */.wrdata (wrdata ),//写数据输入
/*output */.wrempty (wrempty ),//写侧空标志
/*output */.wrfull (wrfull ),//写侧满标志
/*output [FIFO_DEPTH-1:0] */.wrusedw (wrusedw ),//写时钟域下可读数据量
/*input */.rdclk (tb_clk ),//读时钟
/*input */.rdrst_n (tb_rst_n ),//读侧复位,异步复位,低有效
/*input */.rden (rden ),//读使能
/*output [FIFO_WIDTH-1:0] */.rddata (rddata ),//读数据输出
/*output */.rdempty (rdempty ),//读侧空标志
/*output */.rdfull (rdfull ),//读侧满标志
/*output [FIFO_DEPTH-1:0] */.rdusedw (rdusedw ) //读时钟域下可读数据量
);
//产生时钟
initial tb_clk = 1'b0;
always #(CLOCK_CYCLE/2) tb_clk = ~tb_clk;
integer k;
//产生激励
initial begin
tb_rst_n = 1'b1;
wren = 'd0;
wrdata = 'd0;
rden = 'd0;
#(CLOCK_CYCLE*2);
tb_rst_n = 1'b0;
#(CLOCK_CYCLE*20);
tb_rst_n = 1'b1;
#1;
//模拟写操作
for (k=0;k<127;k=k+1) begin
wren = 1'b1;
wrdata = {$random}%256;
#CLOCK_CYCLE;
end
wren = 1'b0;
#(CLOCK_CYCLE*50);
//模拟读操作
for (k=0;k<127;k=k+1) begin
rden = 1'b1;
#CLOCK_CYCLE;
end
rden = 1'b0;
#(CLOCK_CYCLE*50);
//模拟复位,清空fifo
// tb_rst_n = 1'b0;
// #(CLOCK_CYCLE*150);
#(CLOCK_CYCLE*50);
$stop;
end
endmodule
3、波形图仿真
通过前后两张波形图我们可以看到是先写入128个数据之后再进行数据的输出,其中的剩余量,空满信号啥的也是正常变换,说明我们设计的异步FIFO成功。