> 技术文档 > Verilog:同步FIFO、异步FIFO

Verilog:同步FIFO、异步FIFO

目录

一、FIFO原理

1. 什么是FIFO

2. FIFO设计关键

二、同步FIFO

1. 读空、写满判断

2. verilog实现

3.tb仿真文件

4.仿真波形

三、异步FIFO

1. 跨时钟域同步

2. 读空、写满判断

3. verilog实现

4.仿真文件

5.仿真波形

一、FIFO原理

1. 什么是FIFO

        FIFO:即First In First Out,是一种先进先出的数据存储、缓冲器。

        存储方式:FIFO的数据按照写入的先后被顺序性存储,并在需要时被顺序地读出。因此FIFO只能顺序写入和读取数据,而不能像普通存储器(RAM)那样随机读写任意存储单元。

        地址生成:FIFO无外部读写地址线,通过内部的计数器实现地址的自增来控制读写。每当有一个数据被写入或读出时,计数器就会加一,从而生成下一个要写入或读出的数据的地址。

        同步FIFO:写/读时钟为同一时钟,内部所有逻辑都是同步逻辑。常用于交互数据的缓冲。(如数据采集端速度飞快,但数据处理端较慢,故可用FIFO做数据缓冲。反之亦然)

        异步FIFO:写/读时钟为异步时钟,内部的写逻辑和读逻辑的交互需要异步处理。常用于跨时钟域的数据交互,实现不同时钟域之间的数据同步。

        参数与端口:

信号 端口 说明 w_clk input 写时钟(同步共用一个clk) r_clk input 读时钟(同步共用一个clk) rst_n input 复位信号 wr_en input 写使能 rd_en input 读使能 [7:0]wdata input 写入数据输入(8bit) [7:0]rdata output 读取数据输出(8bit) full output 写满信号 empty output 读空信号

2. FIFO设计关键

        不难想到:当FIFO内填满了写入的数据但都没有被读取时,再写入数据会覆盖需要读取的数据。反之当FIFO内写入的数据被读空时,再读数据会读过头,读取到错误的数据。因此FIFO的设计关键在于 “读空” “写满” 的判断:

二、同步FIFO

1. 读空、写满判断

        法1:拓展高位法(深度为2^n)

        在读写地址wr_addr、rd_addr 基础上拓展一个高位MSB作为读写指针w_ptr、r_ptr。当指针越过最后一个FIFO地址时,就将MSB加1,其它位回零。

以深度为8的FIFO为例:wr_addr、rd_addr 地址位只需要3位,拓展一位MSB作为折回标志位,此时读写指针w_ptr、r_ptr 为4位,其中低3位作为地址。

写满时,两指针MSB不同,地址位相同:即指针仅高位不同,写比读指针多折回一次,如rd_ptr=0000,而wr_ptr = 1000 此时写指针比读快一轮,为写满状态

读空时,两指针所有位相同:两指针的折回次数相同,如rd_ptr=0110,而wr_ptr = 0110 此时读指针追上了写,为写满状态

        法2:数据计数法

        定义data_counter 用于计数,每当写入一个数据计数器加1,每读出一个数据减1。此时当data_counter = 0时FIFO读空,data_counter = FIFO深度时则写满。缺点:计数器会占用额外资源,当FIFO较大时,可能会降低FIFO的读写速度。

        

2. verilog实现

同步FIFO,宽度8,深度8,采用拓展高位法设计***************************************************************`timescale 1ns / 1psmodule fifo_sync ( //data_width = 8 data depth =8 input wire clk, input wire rst_n, input wire wr_en, //写使能 input wire rd_en, //读使能 input wire [7:0]wdata, //写入数据输入 output reg [7:0]rdata, //读取数据输出 output wire empty, //读空标志信号 output wire full  //写满标志信号 ); reg [7:0] data [7:0]; //数据存储单元(8bit数据8个) reg [3:0] wr_ptr = 4\'d0; //写指针 reg [3:0] rd_ptr = 4\'d0; //读指针 wire [2:0] wr_addr; //写地址(写指针的低3位) wire [2:0] rd_addr; //读地址(读指针的低3位)assign wr_addr = wr_ptr[2:0]; assign rd_addr = rd_ptr[2:0];always@(posedge clk or negedge rst_n)begin //写数据 if(!rst_n) wr_ptr <= 4\'d0; else if(wr_en && !full)begin data[wr_addr] <= wdata; wr_ptr <= wr_ptr + 4\'d1; endendalways@(posedge clk or negedge rst_n)begin //读数据 if(!rst_n) rd_ptr <= \'d0; else if(rd_en && !empty)begin rdata <= data[rd_addr]; rd_ptr <= rd_ptr + 4\'d1; endendassign empty = (wr_ptr == rd_ptr); //读空assign full = (wr_ptr == {~rd_ptr[3],rd_ptr[2:0]}); //写满endmodule

3.tb仿真文件

同步FIFO仿真:读写使能同时有效,边读边写,每个时钟写入的数据加1*****************************************************************`timescale 1ns / 1psmodule fifo_sync_tb (); reg clk_tb; reg rst_n_tb; reg wr_en_tb; reg rd_en_tb; reg [7:0]wdata_tb; wire [7:0]rdata_tb; wire empty_tb; wire full_tb;always #10 clk_tb = ~clk_tb;initial begin clk_tb = 1\'b0; wdata_tb = 8\'b0000_0000; wr_en_tb = 1\'b0; rd_en_tb = 1\'b0; rst_n_tb = 1\'b0; #15; rst_n_tb = 1\'b1; #75; wr_en_tb = 1\'b1; rd_en_tb = 1\'b1; generate_wdata; //任务:每隔20ns生成一次写数据 #100; $finish;end task generate_wdata; //任务:每隔20ns生成一次写数据 integer i; begin for(i=0;i<15;i=i+1)begin wdata_tb = wdata_tb + 8\'b0000_0001; #20; end endendtaskfifo_sync fifo_sync( .clk (clk_tb), .rst_n (rst_n_tb), .wr_en (wr_en_tb), .rd_en (rd_en_tb), .wdata (wdata_tb), .rdata (rdata_tb), .empty (empty_tb), .full (full_tb));endmodule

4.仿真波形

1.复位时一直为读空状态(没有数据读)

2.同时使能读写时,每写入一个数据,就读出一个数据

3.写入的数据依次存进FIFO存储单元(data)

三、异步FIFO

1. 跨时钟域同步

        为什么同步:由于读写操作分别由不同时钟控制,直接交互可能会导致数据冲突、时序违规以及亚稳态问题,从而影响FIFO的正确性和稳定性。这里要同步读写两部分,不仅需要考虑如何同步,还需考虑信号同步到哪个时钟域的问题,

         亚稳态问题:由于两个指针的变化不同步,当它们被直接比较时,可能会因为时钟边沿的不同步导致采样到的信号值不稳定。这种不稳定状态被称为亚稳态,它可能导致比较结果错误。

        如何同步:首先同步还需要避免亚稳态,一般采取打拍同步的方式解决,打两拍就可以很好的降低亚稳态发生概率。同时指针转换为格雷码再打拍同步,格雷码每次只变一位,有效的避免跨时钟域情况下亚稳态问题发生概率。例如二进制的 0111→1000,4位都会变化,而格雷码 0100→1100 仅第四位变化 ,进一步减小亚稳态发生概率。

        同步到哪个时钟域:举个例子,判断写满时,写指针每变化一次就需要和读指针比较,判断有没写满以决定能不能继续写数据,所以需要将读指针在写指针变化时拿来比较,即读指针同步到写时钟域下比较。

        综上可以得出结论:

如何同步

将二进制指针转化成格雷码后,再打两拍同步

同步到哪个时钟域

 判断读空:将写指针同步到读时钟域

 判断写满:将读指针同步到写时钟域

(提一句:打两拍会有两个周期的延迟,比如判断读空时,同步到读时钟域的写指针实际上是前面隔两个读时钟周期的同步进来的写指针,所以判断出读空时可能是”假读空”,即没读空就提前判断为读空。但是这种情况不影响FIFO的功能,无非是降低了点性能,属于保守设计,保证运行稳定)

2. 读空、写满判断

        不同于同步FIFO,异步FIFO读写指针在转化为格雷码同步后,再作比较,因此关键是如何通过格雷码判断读空写满。这种情况适用拓展高位法:

        与同步FIFO同理,先将二进制的读写地址拓展一位MSB,接着将拓展后的二进制指针转化为雷格码。深度为8的FIFO为例:wr_addr、rd_addr 地址位只需3位,拓展一位作为折回标志位,此时读写指针w_ptr、r_ptr 为4位。然后拓展后的二进制指针转化为雷格码指针wr_ptr_gray、rd_ptr_gray。指针转化结果见下图(8-15表示已折回一次)。

写满时,两指针高两位不同,其余位相同:如rd_ptr_gray = 0100,wr_ptr_gray = 1000 此时写指针比读快一轮,写满。

读空时,两指针所有位相同:如rd_ptr_gray = 0111,wr_ptr_gray = 0111 此时读指针追上写,读空

3. verilog实现

异步FIFO,宽度8,深度8,采用拓展高位法设计***************************************************************`timescale 1ns / 1psmodule fifo_async ( input wire wclk,  //写时钟 input wire rclk,  //读时钟 input wire rst_n, input wire wr_en, //写使能 input wire rd_en, //读使能 input wire [7:0]wdata, //写数据输入端 output reg [7:0]rdata, //读数据输出端 output wire empty, //读空信号 output wire full  //写满信号 ); reg [7:0] data [7:0]; //数据存储单元(8bit数据8个) reg [3:0] wr_ptr = 4\'d0; //写指针 reg [3:0] rd_ptr = 4\'d0; //读指针 wire [2:0] wr_addr; //写地址(写指针的低3位) wire [2:0] rd_addr; //读地址(读指针的低3位) wire [3:0] wr_ptr_gray; //写指针 格雷码 wire [3:0] rd_ptr_gray; //读指针 格雷码 reg [3:0] wr_ptr_gray_d1 = 4\'d0; //寄存一拍 reg [3:0] wr_ptr_gray_d2 = 4\'d0; //寄存两拍 reg [3:0] rd_ptr_gray_d1 = 4\'d0; reg [3:0] rd_ptr_gray_d2 = 4\'d0;assign wr_addr = wr_ptr[2:0]; assign rd_addr = rd_ptr[2:0];assign wr_ptr_gray = ((wr_ptr>>1) ^ wr_ptr); //指针转格雷码assign rd_ptr_gray = ((rd_ptr>>1) ^ rd_ptr);always@(posedge wclk or negedge rst_n)begin //写数据 if(!rst_n) wr_ptr <= 4\'d0; else if(wr_en && !full)begin data[wr_addr] <= wdata; wr_ptr <= wr_ptr + 4\'d1; endendalways@(posedge rclk or negedge rst_n)begin //读数据 if(!rst_n) rd_ptr <= 4\'d0; else if(rd_en && !empty)begin rdata <= data[rd_addr]; rd_ptr <= rd_ptr + 4\'d1; endendalways@(posedge wclk or negedge rst_n)begin //读指针格雷码打两拍同步到写时钟域 if(!rst_n)begin rd_ptr_gray_d1 <= 4\'d0; rd_ptr_gray_d2 <= 4\'d0; end else begin rd_ptr_gray_d1 <= rd_ptr_gray; rd_ptr_gray_d2 <= rd_ptr_gray_d1; endendalways@(posedge rclk or negedge rst_n)begin //写指针格雷码打两拍同步到读时钟域 if(!rst_n)begin wr_ptr_gray_d1 <= 4\'d0; wr_ptr_gray_d2 <= 4\'d0; end else begin wr_ptr_gray_d1 <= wr_ptr_gray; wr_ptr_gray_d2 <= wr_ptr_gray_d1; endendassign empty = (rd_ptr_gray == wr_ptr_gray_d2); //读空(要比较同步后的写指针的格雷码)assign full = (wr_ptr_gray == {~rd_ptr_gray_d2[3:2], rd_ptr_gray_d2[1:0]} ); //写满(要比较同步后的读指针的格雷码)endmodule

4.仿真文件

异步FIFO仿真:读比写时钟快,先使能写,中途再使能读,每个写时钟写入的数据加1***************************************************************************`timescale 1ns / 1psmodule fifo_async_tb (); reg wclk_tb; reg rclk_tb; reg rst_n_tb; reg wr_en_tb; reg rd_en_tb; reg [7:0]wdata_tb; wire [7:0]rdata_tb; wire empty_tb; wire full_tb;always #10 wclk_tb = ~wclk_tb;//读时钟比写快always #5 rclk_tb = ~rclk_tb;initial begin wclk_tb = 0; rclk_tb = 0; wdata_tb = 8\'b0; wr_en_tb = 0; rd_en_tb = 0; rst_n_tb = 0; #15; rst_n_tb = 1; #75; wr_en_tb = 1; generate_wdata; //任务:每隔20ns生成一次写数据 #100; $finish;end task generate_wdata; //任务:每隔20ns生成一次写数据 integer i; begin for(i=0;i=10)? 1:0; //写数据变化第10次后开始读数据 #20; end endendtaskfifo_async fifo_async( .wclk (wclk_tb), .rclk (rclk_tb), .rst_n (rst_n_tb), .wr_en (wr_en_tb), .rd_en (rd_en_tb), .wdata (wdata_tb), .rdata (rdata_tb), .empty (empty_tb), .full (full_tb));endmodule

5.仿真波形

1.复位时一直为读空状态(没有数据读)

2.先使能写,写完第8个数据后为写满状态,后续数据无法写入

3.后使能读,发现读几个数据后,不再为写满状态,数据又可以继续写入(提一句:为什么不是读一个数据就结束写满状态,是因为判断写满时,同步过来的的读指针格雷码有两拍延迟,这个时候实际上是“假写满”属于保守设计,图中最后一个信号就是两拍同步后的读指针格雷码,可以看到延迟了两个写时钟才变化)

4.因为读时钟快,波形后半段读追上写,读数据需要等待数据写入才能读,读空信号此时反复变化