一,FIFO原理
FIFO(First In First Out)是一种先进先出的数据缓存器,没有外部读写地址线,使用起来非常简单,只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。也正是由于这个特性,使得FIFO可以用作跨时钟域数据传输和数据位宽变换。
二,双端口RAM
FIFO中用来存储数据的器件为双口RAM,首先搭建一个Dual Ram(双口RAM)。我们以一个深度为16,数据位宽为8的Dual Ram为例,框图和时序如下。
Dual Ram读端和写端采用两个时钟,可以实现读写时钟为异步时钟,也可以实现读写同时进行的功能。代码实现如下:
// -----------------------------------------------------------------------------
// Author : RLG
// File : Dual_Ram.v
// -----------------------------------------------------------------------------
`timescale 1ns / 1ps
module Dual_Ram#(
parameter ADDR_WIDTH = 4,
parameter DATA_WIDTH = 8
)(
input wrclk ,
input rdclk ,
input wr_en ,
input rd_en ,
input [ADDR_WIDTH-1:0] wr_addr ,
input [ADDR_WIDTH-1:0] rd_addr ,
input [DATA_WIDTH-1:0] wr_data ,
output reg [DATA_WIDTH-1:0] rd_data
);
/*---------------输入数据打一拍-------------*/
reg [ADDR_WIDTH-1:0] wr_addr_d1;
reg [ADDR_WIDTH-1:0] rd_addr_d1;
reg [DATA_WIDTH-1:0] wr_data_d1;
reg wr_en_d1 ;
reg rd_en_d1 ;
/*----------------数据寄存----------------*/
reg [DATA_WIDTH-1:0] rd_data_out;
reg [DATA_WIDTH-1:0] Data_reg [2**ADDR_WIDTH-1:0];
/*---------------输入数据打拍-------------*/
always @(posedge wrclk ) begin
wr_addr_d1 <= wr_addr;
rd_addr_d1 <= rd_addr;
wr_data_d1 <= wr_data;
wr_en_d1 <= wr_en ;
rd_en_d1 <= rd_en ;
end
/*-------------------写数据-----------------*/
always @(posedge wrclk ) begin
if(wr_en_d1)
Data_reg[wr_addr_d1] <= wr_data_d1;
end
/*-------------------读数据-----------------*/
always @(posedge rdclk ) begin
if(rd_en_d1)
rd_data_out <= Data_reg[rd_addr_d1];
end
/*-----------------输出打一拍----------------*/
always @(posedge rdclk ) begin
rd_data <= rd_data_out;
end
endmodule
二、FIFO地址设计
我们知道FIFO中是没有地址线的,地址靠自身计数器自加1来控制,那么我们很容易想到把外部输入信号wr_addr和rd_addr换成内部信号并用计数器来控制其自加,计数器加满之后直接清零,从0重新开始写/读,循环往复。由于写端和读端的时钟速率不同,就会有快慢的问题,
那么就出现了一个问题,以地址2为例,写入的数据还没有被读出,又被新的数据覆盖了,造成数据丢失;或者写入的数据已经被读出,新的数据还没有写进来,地址2的老数据又被读了一遍,造成数据重复。
为了解决上述问题,引入 full 和 empty 信号来表示内部RAM中的数据写满或者读空,新的框图如下所示。
如何产生full和empty信号呢,我们可以用 wr_addr 和 rd_addr 来做判断,当 wr_clk 大于 rd_clk 时,会产生写满的情况,如下图中黄色部分代表已经写入数据,还未被读取,白色代表数据已被读取,图1中当 waddr>raddr时,waddr-raddr → 1111 - 0001 = 1110 可以表示两者的差值。
图2中当 waddr<raddr 时,计算两者的差值为16 – raddr + waddr → 10000 - 1100 +1010 = 1110,此时的 waddr – raddr → 1010-1100 →1010+0011+0001=1110,两者结果相同,所以无论 waddr 大于 raddr 还是小于 raddr,都可以用 waddr-raddr 来表示写比读多几个数据。此时再引入一个full_limit用来设置一个写满的阈值。当waddr – raddr >= full_limit 时,full信号拉高,停止写入。
同理,读比写快的情况下引入一个empty_limit来作为读空的阈值,当 waddr – raddr <= empty_limit 时。empty信号拉高,停止读出。在实际工程中可以根据实际需要和 fifo 的设计区别灵活设置 full_limit 和empty_limit 的数值。
三、空满信号判断
使用读写地址进行判断空满信号。读地址rd_addr是在读时钟域wr_clk内,空信号empty也是在读时钟域内产生的;而写地址wr_addr是在写时钟域内,且满信号full也是在写时钟域内产生的。 那么,要使用读地址rd_addr与写地址wr_addr对比,产生空信号empty,可以直接对比吗?
答案是不可以。
因为这两个信号处于不同的时钟域内,要做跨时钟域CDC处理,而多bit信号做跨时钟域处理,常用的方法就是使用异步FIFO进行同步,可是我们不是在设计异步FIFO吗?
于是,在这里设计异步FIFO,多bit跨时钟域处理的问题可以转化为单bit跨时钟域的处理,把读写地址转换为格雷码后再进行跨时钟域处理,因为无论多少比特的格雷码,每次加1,只改变1位。把读地址rd_addr转换为格雷码,然后同步到写时钟域wr_clk;同样的,把写地址指wr_addr转换为格雷码,然后同步到读时钟域rd_clk。
二进制转格雷码:二进制的最高位作为格雷码的最高位,次高位的格雷码为二进制的高位和次高位相异或得到,其他位与次高位相同。
代码:
assign wr_gray = (wr_addr >> 1) ^ wr_addr;
assign rd_gray = (rd_addr >> 1) ^ rd_addr;
格雷码转二进制:使用格雷码的最高位作为二进制的最高位,二进制次高位产生过程是使用二进制的高位和次高位格雷码相异或得到,其他位的值与次高位产生过程相同。
代码:
assign wr_bin[ADDR_WIDTH-1] = wr_gray_d2[ADDR_WIDTH-1];
genvar i;
generate
for ( i = 0; i < ADDR_WIDTH-1; i=i+1) begin
assign wr_bin[i] = wr_bin[i+1] ^ wr_gray_d2[i];
end
endgenerate
assign rd_bin[ADDR_WIDTH-1] = rd_gray_d2[ADDR_WIDTH-1];
genvar j;
generate
for ( j = 0; j < ADDR_WIDTH-1; j=j+1) begin
assign rd_bin[j] = rd_bin[j+1] ^ rd_gray_d2[j];
end
endgenerate
四、跨时钟域同步
如何避免漏采和重采,首先考虑一个问题,地址同步要在哪个时钟域进行呢,我们所期望的结果是慢时钟地址同步到快时钟域,以免发生快时钟域信号漏采导致的读空或者写满。至于重采的情况,即慢时钟域信号被多采了一次,只会在判断空满状态时更安全,不会导致读空和写满这种不安全现象的出现。不过这样会产生虚假的full和empty信号,即full信号已经拉高,但ram中仍存有可用的地址,或者empty信号已经拉高,但ram中仍存有可被读出的数据。虽然效率和资源上有一点浪费,但不会发生丢失数据或读错数据的不安全行为。
那怎么实现慢时钟域的信号同步到快时钟域呢?因为若同时读写时出现 empty 则一定是读时钟快于写时钟,所以在判断 empty 状态时,读时钟域为快时钟,把较慢的写时钟同步到读时钟域来判断 empty。同理,若同时读写时出现 full 则一定是写时钟快于读时钟,所以在判断 full 状态时,写时钟域为快时钟,把较慢的读时钟同步到写时钟域来判断 full。以判断empty状态为例,过程如下图所示:
其中B2G模块(二进制转格雷码),G2B模块(格雷码转二进制),empty判断模块均为组合逻辑,所以加一级D触发器以满足时序。圈中的两级D触发器用作消除跨时钟域同步的亚稳态。empty信号在RCLK快于WCLK时产生,中间虽然加入了四级D触发器,导致写地址同步到读时钟域时是之前的老地址,这和之前采重的问题一样,只会让empty的判断更安全,但会造成少许的资源浪费,属于保守但安全的做法。
至此,一个简易的异步fifo就被设计出来了,总体框图如下:
代码:
// -----------------------------------------------------------------------------
// Author : RLG
// File : async_fifo.v
// -----------------------------------------------------------------------------
`timescale 1ns / 1ps
module async_fifo#(
parameter ADDR_WIDTH = 4 ,
parameter DATA_WIDTH = 8 ,
parameter EMPTY_LIMIT= 1'b1 ,
parameter FULL_LIMIT = 4'd15
)(
input wrclk ,
input rdclk ,
input wr_rst_n,
input rd_rst_n,
input wr_en ,
input rd_en ,
input [DATA_WIDTH-1:0] wr_data ,
output reg [DATA_WIDTH-1:0] rd_data ,
output reg empty ,
output reg full
);
/*---------------输入数据打一拍-----------*/
reg [DATA_WIDTH-1:0] wr_data_d1 ;
reg wr_en_d1 ;
reg rd_en_d1 ;
/*-- --------------数据寄存----------------*/
reg [DATA_WIDTH-1:0] Data_reg [2**ADDR_WIDTH-1:0];
/*-- --------------读写地址----------------*/
reg [ADDR_WIDTH-1:0] wr_addr ;
reg [ADDR_WIDTH-1:0] rd_addr ;
/*-- --------------二进制转格雷码------------*/
wire [ADDR_WIDTH-1:0] wr_gray ;
wire [ADDR_WIDTH-1:0] rd_gray ;
reg [ADDR_WIDTH-1:0] wr_gray_d0 ;
reg [ADDR_WIDTH-1:0] rd_gray_d0 ;
reg [ADDR_WIDTH-1:0] wr_gray_d1 ;
reg [ADDR_WIDTH-1:0] rd_gray_d1 ;
reg [ADDR_WIDTH-1:0] wr_gray_d2 ;
reg [ADDR_WIDTH-1:0] rd_gray_d2 ;
/*-- --------------格雷码转二进制------------*/
wire [ADDR_WIDTH-1:0] wr_bin ;
wire [ADDR_WIDTH-1:0] rd_bin ;
reg [ADDR_WIDTH-1:0] rd_bin_d0 ;
reg [ADDR_WIDTH-1:0] wr_bin_d0 ;
/*----------------empty 判读---------------*/
wire empty_logic ;
/*----------------full 判读---------------*/
wire full_logic ;
/*---------------------------------------*\
输入数据打拍
\*---------------------------------------*/
always @(posedge wrclk ) begin
wr_data_d1 <= wr_data;
wr_en_d1 <= wr_en ;
rd_en_d1 <= rd_en ;
end
/*---------------------------------------*\
写地址
\*---------------------------------------*/
always @(posedge wrclk ) begin
if(~wr_rst_n)
wr_addr<= 0;
else if(wr_en_d1 && ~full) begin
if(wr_addr == 'd15)
wr_addr <= 0;
else
wr_addr <= wr_addr + 1'b1;
end
end
/*---------------------------------------*\
读地址
\*---------------------------------------*/
always @(posedge rdclk ) begin
if(~rd_rst_n)
rd_addr<= 0;
else if(rd_en_d1 && ~empty) begin
if(rd_addr == 'd15)
rd_addr <= 0;
else
rd_addr <= rd_addr + 1'b1;
end
end
/*---------------------------------------*\
写数据
\*---------------------------------------*/
always @(posedge wrclk ) begin
if(wr_en_d1 && ~full)
Data_reg[wr_addr] <= wr_data_d1;
end
/*---------------------------------------*\
读数据
\*---------------------------------------*/
always @(posedge rdclk ) begin
if(rd_en_d1 && ~empty)
rd_data <= Data_reg[rd_addr];
end
/*---------------------------------------*\
二进制转格雷码
\*---------------------------------------*/
assign wr_gray = (wr_addr >> 1) ^ wr_addr;
assign rd_gray = (rd_addr >> 1) ^ rd_addr;
always @(posedge wrclk ) begin
wr_gray_d0 <= wr_gray;
end
always @(posedge rdclk ) begin
rd_gray_d0 <= rd_gray;
end
/*---------------------------------------*\
格雷码转二进制
\*---------------------------------------*/
always @(posedge wrclk ) begin
if(!wr_rst_n)begin
rd_gray_d1 <= 0;
rd_gray_d2 <= 0;
end
else begin
rd_gray_d1 <= rd_gray_d0;
rd_gray_d2 <= rd_gray_d1;
end
end
always @(posedge rdclk ) begin
if (!rd_rst_n) begin
wr_gray_d1 <= 0;
wr_gray_d2 <= 0;
end
else begin
wr_gray_d1 <= wr_gray_d0;
wr_gray_d2 <= wr_gray_d1;
end
end
assign wr_bin[ADDR_WIDTH-1] = wr_gray_d2[ADDR_WIDTH-1];
genvar i;
generate
for ( i = 0; i < ADDR_WIDTH-1; i=i+1) begin
assign wr_bin[i] = wr_bin[i+1] ^ wr_gray_d2[i];
end
endgenerate
assign rd_bin[ADDR_WIDTH-1] = rd_gray_d2[ADDR_WIDTH-1];
genvar j;
generate
for ( j = 0; j < ADDR_WIDTH-1; j=j+1) begin
assign rd_bin[j] = rd_bin[j+1] ^ rd_gray_d2[j];
end
endgenerate
always @(posedge wrclk) begin
wr_bin_d0 <= wr_bin;
end
always @(posedge rdclk) begin
rd_bin_d0 <= rd_bin;
end
/*---------------------------------------*\
empty
\*---------------------------------------*/
assign empty_logic = ((wr_bin_d0 - rd_addr) <= EMPTY_LIMIT)? 1'b1 : 1'b0;
always @(posedge rdclk) begin
empty <= empty_logic;
end
/*---------------------------------------*\
full
\*---------------------------------------*/
assign full_logic = ((wr_addr - rd_bin_d0) >= FULL_LIMIT)? 1'b1 : 1'b0;
always @(posedge wrclk) begin
full <= full_logic;
end
endmodule
仿真代码:
`timescale 1ns / 1ps
module tb_async_fifo;
parameter ADDR_WIDTH = 4;
parameter DATA_WIDTH = 8;
parameter EMPTY_LIMIT = 1'b1;
parameter FULL_LIMIT = 4'd15;
reg wr_rst_n;
reg rd_rst_n;
reg wr_en;
reg rd_en;
reg [DATA_WIDTH-1:0] wr_data;
wire [DATA_WIDTH-1:0] rd_data;
wire empty;
wire full;
reg wr_clk;
reg rd_clk;
initial begin
wr_clk = 0;
rd_clk = 0;
wr_rst_n = 0;
rd_rst_n = 0;
wr_en = 0;
rd_en = 0;
#20
wr_rst_n = 1;
rd_rst_n = 1;
#20
wr_en = 1;
#30
rd_en = 1;
end
always #10 wr_clk = ~wr_clk;
always #5 rd_clk = ~rd_clk;
always @(posedge wr_clk ) begin
if(!wr_rst_n)
wr_data <= 0;
else if(wr_data == 15)
wr_data <= 0;
else if(wr_en)
wr_data <= wr_data + 1;
end
async_fifo #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.EMPTY_LIMIT(EMPTY_LIMIT),
.FULL_LIMIT(FULL_LIMIT)
) inst_async_fifo (
.wrclk (wr_clk),
.rdclk (rd_clk),
.wr_rst_n (wr_rst_n),
.rd_rst_n (rd_rst_n),
.wr_en (wr_en),
.rd_en (rd_en),
.wr_data (wr_data),
.rd_data (rd_data),
.empty (empty),
.full (full)
);
endmodule
仿真波形:
五,总结
在处理跨时钟域时,转换为格雷码处理。