FIFO在硬件上是一种地址依次自增的Simple Dual Port RAM,按读数据和写数据工作的时钟域是否相同分为同步FIFO和异步FIFO,其中同步FIFO是指读时钟和写时钟为同步时钟,常用于数据缓存和数据位宽转换;异步FIFO通常情况下是指读时钟和写时钟频率有差异,即由两个异步时钟驱动的FIFO,由于读写操作是独立的,故常用于多比特数据跨时钟域处理。本文仅讨论异步FIFO的设计。
因为FIFO的硬件本质是一块Simple Dual Port RAM,无论它的内部结构和原理如何复杂,最核心的部分都只是对这个RAM的读写操作而已,所以我们先设计RAM部分的RTL。一个Simple Dual Port RAM应该由读写时钟、读写使能、读写地址总线和读写数据总线接口构成,这里为了在功能仿真中初始化读数据寄存器,再加入一个读复位接口。Verilog代码如下:
module DPRAM #
(
parameter WIDTH = 16 , // DPRAM数据总线宽度
parameter DEPTH = 16 , // DPRAM存储深度
parameter ADDR = 4 // DPRAM地址总线宽度
)
(
input wrclk , // 写时钟
input rdclk , // 读时钟
input rd_rst_n, // 读复位
input wr_en , // 写使能
input rd_en , // 读使能
input [WIDTH-1:0] wr_data , // 写数据
output reg [WIDTH-1:0] rd_data , // 读数据
input [ADDR-1:0] wr_addr , // 写地址
input [ADDR-1:0] rd_addr // 读地址
);
reg [WIDTH-1:0] DPRAM [DEPTH-1:0];
// RAM写数据
always @(posedge wrclk) begin
if (wr_en)
DPRAM[wr_addr] <= wr_data;
end
// RAM读数据
always @(posedge rdclk or negedge rd_rst_n) begin
if(!rd_rst_n)
rd_data <= 'b0;
else if (rd_en)
rd_data <= DPRAM[rd_addr];
end
endmodule
其中 WIDTH 是RAM数据总线的位宽,DEPTH 是RAM的存储深度(即RAM中可以存下 DEPTH 个宽度为 WIDTH 的数据),ADDR 是地址总线的宽度(即DEPTH = 2^ADDR ,异步FIFO中深度必须是2^n,原因在后面阐述)。
接下来需要解决的是如何控制这个RAM来实现异步FIFO的功能,在实现这部分功能前先来捋一捋异步FIFO的一些重要概念:
1、FIFO数据宽度:FIFO一次读写的数据位宽。(与RAM数据位宽相同)
2、FIFO存储深度:FIFO可存储的固定位宽数据的个数。(与RAM存储深度相同)
3、读时钟:在每个读时钟的边沿来临时读数据。
4、写时钟:在每个写时钟的边沿来临时写数据。
5、读指针:指向下一个要读的地址,读完后自动加1。
6、写指针:指向下一个要写的地址,写完后自动加1。
读写指针其实就是读写的地址,只不过不能任意设置,只能连续自增。
7、空/满标志:为了保证FIFO的正确读写,而不发生写溢出或读空的情况,需要提供写满和读空的标志来提醒外部控制器此状态下不能再进行写/读操作。
根据上述重要概念可以定义出异步FIFO的基本对外接口:写时钟、读时钟、写使能、读使能、写满标志、读空标志、写入数据总线、读出数据总线以及读/写复位。因为我们所设计的是异步FIFO,它的读写部分不是在同一个时钟域内工作,所以可以将它们划分为写时钟域和读时钟域,在两个时钟域各自控制本时钟域内的信号,并将两个时钟域内的一些有关信号进行跨时钟域处理来联合判断FIFO状态。
上图为异步FIFO结构示意图,这里简述一下其结构,不懂也没关系,下面会详细讲解各部分的原理和作用。
由图可见,异步FIFO的核心部件就是一个 Simple Dual Port RAM ;左右两边的长条矩形是地址控制器,负责控制地址自增、将二进制地址转为格雷码以及解格雷码;下面的两对D触发器 sync_r2w 和 sync_w2r 是同步器,负责将写地址同步至读时钟域、将读地址同步至写时钟域。
RAM的地址由自然二进制表示,让其地址随着时钟自增即可满足FIFO指针递增的要求;注意各自时钟域开启使能端并且没有写满/读空的情况下才能让地址自增。Verilog代码如下:
// ******************************** 写时钟域 ******************************** //
// 二进制写地址递增
always @(posedge wrclk or posedge wr_rst_n) begin
if (!wr_rst_n) begin
wr_bin <= 'b0;
end
else if ( wr_en == 1'b1 && wr_full == 1'b0 ) begin
wr_bin <= wr_bin + 1'b1;
end
else begin
wr_bin <= wr_bin;
end
end
// ******************************** 读时钟域 ******************************** //
always @(posedge rdclk or posedge rd_rst_n) begin
if (!rd_rst_n) begin
rd_bin <= 'b0;
end
else if ( rd_en == 1'b1 && rd_empty == 1'b0 ) begin
rd_bin <= rd_bin + 1'b1;
end
else begin
rd_bin <= rd_bin;
end
end
异步FIFO是通过比较读指针和写指针的位置来判断FIFO是否写满或读空,但是不可以直接比较两个指针,因为他们属于不同时钟域,直接相比可能会产生亚稳态从而引起误判,这就需要将两个指针分别进行跨时钟域处理,然后再判断。但是存在一个问题,自然二进制编码的地址在状态翻转的时候是多位变化,这就可能会产生竞争现象并有可能被另一个时钟域的触发器采样到,从而引发误判。最容易的解决方法就是将自然二进制编码的地址转为格雷码编码的地址。
从上图可以看出,格雷码如果每2^n个数一循环,首尾两个格雷码仍然是只有一位变化,如果不是2^n个数,那么首尾数据就不是仅有一位变化,那就不是真正的格雷码,所以这也是异步FIFO的存储深度只能是2^n的原因。
自然二进制码转换成二进制格雷码,其法则是保留自然二进制码的最高位作为格雷码的最高位,而次高位格雷码为二进制码的高位与次高位相异或,而格雷码其余各位与次高位的求法相类似。 如下图:
虽然将二进制转成了格雷码,但仍存在一个隐藏的竞争冒险问题,如下图所示:
上图是从二进制地址自增和转格雷码的RTL连接图。可以看出,A点输出的是二进制数据,可能存在多位变化,但是经过转格雷码电路 bin to gray (组合逻辑电路)就不会产生竞争现象了吗,仔细想想,当A点二进制数据处于变化中的中间态时,由于 bin to gray 是组合逻辑电路,那么B点也会出现中间态二进制数据对应的格雷码数据,这个中间数据就有可能被读时钟域采样到,从而再次引发误判,解决方法就是在二进制转格雷码电路后打一拍转为时序逻辑电路,这样在写时钟的驱动下,C点数据每次更新必然只有一位在变化,就避免了出现冒险现象。有人可能会问了,上面不是说写满和读空标志都是由两个地址指针判断的吗?对格雷码地址打一拍会不会引起标志位来的迟了,这里先说一下标志位是不会来迟的,原因在后面的标志位判断方法中会讲到。
二进制转格雷码Verilog代码如下:
// 写地址:二进制转格雷码
always @(posedge wrclk or posedge wr_rst_n) begin
if (!wr_rst_n) begin
wr_gray <= 'b0;
end
else begin
wr_gray <= { wr_bin[PTR], wr_bin[PTR:1] ^ wr_bin[PTR-1:0] };
end
end
// 读地址:二进制转格雷码
always @(posedge rdclk or posedge rd_rst_n) begin
if (!rd_rst_n) begin
rd_gray <= 'b0;
end
else begin
rd_gray <= { rd_bin[PTR], rd_bin[PTR:1] ^ rd_bin[PTR-1:0] };
end
end
将二进制地址处理为格雷码地址的目的就是为了将其同步到另一个时钟域时不会引起竞争冒险,在前面的异步FIFO结构图中也可以看见两个D触发器构成的同步器对格雷码地址打两拍从而同步至另一个时钟域,这里直接给出同步器的Verilog代码:
// 格雷码读地址同步至写时钟域
always @(posedge wrclk or posedge wr_rst_n) begin
if(!wr_rst_n) begin
rd_gray_ff1 <= 'b0;
rd_gray_ff2 <= 'b0;
end
else begin
rd_gray_ff1 <= rd_gray;
rd_gray_ff2 <= rd_gray_ff1;
end
end
// 格雷码写地址同步至读时钟域
always @(posedge rdclk or posedge rd_rst_n) begin
if(!rd_rst_n) begin
wr_gray_ff1 <= 'b0;
wr_gray_ff2 <= 'b0;
end
else begin
wr_gray_ff1 <= wr_gray;
wr_gray_ff2 <= wr_gray_ff1;
end
end
为什么要对格雷码解码?因为二进制下数据的规律更明显,便于后续判断标志位。
二进制格雷码转换成自然二进制码,其法则是保留格雷码的最高位作为自然二进制码的最高位,而次高位自然二进制码为高位自然二进制码与次高位格雷码相异或,而自然二进制码的其余各位与次高位自然二进制码的求法相类似。
其电路图应如下所示:
在数电书中我们曾学过串行进位加法器,它是一种后一位计算依靠前一位进位的组合逻辑电路,上图的解格雷码电路也与其类似,前一位的输出依靠后一位的异或结果,这会带来更大的组合链延迟并产生竞争现象,不过在位宽不大的情况下对于正确产生标志位的影响概率较小,可以使用组合逻辑进行解格雷,当然也可以对解格雷后的二进制数据打一拍消除竞争现象,这里因为地址总线只有4位,仅使用组合逻辑解格雷。Verilog代码如下:
parameter WIDTH = 16, // FIFO数据总线位宽
parameter PTR = 4 // FIFO存储深度(bit数,深度只能是2^n个)
// 解格雷码电路循环变量
integer i ;
integer j ;
// 同步后的写地址解格雷
always @(*) begin
wr_bin_rd[PTR] = wr_gray_ff2[PTR];
for ( j=PTR-1; j>=0; j=j-1 )
wr_bin_rd[j] = wr_bin_rd[j+1] ^ wr_gray_ff2[j];
end
// 同步后的读地址解格雷
always @(*) begin
rd_bin_wr[PTR] = rd_gray_ff2[PTR];
for ( i=PTR-1; i>=0; i=i-1 )
rd_bin_wr[i] = rd_bin_wr[i+1] ^ rd_gray_ff2[i];
end
这里推荐使用for语句(仅这里),和C/C++不同,Verilog中的for语句是将所以可能的结构全部展开成电路(因为属于组合逻辑,在仿真中第0ns就展开完毕),并且可以通过改变 parameter变量的值来改变电路层级,较为方便,但其他地方慎用for语句,因为可能会综合出较大面积的电路,浪费LUT资源。
“读空” 标志的产生比较好理解,如果读指针=写指针,说明FIFO就已经读空了,意思是读指针赶上了写指针,那么写指针之前的地址空间都已经被读指针遍历了,就说明FIFO已经读空了。
在讲“写满” 标志之前,先来看一张图:
这张图是五位二进制和格雷码的对应表,可以看出,低四位二进制每16个数据一循环,而超出16个数据后最高位会从0变成1,这第五位意味着什么?意味着这一位可以表示循环的次数,即这一位为1就代表了低二进制已经循环过16个数据了,正在处于第二轮循环中。
这给如何判断 “写满” 标志带来了启发,何谓 “写满” ?就是写指针正好超过了读指针一整个存储深度并同处于同一个位置,就好比两个运动员绕着运动场跑步,第一个运动员盖过了第二个运动员一圈,即使他们处于操场的同一个位置,但第一个运动员实际上比第二个运动员多跑了一圈。如何表示写指针盖过了读指针一圈呢?就使用上面说的第五位来判断,其实有效地址只有四位,第五位是用来存储盖过的圈数的。所以可以看见,前面的代码中,地址总线宽度其实都是5位的。
所以判断 “写满” 标志的方法就是:写指针和读指针最高位的数据不同,而其他位都相同。
这里给出两个标志判断的Verilog代码:
reg [PTR:0] rd_bin_wr ; // 同步到写时钟域的二进制读地址
reg [PTR:0] wr_bin_rd ; // 同步到读时钟域的二进制写地址
reg [PTR:0] rd_bin ; // 二进制读地址
reg [PTR:0] wr_bin ; // 二进制写地址
// 写时钟域产生写满标志
always @(*) begin
if( (wr_bin[PTR] != rd_bin_wr[PTR]) && (wr_bin[PTR-1:0] == rd_bin_wr[PTR-1:0]) )
begin
wr_full = 1'b1;
end
else begin
wr_full = 1'b0;
end
end
// 读时钟域产生读空标志
always @(*) begin
if( wr_bin_rd == rd_bin )
rd_empty = 1'b1;
else
rd_empty = 1'b0;
end
先给出异步FIFO实现的全部Verilog代码:
`timescale 1ns / 1ns
//
// Company: Xidian University
// Engineer: Xumingwei
//
// Create Date: 2020/11/06 20:52:45
// Design Name: 异步FIFO控制器
// Module Name: ASFIFO
// Project Name: ASFIFO
// Target Devices: XC7K325T
// Tool Versions: Vivado 2019.2
// Description: 本模块为异步FIFO控制器,将RAM操作为ASFIFO
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module ASFIFO#
(
parameter WIDTH = 16, // FIFO数据总线位宽
parameter PTR = 4 // FIFO存储深度(bit数,深度只能是2^n个)
)
(
// write interface
input wrclk , // 写时钟
input wr_rst_n, // 写指针复位
input [WIDTH-1:0] wr_data , // 写数据总线
input wr_en , // 写使能
output reg wr_full , // 写满标志
//read interface
input rdclk , // 读时钟
input rd_rst_n, // 读指针复位
input rd_en , // 读使能
output [WIDTH-1:0] rd_data , // 读数据输出
output reg rd_empty // 读空标志
);
// 写时钟域信号定义
reg [PTR:0] wr_bin ; // 二进制写地址
reg [PTR:0] wr_gray ; // 格雷码写地址
reg [PTR:0] rd_gray_ff1 ; // 格雷码读地址同步寄存器1
reg [PTR:0] rd_gray_ff2 ; // 格雷码读地址同步寄存器2
reg [PTR:0] rd_bin_wr ; // 同步到写时钟域的二进制读地址
// 读时钟域信号定义
reg [PTR:0] rd_bin ; // 二进制读地址
reg [PTR:0] rd_gray ; // 格雷码读地址
reg [PTR:0] wr_gray_ff1 ; // 格雷码写地址同步寄存器1
reg [PTR:0] wr_gray_ff2 ; // 格雷码写地址同步寄存器2
reg [PTR:0] wr_bin_rd ; // 同步到读时钟域的二进制写地址
// 解格雷码电路循环变量
integer i ;
integer j ;
// DPRAM控制信号
wire dpram_wr_en ; // DPRAM写使能
wire [PTR-1:0] dpram_wr_addr ; // DPRAM写地址
wire [WIDTH-1:0] dpram_wr_data ; // DPRAM写数据
wire dpram_rd_en ; // DPRAM读使能
wire [PTR-1:0] dpram_rd_addr ; // DPRAM读地址
wire [WIDTH-1:0] dpram_rd_data ; // DPRAM读数据
// ******************************** 写时钟域 ******************************** //
// 二进制写地址递增
always @(posedge wrclk or posedge wr_rst_n) begin
if (!wr_rst_n) begin
wr_bin <= 'b0;
end
else if ( wr_en == 1'b1 && wr_full == 1'b0 ) begin
wr_bin <= wr_bin + 1'b1;
end
else begin
wr_bin <= wr_bin;
end
end
// 写地址:二进制转格雷码
always @(posedge wrclk or posedge wr_rst_n) begin
if (!wr_rst_n) begin
wr_gray <= 'b0;
end
else begin
wr_gray <= { wr_bin[PTR], wr_bin[PTR:1] ^ wr_bin[PTR-1:0] };
end
end
// 格雷码读地址同步至写时钟域
always @(posedge wrclk or posedge wr_rst_n) begin
if(!wr_rst_n) begin
rd_gray_ff1 <= 'b0;
rd_gray_ff2 <= 'b0;
end
else begin
rd_gray_ff1 <= rd_gray;
rd_gray_ff2 <= rd_gray_ff1;
end
end
// 同步后的读地址解格雷
always @(*) begin
rd_bin_wr[PTR] = rd_gray_ff2[PTR];
for ( i=PTR-1; i>=0; i=i-1 )
rd_bin_wr[i] = rd_bin_wr[i+1] ^ rd_gray_ff2[i];
end
// 写时钟域产生写满标志
always @(*) begin
if( (wr_bin[PTR] != rd_bin_wr[PTR]) && (wr_bin[PTR-1:0] == rd_bin_wr[PTR-1:0]) ) begin
wr_full = 1'b1;
end
else begin
wr_full = 1'b0;
end
end
// ******************************** 读时钟域 ******************************** //
always @(posedge rdclk or posedge rd_rst_n) begin
if (!rd_rst_n) begin
rd_bin <= 'b0;
end
else if ( rd_en == 1'b1 && rd_empty == 1'b0 ) begin
rd_bin <= rd_bin + 1'b1;
end
else begin
rd_bin <= rd_bin;
end
end
// 读地址:二进制转格雷码
always @(posedge rdclk or posedge rd_rst_n) begin
if (!rd_rst_n) begin
rd_gray <= 'b0;
end
else begin
rd_gray <= { rd_bin[PTR], rd_bin[PTR:1] ^ rd_bin[PTR-1:0] };
end
end
// 格雷码写地址同步至读时钟域
always @(posedge rdclk or posedge rd_rst_n) begin
if(!rd_rst_n) begin
wr_gray_ff1 <= 'b0;
wr_gray_ff2 <= 'b0;
end
else begin
wr_gray_ff1 <= wr_gray;
wr_gray_ff2 <= wr_gray_ff1;
end
end
// 同步后的写地址解格雷
always @(*) begin
wr_bin_rd[PTR] = wr_gray_ff2[PTR];
for ( j=PTR-1; j>=0; j=j-1 )
wr_bin_rd[j] = wr_bin_rd[j+1] ^ wr_gray_ff2[j];
end
// 读时钟域产生读空标志
always @(*) begin
if( wr_bin_rd == rd_bin )
rd_empty = 1'b1;
else
rd_empty = 1'b0;
end
// RTL双口RAM例化
DPRAM
# ( .WIDTH(16), .DEPTH(16), .ADDR(4) )
U_DPRAM
(
.wrclk (wrclk ),
.rdclk (rdclk ),
.rd_rst_n (rd_rst_n ),
.wr_en (dpram_wr_en ),
.rd_en (dpram_rd_en ),
.wr_data (dpram_wr_data ),
.rd_data (dpram_rd_data ),
.wr_addr (dpram_wr_addr ),
.rd_addr (dpram_rd_addr )
);
// 产生DPRAM读写控制信号
assign dpram_wr_en = ( wr_en == 1'b1 && wr_full == 1'b0 )? 1'b1 : 1'b0;
assign dpram_wr_data = wr_data;
assign dpram_wr_addr = wr_bin[PTR-1:0];
assign dpram_rd_en = ( rd_en == 1'b1 && rd_empty == 1'b0 )? 1'b1 : 1'b0;
assign rd_data = dpram_rd_data;
assign dpram_rd_addr = rd_bin[PTR-1:0];
endmodule
以及testbench代码:
`timescale 1ns / 1ns
//
// Company: Xidian University
// Engineer: Xumingwei
//
// Create Date: 2020/11/06 23:54:40
// Design Name: ASFIFO's testbench
// Module Name: ASFIFO_tb
// Project Name:
// Target Devices:
// Tool Versions:
// Description: 异步FIFO的激励文件
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module ASFIFO_tb;
parameter WIDTH = 16;
parameter PTR = 4;
// 写时钟域tb信号定义
reg wrclk ;
reg wr_rst_n ;
reg [WIDTH-1:0] wr_data ;
reg wr_en ;
wire wr_full ;
// 读时钟域tb信号定义
reg rdclk ;
reg rd_rst_n ;
wire [WIDTH-1:0] rd_data ;
reg rd_en ;
wire rd_empty ;
// testbench自定义信号
reg init_done ; // testbench初始化结束
// FIFO初始化
initial begin
// 输入信号初始化
wr_rst_n = 1 ;
rd_rst_n = 1 ;
wrclk = 0 ;
rdclk = 0 ;
wr_en = 0 ;
rd_en = 0 ;
wr_data = 'b0 ;
init_done = 0 ;
// FIFO复位
#30 wr_rst_n = 0;
rd_rst_n = 0;
#30 wr_rst_n = 1;
rd_rst_n = 1;
// 初始化完毕
#30 init_done = 1;
end
// 写时钟
always
#2 wrclk = ~wrclk;
// 读时钟
always
#4 rdclk = ~rdclk;
// 读写控制
always @(*) begin
if(init_done) begin
// 写数据
if( wr_full == 1'b1 )begin
wr_en = 0;
end
else begin
wr_en = 1;
end
end
end
always @(*) begin
if(init_done) begin
// 读数据
if( rd_empty == 1'b1 )begin
rd_en = 0;
end
else begin
rd_en = 1;
end
end
end
// 写入数据自增
always @(posedge wrclk) begin
if(init_done) begin
if( wr_full == 1'b0 )
wr_data <= wr_data + 1;
else
wr_data <= wr_data;
end
else begin
wr_data <= 'b0;
end
end
// 异步fifo例化
ASFIFO
# ( .WIDTH(16), .PTR(4) )
U_ASFIFO
(
.wrclk (wrclk ),
.wr_rst_n (wr_rst_n ),
.wr_data (wr_data ),
.wr_en (wr_en ),
.wr_full (wr_full ),
.rdclk (rdclk ),
.rd_rst_n (rd_rst_n ),
.rd_data (rd_data ),
.rd_en (rd_en ),
.rd_empty (rd_empty )
);
endmodule
Modelsim 仿真波形如下:
可见读数据对写数据实现了多比特跨时钟域处理,这是异步FIFO的一个常用用途。
1、对格雷码编码电路打拍和跨时钟域的打两拍会导致其中一个时钟域跨过来的地址数据滞后,这样会导致FIFO工作异常吗?
不会。在写时钟域中,写地址是本地数据,无延迟,读地址是跨时钟域而来,有一定的延迟。“写满” 的判断条件是读写指针高位不同,其他位相同,当 “写满” 信号出现时,意味着写指针和读指针的低四位已经相同,而此时的读指针可能不是真正的读指针,也就是假设参与判断的读指针是5时,实际的读指针可能已经到8了,这说明读出的数据更多了,也就是 “写满” 标志来早了,其实这并不是真正的写满,而是 “假满” 。
在读时钟域中,读地址是本地数据,无延迟,写地址是跨时钟域而来,有一定的延迟。“读空” 的判断条件是读写指针相同,当 “读空” 信号出现时,意味着读指针和写指针已经相同,而此时的写指针可能不是真正的写指针,也就是假设参与判断的写指针是3时,实际的写指针可能已经到6了,这说明写入的数据更多了,也就是 “读空” 标志来早了,其实这并不是真正的读空,而是 “假空” 。
但 “写满” 和 “读空” 标志来早并不会影响异步FIFO的正常工作,仅相当于FIFO的有效深度减小了,也可以看做是对FIFO的一种保护,防止写溢出或读溢出。
2、异步FIFO的读写时钟频率不同,在进行两级跨时钟同步时,慢时钟会对快时钟域下的数据产生 “漏采” 现象,这会影响异步FIFO的功能吗?
不会。当读慢写快时,将格雷码格式的写指针同步到读时钟域,可能会发生漏采现象。比如写指针从1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8,而读时钟仅采样到了2 -> 4 -> 6,但这些漏掉的读指针并不会影响FIFO的逻辑功能,当读时钟采样到6时,它以为采样到了最新的数据6,而实际的最新的数据已经写到了8,和第一个问题一样,这也会让 “读空” 标志来早,从而保护FIFO。