之前也在CSDN上面写过两个FIFO相关的文章,不过代码看起来比较复杂,且注释也比较少,不利于新手入门。很多时候都没有耐心继续看下去。
http://t.csdn.cn/0dPX6
http://t.csdn.cn/lYvoY
因为自己本身是一个初学者,就从初学者的视角来看待并学习FIFO。
为什么选择学习FIFO?
在学完双端口RAM之后看待FIFO,会觉得为什么要用FIFO呢?双端口的RAM也可以实现数据的存储与读取,读写的时钟也可以不一样,为什么不用RAM而要基于RAM来设计一个FIFO呢?
FIFO与RAM的区别是先进先出,不需要读地址和写地址。 写时钟下,将数据写入FIFO中,在读时钟下将先写入的数据先读出来,不需要向FIFO输入写地址和读地址即可完成数据的存储以及不同时钟下的读写操作,这样一听是非常方便的。在SDRAM的学习过程中,我们知道有突发长度这个东西,当突发长度为1的时候,即一个写地址对应一个写数据,这样是非常麻烦的,所以很多SDRAM如DDR3这种,都会将突发长度设置为8,即给一个首地址,后面连续读取8个数据。
再贴一张异步FIFO的图
在写代码之前,需要了解几个概念。
先思考,FIFO的存储空间和RAM是一样的,就像一个n行x1列的表格,每个表格里面存放一个数据,并且对应一个地址,在读写的过程中肯定会存在表格写满的情况和读的时候里面没有数据的情况,那应该怎么判断呢?
读写同时进行时
①首先是在读的视角,如果如果读一行数据的时候,刚好也在往这一行数据里面写数据,那这个时候即可判断读空了,如果再继续向下读的话,里面就没有写进的数据,读出的数据也不是我们写进去的,就是无效的。
所以读空的判断条件是:在读时钟的视角下,写时钟同步过来的地址等于我目前正在读的地址。
关于跨时钟域的问题,大家可以去搜索一下跨时钟域以及亚稳态。也可以看我的这篇文章。
http://t.csdn.cn/hvJTa
②在写的视角下, 那什么时候写满呢?因为地址是有限的嘛,当读完一个数据的时候,读对应哪个地址的数据就已经不需要了,因为我们以及读了,即读完的那个“位置”空了。所以当写完一圈,并且追上下一轮的读的时候,就代表写满了。
所以写满判断的条件是:在写的时钟下,写完一圈对应的地址,等于同步过来的读地址。
其次在写代码的时候,还需要了解格雷码,地址是按照0000-0001-0010-xxxx这种增长的,但是在地址变化的过程中,地址中的位数会存在”跳变“,如从0001-0010这两个相邻码的时候,有两位发生了变化,这样是不好的。 具体可以参考这篇文章
http://t.csdn.cn/OiesB
以下是代码Verilog的代码
`timescale 1ns / 1ps
module asyn_fifo1
(
input rst_n ,
input wr_clk ,
input wr_en ,
input [7:0] data_in ,
input rd_clk ,
input rd_en ,
output full ,
output empty ,
output reg [7:0] data_out
);
reg [7:0] ram_mem[255:0] ; //定义一个位宽为8bit深度为256的双端口RAM
wire [7:0] rd_addr ;
wire [7:0] wr_addr ;
reg [8:0] rd_addr_ptr ; //格雷码需要移位运算,且判断写满信号也需要多一位
reg [8:0] wr_addr_ptr ;
wire [8:0] rd_addr_gray ;
reg [8:0] rd_addr_gray1 ;
reg [8:0] rd_addr_gray2 ;
wire [8:0] wr_addr_gray ;
reg [8:0] wr_addr_gray1 ;
reg [8:0] wr_addr_gray2 ;
assign rd_addr[7:0] = rd_addr_ptr[7:0];
assign wr_addr[7:0] = wr_addr_ptr[7:0];
assign rd_addr_gray = (rd_addr_ptr>>1) ^ rd_addr_ptr; //bin to gray
assign wr_addr_gray = (wr_addr_ptr>>1) ^ wr_addr_ptr;
//dual port ram
integer i;
always @(posedge wr_clk or negedge rst_n) //写时钟下初始化RAM
begin
if(rst_n == 1'b0)
for(i=0;i<256;i=i+1)
ram_mem[i] <= 1'b0;
else if(wr_en && ~full) //写使能且没有写满
ram_mem[wr_addr] = data_in;
else
ram_mem[wr_addr] = ram_mem[wr_addr];
end
//rd_addr_ptr++ and wr_addr_ptr++
always @(posedge rd_clk or negedge rst_n) //读时钟下,对读地址进行操作
begin
if(rst_n == 1'b0)
rd_addr_ptr <= 1'b0;
else if(rd_en && ~empty)
rd_addr_ptr <= rd_addr_ptr + 1'b1;
else
rd_addr_ptr <= rd_addr_ptr;
end
always @(posedge wr_clk or negedge rst_n) //写时钟下,对写地址进行操作
begin
if(rst_n == 1'b0)
wr_addr_ptr <= 1'b0;
else if(wr_en && ~full)
wr_addr_ptr <= wr_addr_ptr + 1'b1;
else
wr_addr_ptr <= wr_addr_ptr;
end
//gray and two regsiter
always @(posedge wr_clk or negedge rst_n) //写时钟视角下,把读时钟同步到自己的时钟下
begin
if(rst_n == 1'b0)begin
rd_addr_gray1 <= 1'b0;
rd_addr_gray2 <= 1'b0;
end
else begin
rd_addr_gray1 <= rd_addr_gray;
rd_addr_gray2 <= rd_addr_gray1;
end
end
always @(posedge rd_clk or negedge rst_n) //读时钟视角下,把写时钟同步到自己的时钟下
begin
if(rst_n == 1'b0)begin
wr_addr_gray1 <= 1'b0;
wr_addr_gray2 <= 1'b0;
end
else begin
wr_addr_gray1 <= wr_addr_gray;
wr_addr_gray2 <= wr_addr_gray1;
end
end
//data_out
always @(posedge rd_clk or negedge rst_n)
begin
if(rst_n == 1'b0)
data_out <= 1'b0;
else if(rd_en && ~empty)
data_out <= ram_mem[rd_addr];
else
data_out <= 1'b0;
end
assign empty = (rd_addr_gray == wr_addr_gray2)?1'b1:1'b0; //判断读没读空
assign full = (wr_addr_gray[8:7] != rd_addr_gray2[8:7]) && (wr_addr_gray[6:0] == rd_addr_gray2[6:0]); //判断是否写满
endmodule
以下是tb仿真文件代码
`timescale 1ns / 1ps
module asyn_fifo1_tb();
reg rst_n ;
reg wr_clk ;
reg wr_en ;
reg [7:0] data_in ;
reg rd_clk ;
reg rd_en ;
wire full ;
wire empty ;
wire [7:0] data_out ;
asyn_fifo1 asyn_fifo1_inst
(
.rst_n (rst_n) ,
.wr_clk (wr_clk) ,
.wr_en (wr_en) ,
.data_in (data_in) ,
.rd_clk (rd_clk) ,
.rd_en (rd_en) ,
.full (full) ,
.empty (empty) ,
.data_out (data_out)
);
initial wr_clk = 0;
always #10 wr_clk = ~wr_clk; //写时钟为50MHz
initial rd_clk = 0;
always #30 rd_clk = ~rd_clk; //读时钟频率为写时钟的1/3
always @(posedge wr_clk or negedge rst_n) //不停的向FIFO中写0-255的数据
begin
if(rst_n == 1'b0)
data_in <= 0;
else if (wr_en)
data_in <= data_in+1'b1;
else
data_in <= data_in;
end
initial begin
rst_n = 0;
wr_en = 0;
rd_en = 0;
#200; //时间为200ns时,允许写入
rst_n = 1;
wr_en = 1;
#20000; //时间再过20000ns时,不允许写,开始读
wr_en = 0;
rd_en = 1;
#20000; //时间再过20000ns时,读停止
rd_en=0;
$stop;
end
endmodule
波形分析
200ns时,数据开始写入FIFO中,如下图
写满时,full信号拉高,后面继续不停写入,但满了,都是无效的写入。
20200ns时,开始读数据
由于写时钟为读时钟的三倍,从整体的波形图中也可以看出,写满数据的时间是读完数据时间的1/3.