概述
1.异步FIFO的定义
FIFO:First in first out的缩写,即先进先出,可以理解为一个双端口ram。异步fifo是指读写时钟不一样,有两个时钟信号,分别是读时钟信号和写时钟信号。与之对应的是同步fifo,它的读和写都由同一个时钟触发,只有一个时钟信号。
2.FIFO用途
首先做一个类比:把FIFO类比为一个蓄水池,该蓄水池有进水口和出水口。当进水量大于出水量时,可以把多余的水存入蓄水池中。当出水量大于进水量时,可以从蓄水池中取出水。由于受到重力的作用,从进水口先进入的水肯定先从出水口出来。但是,你的蓄水池有容量的限制,不可能无止境地装水,当装满时就不能再进水了;同时,蓄水池里面的水也可能用完,当排空时就不能再出水了,这就是对应的满和空状态。另外,进水和出水要有阀门对其控制。
而FIFO就是数字世界中用于数据缓存的这么一个“蓄水池”。
上面的类比对应的对象如下:
FIFO:蓄水池
进水:写数据
出水:读数据
进水阀门:写使能
出水阀门:读使能
水池装满:FIFO写满
水池排空:FIFO读空
由于存放的是数据,所以就有数据宽度,对应FIFO宽度;同时,把数据放在FIFO的哪个位置,以及从FIFO哪个位置取出数据,这就需要涉及到FIFO的写地址、读地址。因为异步FIFO读写的频率不一样,所以会有对应的读时钟、写时钟。
以上,就是设计一个FIFO需要的所有信号了,接下来我们以一个256位深度,存入数据位宽为8位的FIFO为例进行讲解。
信号定义如下:
输入信号:wr_clk(写时钟),rd_clk(读时钟),rst_n(复位信号),wr_en(写使能),rd_en(读使能),data_in(写入的八位数据)
输出信号:data_out(读出的八位数据),empty(读空信号标志),full(写满信号标志)
3.代码实现
module fifo(
input wr_clk,//写时钟
input rd_clk,//读时钟
input rst_n,//复位信号
input wr_en,//写使能
input rd_en,//读使能
input [7:0]data_in,//写入的数据
output reg [7:0] data_out,//读出的数据
output empty,//读空信号
output full//写满信号
);
reg [7:0] fifo [255:0];//定义一个位宽为八位,深度为256位的数组,代表fifo
reg [8:0] wr_addr_ptr;//写地址指针
reg [8:0] rd_addr_ptr;//读地址指针
wire [7:0] wr_addr;//写地址
wire [7:0] rd_addr;//读地址
assign wr_addr = wr_addr_ptr[7:0];
assign rd_addr = rd_addr_ptr[7:0];
wire [8:0] wr_gray;//写地址格雷码
reg [8:0] wr_gray1;
reg [8:0] wr_gray2;
wire [8:0] rd_gray;//读地址格雷码
reg [8:0] rd_gray1;
reg [8:0] rd_gray2;
assign wr_gray = (wr_addr_ptr >> 1) ^ wr_addr_ptr;//二进制码转化为格雷码
assign rd_gray = (rd_addr_ptr >> 1) ^ rd_addr_ptr;
integer i;
always@(posedge wr_clk or negedge rst_n)
if(!rst_n)begin
for(i=0;i<256;i=i+1)
fifo[i] <= 0;
end
else if (wr_en && ~full)begin
fifo[wr_addr] <= data_in;
end
else begin
fifo[wr_addr] <= fifo[wr_addr];
end
always@(posedge wr_clk or negedge rst_n)
if(!rst_n)begin
wr_addr_ptr <= 0;
end
else if(wr_en && ~full) begin
wr_addr_ptr <= wr_addr_ptr + 1'b1;
end
else begin
wr_addr_ptr <= wr_addr_ptr;
end
always@(posedge rd_clk or negedge rst_n)
if (!rst_n) begin
rd_addr_ptr <= 0;
end
else if (rd_en && ~empty) begin
rd_addr_ptr <= rd_addr_ptr + 1'b1;
end
else begin
rd_addr_ptr <= rd_addr_ptr;
end
always@(posedge rd_clk or negedge rst_n)
if (!rst_n) begin
data_out <= 0;
end
else if (rd_en && ~empty) begin
data_out <= fifo[rd_addr];
end
else begin
data_out <= 0;
end
always@(posedge wr_clk or negedge rst_n)//读地址格雷码同步到写时钟域
if (!rst_n) begin
rd_gray1 <= 0;
rd_gray2 <= 0;
end
else begin
rd_gray1 <= rd_gray;
rd_gray2 <= rd_gray1;
end
always@(posedge rd_clk or negedge rst_n)//写地址格雷码同步到读时钟域
if (!rst_n) begin
wr_gray1 <= 0;
wr_gray2 <= 0;
end
else begin
wr_gray1 <= wr_gray;
wr_gray2 <= wr_gray1;
end
assign empty = (rd_gray == wr_gray2)?1:0;
assign full = (wr_gray[8:7] != rd_gray2[8:7]) &&(wr_gray[6:0] == rd_gray2[6:0]);
endmodule
testbench代码:
`timescale 1ns/1ps
module fifo_tb ();
reg rst_n;
reg [7:0] data_in;
reg wr_clk;
reg rd_clk;
reg wr_en;
reg rd_en;
wire [7:0]data_out;
wire full;
wire empty;
fifo fifo(
.wr_clk(wr_clk),//写时钟
.rd_clk(rd_clk),//读时钟
.rst_n(rst_n),//复位信号
.wr_en(wr_en),//写使能
.rd_en(rd_en),//读使能
.data_in(data_in),//写入的数据
.data_out(data_out),//读出的数据
.empty(empty),//读空信号
.full(full)//写满信号
);
initial wr_clk = 0;//产生写时钟
always #10 wr_clk = ~wr_clk;
initial rd_clk = 0;//产生读时钟
always #30 rd_clk = ~rd_clk;
always@(posedge wr_clk or negedge rst_n)//产生写入的数据
if(!rst_n)
data_in <= 0;
else if (wr_en) begin
data_in <= data_in + 1'b1;
end
else
data_in <= data_in;
initial begin
rst_n = 0;
wr_en = 0;
rd_en = 0;
#200;
rst_n = 1;
wr_en = 1;
#20000;
wr_en = 0;
rd_en = 1;
#20000;
rd_en = 0;
$stop;
end
endmodule
仿真结果:
4.代码讲解
reg [7:0] fifo [255:0];//定义一个位宽为八位,深度为256位的数组,代表fifo
reg [8:0] wr_addr_ptr;
reg [8:0] rd_addr_ptr;
wire [7:0] wr_addr;
wire [7:0] rd_addr;
首先定义一个8×256的二维数组,用来表示FIFO。FIFO的宽度为8位,表示可以存入的数据位宽为8位,FIFO的深度为256位,表示一共可以存入256个数据。这256的数据在FIFO中的地址依次为0-255。这个地址用于后续把数据写入FIFO以及从FIFO中读出数据。
然后定义了读、写地址指针以及读、写地址,这里注意,指针的位宽要比地址多一位。是因为后面在判断“满”状态时,写指针要领先读指针一圈,因此指针位宽为9位,指针的0-7位就是地址。
assign wr_addr = wr_addr_ptr[7:0];
assign rd_addr = rd_addr_ptr[7:0];
wire [8:0] wr_gray;
reg [8:0] wr_gray1;
reg [8:0] wr_gray2;
wire [8:0] rd_gray;
reg [8:0] rd_gray1;
reg [8:0] rd_gray
然后定义了格雷码,格雷码可以减小亚稳态发生的概率。因为格雷码是根据地址指针产生的,因此格雷码位宽也是9位。
assign wr_gray = (wr_addr_ptr >> 1) ^ wr_addr_ptr;
assign rd_gray = (rd_addr_ptr >> 1) ^ rd_addr_ptr;
格雷码产生方法:数剧往右移一位然后与它本身异或。
integer i;
always@(posedge wr_clk or negedge rst_n)
if(!rst_n)begin
for(i=0;i<256;i=i+1)
fifo[i] <= 0;
end
else if (wr_en && ~full)begin
fifo[wr_addr] <= data_in;
end
else begin
fifo[wr_addr] <= fifo[wr_addr];
end
always@(posedge wr_clk or negedge rst_n)
if(!rst_n)begin
wr_addr_ptr <= 0;
end
else if(wr_en && ~full) begin
wr_addr_ptr <= wr_addr_ptr + 1;
end
else begin
wr_addr_ptr <= wr_addr_ptr;
end
这里定义了一个整数i,用于在复位时,清空FIFO中的数据。另外,在有写使能且FIFO没有写满时,每来一个写时钟信号,写地址指针加1,并根据当前的写地址将数据写入FIFO中。
always@(posedge rd_clk or negedge rst_n)
if (!rst_n) begin
rd_addr_ptr <= 0;
end
else if (rd_en && ~empty) begin
rd_addr_ptr <= rd_addr_ptr + 1;
end
else begin
rd_addr_ptr <= rd_addr_ptr;
end
always@(posedge rd_clk or negedge rst_n)
if (!rst_n) begin
data_out <= 0;
end
else if (rd_en && ~empty) begin
data_out <= fifo[rd_addr];
end
else begin
data_out <= 0;
end
和写数据类似,当有读使能且FIFO没有读空时,每来一个读时钟信号,读地址指针加1,并根据当前的读地址将FIFO中的数据读出。
always@(posedge wr_clk or negedge rst_n)
if (!rst_n) begin
rd_gray1 <= 0;
rd_gray2 <= 0;
end
else begin
rd_gray1 <= rd_gray;
rd_gray2 <= rd_gray1;
end
always@(posedge rd_clk or negedge rst_n)
if (!rst_n) begin
wr_gray1 <= 0;
wr_gray2 <= 0;
end
else begin
wr_gray1 <= wr_gray;
wr_gray2 <= wr_gray1;
end
格雷码打两拍及时钟域同步:因为要将信号同步在同一时钟域。所以,在写时钟域内同步读格雷码;在读时钟域内同步写格雷码。打两拍是为了尽可能降低亚稳态发生的概率。
assign empty = (rd_gray == wr_gray2)?1:0;
assign full = (wr_gray[8:7] != rd_gray2[8:7]) &&(wr_gray[6:0] == rd_gray2[6:0]);
空满判断:
empty判断:用的是rd_gray和wr_gray2这两个信号。因为empty状态一定是发生在读时钟域,只有读才能把FIFO读空,所以在判断empty状态时必须用读时钟域的信号。从上面的时钟域同步可以看到,rd_gray是读时钟域的rd_addr_ptr信号生成的。wr_gray2是wr_gray同步到读时钟域的信号。
full判断:用的是wr_gray和rd_gray2这两个信号。和empty判断的逻辑一样,full只可能发生在写时钟域,所以在判断full状态时必须用写时钟域的信号来判断。接下来解释full的判断方法:
如图所示:绿色代表读指针,红色代表写指针。因为读写时钟不一样,FIFO在写数据的过程中也一直在读数据。所以判断FIFO写满的标志就是:当写指针领先读指针一周的时候,这个时候就是FIFO写满的时候,一周的长度就是FIFO的深度。因此,这也就解释了为什么代码中读写指针的位宽(9位)比地址位宽(8位)多一位的原因。
接下来以一个简单的例子,说明当指针领先一周时,他们对应的格雷码应该是怎么样的。
二进制码转化为格雷码的方法是,先右移一位,然后将得到的数与原来的数做异或。
以读指针为000001110为例,写指针领先它一圈时,应该是100001110。二者对应的格雷码分别是000001001、110001001。可以看出,此时格雷码前两位相反,其他位相同。(其他例子可自行验证)
对应的full代码为:
assign full = (wr_gray[8:7] != rd_gray2[8:7]) &&(wr_gray[6:0] == rd_gray2[6:0]);
到此,一个FIFO的设计就完成了。
下面解释一下为什么empty信号在第7个数据写入时才拉低,按理说,第一个数据写入时,FIFO就不是空了,就应该把empty拉低。
因为我们判断empty用的是rd_gray和wr_gray2这两个信号,而wr_gray2是由wr_gray在读时钟域中打两拍之后得到的,所以当data_in为07时,wr_gray2才开始变化,故empty信号也是在这里拉低。
最后
以上就是风趣发卡为你收集整理的异步FIFO:Verilog代码设计与实现的全部内容,希望文章能够帮你解决异步FIFO:Verilog代码设计与实现所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复