一开始是想既然是极简教程,就应该只给出FIFO的概念,没想到还是给出了同步以及异步FIFO的设计,要不然总感觉内容不完整,也好,自己设计的FIFO模块不用去担心因IP核跨平台不通用的缺陷!那我们开始吧。
上一篇博客讲了同步FIFO的概念以及同步FIFO的设计问题,并给出了同步FIFO的Verilog代码以及VHDL代码,并经过了行为仿真测试,链接如下:
FPGA基础知识极简教程(3)从FIFO设计讲起之同步FIFO篇
这里简单提一下,同步FIFO的代码中用到了一个系统函数$clog2(),这个系统函数的使用方法很简单:
parameter DATA_WIDTH = 8;
parameter DATA_DEPTH = 8;
reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
reg [$clog2(DATA_DEPTH) - 1 : 0] wr_pointer = 0;
reg [$clog2(DATA_DEPTH) - 1 : 0] rd_pointer = 0;
例如我定义了FIFO缓冲区的深度为DATA_DEPTH = 8,那么其地址(指针)位宽是多少呢?
这时候就可以使用系统函数$clog2()了,位宽可以表示为:
$clog2(DATA_DEPTH) // = 3;
指针就可以定义为:
reg [$clog2(DATA_DEPTH) - 1 : 0] wr_pointer = 0;
reg [$clog2(DATA_DEPTH) - 1 : 0] rd_pointer = 0;
还有一点需要提的是,我们都知道在FPGA中FIFO的实现可以使用分布式资源或者BLOCK RAM,那么如何掌控呢?
当使用FIFO缓冲空间较小时,我们选择使用Distributed RAM;当使用FIFO缓冲空间较大时,我们选择使用BLOCK RAM资源;这是一般的选择原则。
我们可以通过在设计代码中加入约束条件来控制,之前有写过
Vivado 随笔(1) 综合属性之 ram_style & rom_style?
就上述同步FIFO而言,我们可以在缓冲区定义时候添加如下约束:
(*ram_style = "distributed"*) reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
或者:
(*ram_style = "block"*) reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
为了验证是否有用,我们在Vivado中进行验证如下:
当设计中使用BLOCK RAM约束:
(*ram_style = "block"*)reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
综合后的电路图如下,可见FIFO缓存区使用的资源为BLOCK RAM;
同时给出资源利用率报告:
可见存在BLOCK RAM ,由于我仅仅综合了一个同步FIFO,因此这个Block RAM一定是FIFO缓冲区消耗的。
当使用Distributed RAM约束时:
(*ram_style = "distributed"*)reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
综合后电路图FIFO缓冲区部分:
资源利用率情况;
可见 ,并未使用BLOCK RAM,而是使用了LUT RAM,也即分布式RAM。
综上,验证了这条约束的有效性。
再设计异步FIFO电路之前,有必要说明一下FIFO的用途,上篇博文提到:
FPGA或者ASIC设计内部电路多位数据在不同的时钟域交互,为了数据安全、正确、稳定交互,我们需要设计异步FIFO进行跨时钟域交互。正如之前博客所写:漫谈时序设计(1)跨时钟域是设计出来的,而非约束出来的!
我们在时序分析时候,通常都将跨时钟域路径进行伪路径约束,因此我们必须在设计时候解决跨时钟域数据传输问题,异步FIFO在此起到关键作用。
这三条大概讲的都是一个意思,总结起来就是FIFO可以起到数据缓冲或缓存的作用,例如突然数据,我们就需要先将其缓存起来,之后再从FIFO中读出出来进行处理,这样也可以保证数据不会丢失。
引用互联网上其他说法就是:数据写入过快,并且间隔时间长,也就是突发写入。那么通过设置一定深度的FIFO,可以起到数据暂存的功能,且使得后续处理流程平滑。
无论是同步FIFO还是异步FIFO,其大致原理都是一致的,先入先出自然不必多说,关于空满的判断都是通过读写指针之间的关系来判断;还有就是异步FIFO的指针需要进行一定的处理,例如格雷码处理,这样可以减小读指针同步到写指针时钟域,或者写指针同步到读指针时钟域时出现亚稳态的概率,这是因为格雷码每次只有一位变化,这样一位数据在进行跨时钟域传输的时候亚稳态出现的概率会大大减小。同步之后便进行对比,以此来判断FIFO的空满。
那异步FIFO如何判断空满呢?
回答这个问题之前,我想先统一的说明FIFO(同步或者异步)是如何判断空满的?
起始,读写指针都是0,FIFO一定为空;之后对FIFO进行一系列的读写操作,导致读写指针关系发生了变化,可以分为下面两种情况:
在同步FIFO中,我们使用计数的方式进行判断空满,运用的也是这个原理,写一个数据时,计数值加1,读出一个数据时,计数值减1,如下图:
如果再接着读2个 数据,则计数值为0,FIFO就被读空了。
好了,我们分析完了同步FIFO是如何判断空满的,下面重点放在异步FIFO的原理上。
我曾写过一篇CDC问题的博客,谈到了异步FIFO的设计:
谈谈跨时钟域传输问题(CDC)
这篇博客中说,同步FIFO可以使用计数方式来判断空满,但是异步FIFO不能,因为写指针和读指针根本不在同一个时钟域,计数器无法处理这样的计数。
那么怎么处理呢?
博客里采用的方法是对读写指针的位宽多添1位,这样可以在读写指针相等时,表示FIFO空,而在写指针和读指针最高位不同,而其他位相等时,也即写指针大于读指针一个FIFO深度的数值,表示FIFO满,这不就是意味着写指针绕了一圈,又追上了读指针了吗?
恰是如此,用来解决不用计数而具体判断FIFO空满的问题。
这只是解决了判断空满的一个问题,也就是确定指针的关系!
那下一个问题就是如何判断?
由于读写指针不在同一个时钟域,二者需要同步到同一个时钟域后进行判断大小。
具体的操作就是在各自的时钟域内进行读写操作,同时:
上面是写满判断的情况,下面给出读空判断的可能情形分析:
下面给出手绘示意图:
到此,这一种设计方式的异步FIFO算是讲完了,下面就是设计的问题了。
如果你认真分析了上述异步FIFO的实现方式,那么你会分分钟写出实现代码,我的版本如下:
`timescale 1ns / 1ps
////////////////////////////////////////////////////
// Engineer: Reborn Lee
// Module Name: asy_fifo
// https://blog.csdn.net/Reborn_Lee
////////////////////////////////////////////////////
module asy_fifo#(
parameter DATA_WIDTH = 8,
parameter DATA_DEPTH = 32
)(
input wr_clk,
input wr_rst,
input wr_en,
input [DATA_WIDTH - 1 : 0] wr_data,
output reg full,
input rd_clk,
input rd_rst,
input rd_en,
output reg [DATA_WIDTH - 1 : 0] rd_data,
output reg empty
);
// define FIFO buffer
reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
//define the write and read pointer and
//pay attention to the size of pointer which should be greater one to normal
reg [$clog2(DATA_DEPTH) : 0] wr_pointer = 0, rd_pointer = 0;
//write data to fifo buffer and wr_pointer control
always@(posedge wr_clk) begin
if(wr_rst) begin
wr_pointer <= 0;
end
else if(wr_en) begin
wr_pointer <= wr_pointer + 1;
fifo_buffer[wr_pointer] <= wr_data;
end
end
//read data from fifo buffer and rd_pointer control
always@(posedge rd_clk) begin
if(rd_rst) begin
rd_pointer <= 0;
end
else if(rd_en) begin
rd_pointer <= rd_pointer + 1;
rd_data <= fifo_buffer[rd_pointer];
end
end
//wr_pointer and rd_pointer translate into gray code
wire [$clog2(DATA_DEPTH) : 0] wr_ptr_g, rd_ptr_g;
assign wr_ptr_g = wr_pointer ^ (wr_pointer >>> 1);
assign rd_ptr_g = rd_pointer ^ (rd_pointer >>> 1);
//wr_pointer after gray coding synchronize into read clock region
reg [$clog2(DATA_DEPTH) : 0] wr_ptr_gr, wr_ptr_grr, rd_ptr_gr, rd_ptr_grr;
always@(rd_clk) begin
if(rd_rst) begin
wr_ptr_gr <= 0;
wr_ptr_grr <= 0;
end
else begin
wr_ptr_gr <= wr_ptr_g;
wr_ptr_grr <= wr_ptr_gr;
end
end
//rd_pointer after gray coding synchronize into write clock region
always@(wr_clk) begin
if(wr_rst) begin
rd_ptr_gr <= 0;
rd_ptr_grr <= 0;
end
else begin
rd_ptr_gr <= rd_ptr_g;
rd_ptr_grr <= rd_ptr_gr;
end
end
// judge full or empty
always@(posedge rd_clk) begin
if(rd_rst) empty <= 0;
else if(wr_ptr_grr == rd_ptr_g) begin
empty <= 1;
end
else empty <= 0;
end
always@(posedge wr_clk) begin
if(wr_rst) full <= 0;
else if( (rd_ptr_grr[$clog2(DATA_DEPTH) - 1 : 0] == wr_ptr_g[$clog2(DATA_DEPTH) - 1 : 0])
&& ( rd_ptr_grr[$clog2(DATA_DEPTH)] != wr_ptr_g[$clog2(DATA_DEPTH)] ) ) begin
full <= 1;
end
else full <= 0;
end
endmodule
注意事项
reg [$clog2(DATA_DEPTH) : 0] wr_pointer = 0, rd_pointer = 0;
always@(posedge rd_clk) begin
if(rd_rst) empty <= 0;
else if(wr_ptr_grr == rd_ptr_g) begin
empty <= 1;
end
else empty <= 0;
end
一定要二者相等的下一个读周期empty信号为1;
always@(posedge wr_clk) begin
if(wr_rst) full <= 0;
else if( (rd_ptr_grr[$clog2(DATA_DEPTH) - 1 : 0] == wr_ptr_g[$clog2(DATA_DEPTH) - 1 : 0])
&& ( rd_ptr_grr[$clog2(DATA_DEPTH)] != wr_ptr_g[$clog2(DATA_DEPTH)] ) ) begin
full <= 1;
end
else full <= 0;
end
//wr_pointer and rd_pointer translate into gray code
wire [$clog2(DATA_DEPTH) : 0] wr_ptr_g, rd_ptr_g;
assign wr_ptr_g = wr_pointer ^ (wr_pointer >>> 1);
assign rd_ptr_g = rd_pointer ^ (rd_pointer >>> 1);
当然你用时序逻辑也可以哦。
我们对上述设计进行行为仿真,先给出我的测试文件:
`timescale 1ns/1ps
module asy_fifo_tb;
parameter DATA_WIDTH = 8;
parameter DATA_DEPTH = 16;
reg wr_clk;
reg wr_rst;
reg wr_en;
reg [DATA_WIDTH - 1 : 0] wr_data;
wire full;
reg rd_clk;
reg rd_rst;
reg rd_en;
wire [DATA_WIDTH - 1 : 0] rd_data;
wire empty;
initial begin
wr_clk = 0;
forever begin
#5 wr_clk = ~wr_clk;
end
end
initial begin
rd_clk = 0;
forever begin
#10 rd_clk = ~rd_clk;
end
end
initial begin
wr_rst = 1;
rd_rst = 1;
wr_en = 0;
rd_en = 0;
#30
wr_rst = 0;
rd_rst = 0;
//write data into fifo buffer
@(negedge wr_clk)
wr_data = $random;
wr_en = 1;
repeat(7) begin
@(negedge wr_clk)
wr_data = $random; // write into fifo 8 datas in all;
end
// read parts
@(negedge wr_clk)
wr_en = 0;
@(negedge rd_clk)
rd_en = 1;
repeat(7) begin
@(negedge rd_clk); // read empty
end
@(negedge rd_clk)
rd_en = 0;
//write full
# 150
@(negedge wr_clk)
wr_en = 1;
wr_data = $random;
repeat(15) begin
@(negedge wr_clk)
wr_data = $random;
end
@(negedge wr_clk)
wr_en = 0;
#50 $finish;
end
asy_fifo #(
.DATA_WIDTH(DATA_WIDTH),
.DATA_DEPTH(DATA_DEPTH)
) inst_asy_fifo (
.wr_clk (wr_clk),
.wr_rst (wr_rst),
.wr_en (wr_en),
.wr_data (wr_data),
.full (full),
.rd_clk (rd_clk),
.rd_rst (rd_rst),
.rd_en (rd_en),
.rd_data (rd_data),
.empty (empty)
);
endmodule
由于本博客写的时候有点长,几乎一天了,所以就到这里吧!不得不说的是,异步FIFO的实现方式肯定不只有这一种,还有很多其他实现方式,各位可以自行尝试。
后面如果有更多有关FIFO的有趣知识或者心得体会,我会继续补充!