代码来自asic world 和paper“ Simulation and Synthesis Techniques for Asynchronous FIFO Design”,文章解说均为自己的理解,如果有错误欢迎纠正~
What is FIFO?
FIFO 是一个满足“先进先出”的储存数据的结构,如果我以“A->B->C”的顺序存入数据,由于我先存入的C,那么我先读取的内容依然是C,最后读取到A。
Introduction:
异步fifo可以满足不同时钟域的数据读写问题,在fpga等时钟不止一个的系统里,当数据从(faster domain)快时钟域传递到慢的时钟域时(slow domain),需要fifo来满足数据的降频率读。所以异步fifo的设计非常重要。
对于同步FIFO(FIFO的读和写都发生于同一个时钟沿0。用一个counter计算写入的数量:递增(FIFO仅写入),递减(FIFO仅读)或保持(不写入和读取,或同时写入和读取操作)。当FIFO计数器达到预定的满值时,将FIFO的信号置为“满”,当FIFO计数器为零时FIFO为空。由于读和写都发生在相同的时钟,所以可以当作普通的储存器使用。
对于异步FIFO设计,则不能使用递增 - 递减FIFO计数器,因为它需要两个不同的异步时钟来控制计数器。要确定异步FIFO设计的满和空状态,必须比较写和读指针。
Principle:
要想理解FIFO设计,需先了解FIFO指针的工作原理。写指针总是指向要写入的下一个字;因此,在复位时,读写指针都清零,这也恰好是要写入的下一个FIFO字位置。在FIFO写操作中,写指针所指向的存储单元被写入,然后写指针递增以指向要写入的下一个位置。 类似地,读指针总是指向要读取的当前FIFO字。再次复位时,两个指针都复位为零,FIFO为空,读指针指向无效数据(因为FIFO为空,空标志置位)。一旦第一个数据字写入FIFO,写指针就会递增,空标志被清除,此时正在寻址第一个FIFO存储字内容的读指针会立即将第一个有效字传到FIFO的数据输出端口,由接收器逻辑读取。
但是,当读指针和写指针都相等时,FIFO有可能为空,也有可能为满:当两个指针在复位操作期间被复位为零时,或者当读指针赶上写指针时,读取了FIFO中的最后一个字,fifo空。当写指针写了一圈并再次赶上读指针,与其相等时,fifo为满。
这里介绍了两种常见的异步fifo设计方法。第一种的代码比较简单,第二种的综合出的电路更加安全。
设计理念一:当读指针读到最后一位,且马上追上写指针时,将“预设空”变量设为1。 当写指针写到最后一位,且马上追上读指针时,写指针已经多跑了一圈了,将“预设满”变量设为1。原理如下图:
如果写指针指向“1000”,读指针指向“0000”,此时写指针最高位“1”和读指针次高位“0”不同,写指针次高位“0”和读指针最高位“0”相同时,写指针马上追上读指针,即将达到fifio“预设满”状态。反之如果读指针此时为1000, 写指针此时为0000, 那么读指针即将追上写指针,fifo内容马上被读完,此时将“预设空”变量设为1。实现代码如下:
assign Set_Status = (pNextWordToWrite[ADDRESS_WIDTH-2] ~^ pNextWordToRead[ADDRESS_WIDTH-1]) &
(pNextWordToWrite[ADDRESS_WIDTH-1] ^ pNextWordToRead[ADDRESS_WIDTH-2]);
assign Rst_Status = (pNextWordToWrite[ADDRESS_WIDTH-2] ^ pNextWordToRead[ADDRESS_WIDTH-1]) &
(pNextWordToWrite[ADDRESS_WIDTH-1] ~^ pNextWordToRead[ADDRESS_WIDTH-2]);
设计中指针的译码使用的是格雷码,因为二进制码递增可能要改变好几位,格雷码只允许每个时钟转换一位改变,消除了在同一时钟边沿尝试同步多个改变信号的问题。 比如在二进制中,由1变为2需要经历两个阶段"0001"->"0000"->"0010",这就导致采样的时候可能得到0000而不是0010,从而引发问题。
Design Code:
module asynFifo
#(parameter DATA_WIDTH = 8,
ADDRESS_WIDTH = 4,
FIFO_DEPTH = (1 << ADDRESS_WIDTH))
//Reading port
(output reg [DATA_WIDTH-1:0] Data_out,
output reg Empty_out,
input wire ReadEn_in,
input wire RClk,
//Writing port.
input wire [DATA_WIDTH-1:0] Data_in,
output reg Full_out,
input wire WriteEn_in,
input wire WClk,
input wire Clear_in
);
reg [DATA_WIDTH-1:0] Mem [FIFO_DEPTH-1:0]; //memory
wire [ADDRESS_WIDTH-1:0] pNextWordToWrite, pNextWordToRead; //r/w pointers
wire EqualAddresses;
wire NextWriteAddressEn, NextReadAddressEn;
wire Set_Status, Rst_Status;
reg Status;
wire PresetFull, PresetEmpty;
//(Uses a dual-port RAM).
//read data logic:
always @ (posedge RClk)
if (ReadEn_in & !Empty_out) //读信号,fifo不是空的
Data_out <= Mem[pNextWordToRead]; //输出指针指向的内容
//write data logic:
always @ (posedge WClk)
if (WriteEn_in & !Full_out) //写信号,fifo不是满的
Mem[pNextWordToWrite] <= Data_in; //指针指向的地址处写入
//Fifo addresses support logic:
//'Next Addresses' enable logic:
assign NextWriteAddressEn = WriteEn_in & ~Full_out; //写信号&fifo空
assign NextReadAddressEn = ReadEn_in & ~Empty_out; //读信号&fifo满
//Addreses (Gray counters) logic:
GrayCounter GrayCounter_pWr
(.GrayCount_out(pNextWordToWrite),
.Enable_in(NextWriteAddressEn),
.Clear_in(Clear_in),
.Clk(WClk)
);
GrayCounter GrayCounter_pRd
(.GrayCount_out(pNextWordToRead),
.Enable_in(NextReadAddressEn),
.Clear_in(Clear_in),
.Clk(RClk)
);
//'EqualAddresses' logic:
assign EqualAddresses = (pNextWordToWrite == pNextWordToRead);
//'Quadrant selectors' logic:
assign Set_Status = (pNextWordToWrite[ADDRESS_WIDTH-2] ~^ pNextWordToRead[ADDRESS_WIDTH-1]) &
(pNextWordToWrite[ADDRESS_WIDTH-1] ^ pNextWordToRead[ADDRESS_WIDTH-2]);
assign Rst_Status = (pNextWordToWrite[ADDRESS_WIDTH-2] ^ pNextWordToRead[ADDRESS_WIDTH-1]) &
(pNextWordToWrite[ADDRESS_WIDTH-1] ~^ pNextWordToRead[ADDRESS_WIDTH-2]);
//'Status' latch logic:
always @ (Set_Status, Rst_Status, Clear_in) //D Latch w/ Asynchronous Clear & Preset.
if (Rst_Status | Clear_in)
Status = 0; //Going 'Empty'.
else if (Set_Status)
Status = 1; //Going 'Full'.
//'Full_out' logic for the writing port:
assign PresetFull = Status & EqualAddresses; //'Full' Fifo.
always @ (posedge WClk, posedge PresetFull) //D Flip-Flop w/ Asynchronous Preset.
if (PresetFull)
Full_out <= 1;
else
Full_out <= 0;
//'Empty_out' logic for the reading port:
assign PresetEmpty = ~Status & EqualAddresses; //'Empty' Fifo.
always @ (posedge RClk, posedge PresetEmpty) //D Flip-Flop w/ Asynchronous Preset.
if (PresetEmpty)
Empty_out <= 1;
else
Empty_out <= 0;
endmodule
module GrayCounter
#(parameter COUNTER_WIDTH = 4)
(output reg [COUNTER_WIDTH-1:0] GrayCount_out, //'Gray' code count output.
input wire Enable_in, //Count enable.
input wire Clear_in, //Count reset.
input wire Clk);
/////////Internal connections & variables///////
reg [COUNTER_WIDTH-1:0] BinaryCount;
/////////Code///////////////////////
always @ (posedge Clk)
if (Clear_in) begin
BinaryCount <= {COUNTER_WIDTH{1'b 0}} + 1; //Gray count begins @ '1' with
GrayCount_out <= {COUNTER_WIDTH{1'b 0}}; // first 'Enable_in'.
end
else if (Enable_in) begin
BinaryCount <= BinaryCount + 1;
GrayCount_out <= {BinaryCount[COUNTER_WIDTH-1],
BinaryCount[COUNTER_WIDTH-2:0] ^ BinaryCount[COUNTER_WIDTH-1:1]};
end
endmodule
设计理念二:
来自paper“Simulation and Synthesis Techniques for Asynchronous FIFO Design” ---Clifford E. Cummings, Sunburst Design, Inc
用“MSBs”来区分满和空:当写指针超过最后的FIFO地址的值时,写指针将使未使用的MSB递增,同时将其余的位设为0。读指针也是如此。 如果两个指针的MSB不同,则代表写指针已经游历了整个FIFO,多跑了一圈且又赶上了读指针,此时FIFO为满,否则为空。如果fifo地址位宽为n-1,那么指针的位宽为n。
这篇paper介绍了一种改进的“格雷码”,不同于传统意义的格雷码,它把第一位当作msb位来区分空和满,后三位做地址位。这种做法会导致传统格雷码的“7”和“8”出现问题,因为他两的后三位是一样的,而第一位msb却不同,算法将会认为这是“满”,而其实fifo并未满。所以为了解决这个问题,作者把“8”往后的三位从“镜面”反射“0”到“7”的后三位,改为了复制从“0”到“7”的后三位。改后的新格雷码如图:
这个格雷码(假设为rgraynext)可以通过二进制数(rbinnext)右移一位与自身异或实现:
assign rgraynext=(rbinnext>>1)^rbinnext;
由于这个伪格雷码没有严格意义上的每次只变化一位,所以整个fifo设计用了二进制和伪格雷码两种译码方式。用伪格雷码来比较读指针和写指针,用二进制码来查找dual ram中的地址。保证了fifo运行过程中不会出错。
记住,由于首位用于“msb”比较,所以读写指针的位数比寻址数多一位。
整个FIFO设计如下:
代码可以在“http://www.sunburst-design.com/papers/CummingsSNUG2002SJ_FIFO1.pdf” 里找到。