FPGA基础知识极简教程(3)从FIFO设计讲起之同步FIFO篇

2020-06-29 14:37:12 浏览数 (1)

  • 写在前面
  • 正文
    • FPGA/ASIC中的FIFO
    • 同步FIFO的设计
  • 参考资料
  • 交个朋友

写在前面

  • 个人博客首页[1]
  • 注:学习交流使用!

正文


FPGA/ASIC中的FIFO

FIFO缓冲区如何用于传输数据和跨时钟域

缩写FIFO代表 First In First Out。FIFO在FPGA和ASIC设计中无处不在,它们是基本的构建模块之一。而且它们非常方便!FIFO可用于以下任何目的:

  • 跨时钟域
  • 在将数据发送到芯片外之前将其缓冲(例如,发送到DRAM或SRAM)
  • 缓冲数据以供软件在以后查看
  • 存储数据以备后用

FIFO可以认为是汽车可以驶过的单向隧道。隧道的尽头是一个带门的收费站。门一旦打开,汽车便可以离开隧道。如果那扇门从未打开,而更多的汽车继续进入隧道,那么最终隧道将充满汽车。这称为FIFO溢出,通常这不是一件好事。FIFO的深度可以认为是隧道的长度。FIFO越深,在溢出之前可以容纳更多的数据。FIFO也具有宽度,该宽度表示进入FIFO的数据的宽度(以位数为单位)。下面是任何FIFO基本接口的图像。当您查看任何FIFO时,总是会找到这些信号。通常,会有更多的信号添加其他功能,例如FIFO中的字数计数。参见下图:

基础FIFO

FIFO可以分为写一侧和读一侧。写入一侧具有信号“写入使能wr_en”,“写入数据wr_data”和“ FIFO已满fifo_full”。设计人员切勿写入已满的FIFO!始终检查FIFO已满标志,以确保有空间可以写入另一条数据,否则您将丢失该数据。

读取的一侧具有信号“读取使能rd_en”,“读取数据rd_data”和“ FIFO空fifo_empty”。设计人员切勿读取空的FIFO!只要您遵循这两个基本规则,您和FIFO就会相处融洽。我再说一遍,因为它们是如此重要。

FIFO的两个规则:

  • 永远不要写入完整的FIFO(溢出)
  • 永远不要从空的FIFO中读取(下溢)

FIFO本身可以由FPGA或ASIC内的专用逻辑组成,也可以由触发器(分布式寄存器)创建。综合工具将使用这两种工具中的哪一种完全取决于您使用的FPGA供应商以及代码的结构。只需知道,当您使用专用逻辑块时,与使用基于寄存器的FIFO相比,它们具有更好的性能。FIFO是FPGA设计人员的基本构建模块之一,对于正确理解和正确使用至关重要!


同步FIFO的设计

为了简单起见,本文先设计一个同步FIFO,仅带有空满标志。在给出同步FIFO设计之前,有必要说说同步FIFO的原理,同步FIFO的设计很有必要,它是通往异步FIFO的基础,同步FIFO中的所有原理都理解了,异步FIFO中和同步FIFO相同的东西就不必再费心思思考了,而是直接进入重点,如何控制空满!

FIFO是先进先出的首字母缩写,它描述了如何相对于时间或优先级管理数据。在这种情况下,到达的第一个数据也将是从一组数据中离开的第一个数据。FIFO缓冲区是一种读/写存储阵列,可自动跟踪数据进入模块的顺序并以相同顺序读出数据。在硬件中,FIFO缓冲区用于同步目的。 它通常实现为循环队列,并具有两个指针:

  • 读指针/读地址寄存器
  • 写指针/写地址寄存器

读写地址最初都位于第一个存储器位置,并且FIFO队列为空。当FIFO缓冲区的读地址和写地址之间的差等于内存阵列的大小时,则FIFO队列为Full(对于异步FIFO而言,可以设计多一位地址表示读指针以及写指针)。

FIFO可以分为同步时钟还是异步时钟,具体取决于是相同时钟(同步)还是不同时钟(异步)控制读写操作。

同步FIFO是指FIFO设计,其中使用时钟信号将数据值顺序写入存储阵列,并使用相同的时钟信号从存储阵列顺序读出数据值。图1显示了典型FIFO的操作流程。

再看一幅图:

从这幅图中我们可以得到如下信息:

  • 写指针WP总是指向下一个时钟要写的地址;
  • 读指针RP总是指向下一个时钟要读的地址;
  • 读指针等于写指针的时候有可能为空,有可能为满。

这几点都很重要,到后面我们慢慢体会。

代码设计

代码语言:javascript复制
// Reborn Lee 
// blog address: https://blog.csdn.net/Reborn_Lee
module syn_fifo#(
    parameter DATA_WIDTH = 8,
    parameter DATA_DEPTH = 8
    )(
    input i_clk,
    input i_rst,
    
    //write port
    input wr_en,
    input [DATA_WIDTH - 1 : 0] wr_data,
    output wr_full,
    //read port
    input rd_en,
    output [DATA_WIDTH - 1 : 0] rd_data,
    output rd_empty
    
);

    //define ram
    
    reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
    reg [$clog2(DATA_DEPTH) : 0] fifo_cnt = 0;
    
    reg [$clog2(DATA_DEPTH) - 1 : 0] wr_pointer = 0;
    reg [$clog2(DATA_DEPTH) - 1 : 0] rd_pointer = 0;
    
    // keep track of the  fifo counter
    always@(posedge i_clk) begin
        if(i_rst) begin
            fifo_cnt <= 0;
        end
        else begin
            if(wr_en && !rd_en) begin //wr_en is asserted and fifo is not full
                fifo_cnt <= fifo_cnt   1;
            end
            else if(rd_en && !wr_en) begin // rd_en is asserted and fifo is not empty
                fifo_cnt <= fifo_cnt - 1;
            end
        end
        
    
    end
    
    //keep track of the write  pointer
    always@(posedge i_clk) begin
        if(wr_en && !wr_full) begin
            if(wr_pointer == DATA_DEPTH - 1) begin
                wr_pointer <= 0; 
            end
            else begin
                wr_pointer <= wr_pointer   1;
            end
                  
        end
    
    end
    
    
    //keep track of the read pointer 
    always@(posedge i_clk) begin
        if(rd_en && !rd_empty) begin
            if(rd_pointer == DATA_DEPTH - 1) begin
                rd_pointer <= 0;
            end
            else begin
                rd_pointer <= rd_pointer   1;
            end
        end
    
    end
    
    //write data into fifo when wr_en is asserted
    always@(posedge i_clk) begin
        if(wr_en) begin
            fifo_buffer[wr_pointer] <= wr_data;
        end
    end
    
    //read data from fifo when rd_en is asserted
    //assign rd_data = (rd_en)?fifo_buffer[rd_pointer]: 'bz;
    assign rd_data = fifo_buffer[rd_pointer];
    
    assign wr_full = (fifo_cnt == DATA_DEPTH)? 1 : 0;
    assign rd_empty = (fifo_cnt == 0) ? 1 : 0;
    
endmodule

测试平台:

代码语言:javascript复制
`timescale 1ns / 1ps
////////////////////////////////////////////////////////////
// Engineer: Reborn Lee
// Module Name: syn_fifo_tb
//https://blog.csdn.net/Reborn_Lee
//////////////////////////////////////////////////////////


module syn_fifo_tb(

    );

	parameter DATA_WIDTH = 8;
    parameter DATA_DEPTH = 8;
  
    reg i_clk;
    reg i_rst;
    
    //write port
    reg wr_en;
    reg [DATA_WIDTH - 1 : 0] wr_data;
    wire wr_full;
    //read port
    reg rd_en;
    wire [DATA_WIDTH - 1 : 0] rd_data;
    wire rd_empty;


    initial begin
    	i_clk = 0;
    	forever begin
    		#5 i_clk = ~i_clk;
    	end
    end

    initial begin
    	
    	i_rst = 1;
    	wr_en = 0;
    	rd_en = 0;

    	@(negedge i_clk) i_rst = 0;
    	@(negedge i_clk) wr_en = 1;
    	wr_data = $random;

    	repeat(3) begin
    		@(negedge i_clk)
    		wr_data = $random;	
    	end
    	
    	

    	@(negedge i_clk)
    	wr_en = 0;
    	rd_en = 1;

    	repeat(3) begin
    		@(negedge i_clk);	
    	end

    	@(negedge i_clk)
    	rd_en = 0;
    	wr_en = 1;
    	wr_data = $random;

    	repeat(7) begin   		
    		@(negedge i_clk)
    		wr_data = $random;
    	end

    	#20 $finish;


    end


	syn_fifo #(.DATA_WIDTH(DATA_WIDTH),.DATA_DEPTH(DATA_DEPTH))
	inst_syn_fifo
		(
			.i_clk    (i_clk),
			.i_rst    (i_rst),
			.wr_en    (wr_en),
			.wr_data  (wr_data),
			.wr_full  (wr_full),
			.rd_en    (rd_en),
			.rd_data  (rd_data),
			.rd_empty (rd_empty)
		);

endmodule

仿真波形

先看最直观的信息:

写入FIFO的数据依次是24,81, 09, 63,读出的数据(从读使能有效开始读)24,81,09,63,读完之后的一个时钟,不在读了,空信号拉高,表示读空了。如下图用箭头以及数字示意:

我们再看看是否写入FIFO的数据依次是24,81,09,63:

确实如此!

再看看读数据的情况:

也确实是从0指针开始读的。

至于,这个FIFO的某些地方值为什么是红色的,是因为没有给FIFO的存储空间赋初值,在仿真时候显示红色,未知而已,在实际的FPGA或者ASIC中,实际是随机值。

我们再来看看设计代码中的写指针,初值为0,在写使能有效时,当时钟上升沿到达时,写指针加1:

代码语言:javascript复制
//keep track of the write  pointer
    always@(posedge i_clk) begin
        if(wr_en && !wr_full) begin
            if(wr_pointer == DATA_DEPTH - 1) begin
                wr_pointer <= 0; 
            end
            else begin
                wr_pointer <= wr_pointer   1;
            end
                  
        end
    
    end

而此时,也就是写使能有效,且时钟上升沿到来时,又对FIFO进行写操作:

代码语言:javascript复制
//write data into fifo when wr_en is asserted
    always@(posedge i_clk) begin
        if(wr_en) begin
            fifo_buffer[wr_pointer] <= wr_data;
        end
    end

我想提醒的是,此时写入的FIFO空间地址,应该是指针加1之前的地址值(指针值),这是因为使用了非阻塞赋值,指针即使加1了,在此刻时钟上升沿写FIFO时,加1的指针还未生效,这就是非阻塞赋值的作用了。你不信吗?按照上面说的,在仿真中,指针的值应该比写入FIFO中的地址值大1. 看看仿真图:

这本不是问题,可还是要提出来,就怕有的同学会迷!

刚才的Verilog设计对于写采用的是同步写,但是对于读却采用的是异步读,如果我们采用同步读呢?就和FIFO写数据达成统一,我们可以猜测(其实内心很确信),读指针值超越读数据地址1,也就是说,如果读地址在时钟上升沿为2的话,其实当前读的值为1地址的值。那我们测试一下吧,先令读改为同步读:

代码语言:javascript复制
// assign rd_data = fifo_buffer[rd_pointer];

    always@(posedge i_clk) begin
    	if(rd_en) begin
    		rd_data <= fifo_buffer[rd_pointer];
    	end
    end

然后观测仿真结果:

数据的存取倒是没有问题,先进先出。但可以看到的另一个情况是,1地址时,存的数据和取得数据其实都是0地址的数据。继续看仿真图:

可见,此时的地址虽然变成了1,但对于FIFO来说,并未生效,存以及取仍然按照前一个地址来存或取,这是非阻塞赋值的原因。但这些细节问题,并不会影响我们使用FIFO,我们使用FIFO的时候不必关注这些,我们只需要只要我们存取都是先进先出即可。封装成FIFO模块,用就是了!不过对于数字设计师来说,这种细节你还是要知道的,要不然用FIFO是没有灵魂的,还有就是如果面试或者笔试让你写一个FIFO你该怎么办呢?既然是设计,你肯定要知道细节了,因为是你设计的细节。

VHDL版设计

代码语言:javascript复制
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
 
entity module_fifo_regs_no_flags is
  generic (
    g_WIDTH : natural := 8;
    g_DEPTH : integer := 32
    );
  port (
    i_rst_sync : in std_logic;
    i_clk      : in std_logic;
 
    -- FIFO Write Interface
    i_wr_en   : in  std_logic;
    i_wr_data : in  std_logic_vector(g_WIDTH-1 downto 0);
    o_full    : out std_logic;
 
    -- FIFO Read Interface
    i_rd_en   : in  std_logic;
    o_rd_data : out std_logic_vector(g_WIDTH-1 downto 0);
    o_empty   : out std_logic
    );
end module_fifo_regs_no_flags;
 
architecture rtl of module_fifo_regs_no_flags is
 
  type t_FIFO_DATA is array (0 to g_DEPTH-1) of std_logic_vector(g_WIDTH-1 downto 0);
  signal r_FIFO_DATA : t_FIFO_DATA := (others => (others => '0'));
 
  signal r_WR_INDEX   : integer range 0 to g_DEPTH-1 := 0;
  signal r_RD_INDEX   : integer range 0 to g_DEPTH-1 := 0;
 
  -- # Words in FIFO, has extra range to allow for assert conditions
  signal r_FIFO_COUNT : integer range -1 to g_DEPTH 1 := 0;
 
  signal w_FULL  : std_logic;
  signal w_EMPTY : std_logic;
   
begin
 
  p_CONTROL : process (i_clk) is
  begin
    if rising_edge(i_clk) then
      if i_rst_sync = '1' then
        r_FIFO_COUNT <= 0;
        r_WR_INDEX   <= 0;
        r_RD_INDEX   <= 0;
      else
 
        -- Keeps track of the total number of words in the FIFO
        if (i_wr_en = '1' and i_rd_en = '0') then
          r_FIFO_COUNT <= r_FIFO_COUNT   1;
        elsif (i_wr_en = '0' and i_rd_en = '1') then
          r_FIFO_COUNT <= r_FIFO_COUNT - 1;
        end if;
 
        -- Keeps track of the write index (and controls roll-over)
        if (i_wr_en = '1' and w_FULL = '0') then
          if r_WR_INDEX = g_DEPTH-1 then
            r_WR_INDEX <= 0;
          else
            r_WR_INDEX <= r_WR_INDEX   1;
          end if;
        end if;
 
        -- Keeps track of the read index (and controls roll-over)        
        if (i_rd_en = '1' and w_EMPTY = '0') then
          if r_RD_INDEX = g_DEPTH-1 then
            r_RD_INDEX <= 0;
          else
            r_RD_INDEX <= r_RD_INDEX   1;
          end if;
        end if;
 
        -- Registers the input data when there is a write
        if i_wr_en = '1' then
          r_FIFO_DATA(r_WR_INDEX) <= i_wr_data;
        end if;
         
      end if;                           -- sync reset
    end if;                             -- rising_edge(i_clk)
  end process p_CONTROL;
   
  o_rd_data <= r_FIFO_DATA(r_RD_INDEX);
 
  w_FULL  <= '1' when r_FIFO_COUNT = g_DEPTH else '0';
  w_EMPTY <= '1' when r_FIFO_COUNT = 0       else '0';
 
  o_full  <= w_FULL;
  o_empty <= w_EMPTY;
   
  -- ASSERTION LOGIC - Not synthesized
  -- synthesis translate_off
 
  p_ASSERT : process (i_clk) is
  begin
    if rising_edge(i_clk) then
      if i_wr_en = '1' and w_FULL = '1' then
        report "ASSERT FAILURE - MODULE_REGISTER_FIFO: FIFO IS FULL AND BEING WRITTEN " severity failure;
      end if;
 
      if i_rd_en = '1' and w_EMPTY = '1' then
        report "ASSERT FAILURE - MODULE_REGISTER_FIFO: FIFO IS EMPTY AND BEING READ " severity failure;
      end if;
    end if;
  end process p_ASSERT;
 
  -- synthesis translate_on
end rtl;

仿真就算了,和Verilog版一致也可。

带有几乎空almost empty 以及几乎满 almost full的同步FIFO带有几乎空以及几乎满的同步FIFO设计也不是什么难事,我们只需要设置两个参数,几乎空以及几乎满的阈值,最后再将读写计数器和阈值对比,如果小于几乎空阈值,则几乎空标志有效;如果大于几乎满阈值,则几乎满标志有效。设计十分简单,就在上述代码基础上添加几条,这里不再赘余。


参考资料

  • 参考资料1[2]
  • 参考资料2[3]

参考资料

[1]

个人博客首页: https://blog.csdn.net/Reborn_Lee

[2]

参考资料1: https://esrd2014.blogspot.com/p/first-in-first-out-buffer.html

[3]

参考资料2: https://www.nandland.com/vhdl/modules/module-fifo-regs-with-flags.html

[4]

FPGA/IC技术交流2020: https://blog.csdn.net/Reborn_Lee/article/details/105844330

0 人点赞