FIFO实战学习-同步FIFO/异步FIFO-格雷码

FIFO

  这篇更多的是记录FIFO学习,参考了众多优秀的文章,我都会标记出来,首先是关于同步FIFO的参考搞懂FIFO书写的关键是什么,异步FIFO参加这篇异步FIFO讲解

  FIFO 的英文全称是 First In First Out,即先进先出。 FPGA 使用的 FIFO 一般指的是对数据的存储具有先进先出特性的一个缓存器,常被用于数据的缓存,或者高速异步数据的交互也即所谓的跨时钟域信号传递。它与 FPGA 内部的 RAM 和 ROM 的区别是没有外部读写地址线,采取顺序写入数据,顺序读出数据的方式,使用起来简单方便,由此带来的缺点就是不能像 RAM 和 ROM 那样可以由地址线决定读取或写入某个指定的地址。

一. 自定义同步FIFO

  为了增强对FIFO的认识,用Verilog描述一个FIFO。

  FIFO是基于双口RAM衍生出来的,同步FIFO和异步FIFO的区别是主要是双口RAM的两套独立端口是否使用同一时钟,同步FIFO读写受同一时钟控制,其中设计的同步FIFO的信号如下所示。

  FIFO大致分为写端和读端,两者都拥有独自的使能信号,以及写数据,这里数据宽度是8Bit,输出端对于FIFO的空满有相应的标志位,以及计数器和读出的数据。
在这里插入图片描述

  FIFO读写的规则:

  • 永远不要写入满FIFO
  • 永远不要读空FIFO

  FIFO在外部端口上表现没有地址线,因为是第一个到达的数据也将是第一个输出的数据,所以 FIFO缓冲区是一种读/写存储阵列,可自动跟踪数据进入模块的顺序并以相同顺序读出数据。而内部的实现需要两个指针来分别表示读和写的地址,读指针和写指针。初始时读写指针指向第一个存储单元,此时FIFO队列为空,fifo_empty有效,当写入256个数据,也就是写指针重新指向第一个FIFO的存储单元,而计数data_count等于FIFO的深度(0~255,但是写指针总是指向在下一个写的存储单元,所以当写完地址为255,8’b1111 1111后,变为9位的256 = 9‘b1 0000 0000)时,fifo_full有效。由此可知读写指针相同,此时有可能是满状态(写一直有效,写完最后一个255地址后,取低8位= 8’b0000 0000,此时读写指针的地址相同),所以用地址差不能用于判断空满,而是使用计数器来判断,但是计数器要设置比深度的位数多一位 ,这里也就是9位。

  上面提到的博文中的这幅图,结合上一段的解释可以帮助理解地址的变化:

在这里插入图片描述

1.1 代码设计

  其实更加规范的模式是想其中的位宽设置为参数,接下来的代码书写使用参数声明宽度。

//FIFO两条铁律就是不能满写  不能空读

module custom_FIFO(
    input wire           clk,
    input wire           rst_n,
    input wire           wr_en,
    input wire [7:0]     wr_data,
    input wire           rd_en,

    output wire           fifo_full,    //空和满都是1有效
    output reg [7:0]     rd_data,
    output wire           fifo_empty,
    output reg [8:0]     data_count
    );

    reg [7:0] mem [255:0];
    reg [7:0] wr_add;
    reg [7:0] rd_add;

    //FIFO的书写方式应该是分信号进行书写
    //wr_add总是指向下一个写的存储单元,在上升沿监测到写地址为255时,实际上的地址值重新因为溢出指向了地址0,此时溢出信号为真,而计数值达到256
    //这里的逻辑是读写地址会自动从255变为0
    always@(posedge clk)begin
        if(rst_n == 1'b0)begin
            wr_add <= 8'b0;
        end
        else if(wr_en == 1'b1 && fifo_full== 1'b0) begin
            wr_add <= wr_add + 1'b1;
        end
    end

    // rd_add
    always@(posedge clk)begin
        if(rst_n == 1'b0)begin
            rd_add <= 8'b0;
        end
        else if(rd_en == 1'b1 && fifo_empty == 1'b0) begin
            rd_add <= rd_add + 1'b1;
        end
    end


    //rd_data
    always@(posedge clk)begin
        if(rst_n == 1'b0)begin
            rd_data <= 8'h00;
        end
        else if(rd_en == 1'b1 && fifo_empty == 1'b0)begin
            rd_data <= mem[rd_add];
        end
    end

    //wr_data
    always@(posedge clk)begin
        if(wr_en == 1'b1 && fifo_full== 1'b0)begin
            mem[wr_add] <= wr_data;
        end
    end

    always@(posedge clk)begin
        if(rst_n == 1'b0)begin
            data_count <= 9'h0;
        end
        else   begin
            if(wr_en == 1'b1 && !rd_en && fifo_full== 1'b0)begin  //当读写都有效,则不变
                data_count <= data_count + 1'b1;
            end
            else if(rd_en == 1'b1 && wr_en && fifo_empty == 1'b0)begin
                data_count <= data_count - 1'b1;
            end
        end
    end

    assign fifo_full =  (data_count == 9'h100);  //数据计数的范围是1-256,当为256时数据满
    assign fifo_empty = (data_count  == 9'h00);


endmodule

1.2 Testbech

  读取的TXT中有259个数据,可以看到第256个数据为8‘h25,仿真文件中将所有的259个数据全部读取到mem中,然后将读到的数据写256到FIFO中。
在这里插入图片描述

module tb_custom_FIFO();
    reg        clk;
    reg        rst_n;
    reg        wr_en;
    reg [7:0]  wr_data;
    reg        rd_en;

    wire        fifo_full;    //空和满都是1有效
    wire [7:0]  rd_data;
    wire        fifo_empty;
    wire [8:0]   data_count;



    reg [7:0] mem [258:0];


    always begin
        #5 clk = ~clk;
    end

    initial begin
        clk = 0;
        rst_n = 1'b1;
        #10;
        rst_n = 1'b0;
        #10 rst_n = 1'b1;
    end

    initial begin
        $readmemh("D:/vivado_pf/Basic_pratice/uart_data.txt",mem);  
        $display("the file is load to mem");
        $display("Read memory1: %h", mem[0]) ;
        $display("Read memory2: %h", mem[1]) ;
        $display("Read memory3: %h", mem[2]) ;
        $display("Read memory4: %h", mem[3]) ;
    end

    initial begin
        wr_en <= 1'b0;
        rd_en <= 1'b0;
        wr_data <= 8'h00;
        #50;
        wr_data_to_fifo();

        #2620;
        rd_en <= 1'b1;




    end


    //写256个数据到FIFO中
    task wr_data_to_fifo(); //任务没有返回值
        integer i;
        begin
            for (i = 0; i < 257; i = i + 1)begin
                wr_data = mem[i];
                wr_en = 1;
                #10; //任务不能出现always语句,但是可以包含时序控制,任务可以没有或者多个输入、输出
            end 
            wr_en = 0;
        end 
    endtask

    custom_FIFO custom_FIFO_inst(
        .clk(clk),
        .rst_n(rst_n),
        .wr_en(wr_en),
        .wr_data(wr_data),
        .rd_en(rd_en),
        .fifo_full(fifo_full),
        .rd_data(rd_data),
        .fifo_empty(fifo_empty),
        .data_count(data_count)
    );

endmodule

1.3 行为仿真

  写数据在上升沿前后要有效一段时间,首先是正常工作的仿真,在设置的时候就没有溢出的情况,第一张图是开始写入,第二张图是写入结束。

在这里插入图片描述

在这里插入图片描述
  修改仿真文件,第一张表示溢出写,可以看到,溢出写无效。第二章可以看到读空出两个单元后,写入读写地址相同也就停止了写。到这里同步的FIFO功能就是正确的。
在这里插入图片描述
在这里插入图片描述

***学习位宽计算函数$clog2()

***$clog2()系统函数使用,可以不关注

  该系统函数可用于计算寻址给定大小的存储器所需的最小地址宽度或表示给定数量的状态所需的最小向量宽度。

//位宽计算函数
function integer clog2 (input integer depth);
begin
    for (clog2=0; depth>0; clog2=clog2+1) 
        depth = depth >>1;                          
end
endfunction
//$clog2(8) == 3;

  使用方法

parameter p_cnt_max = p_rev_time*p_clk_fre*1000_000 - 1; //翻转时间内所需计数的最大值
wire [$clog2(p_cnt_max)-1:0] w_cnt_max;
***分布式资源或者BLOCK BRAM

  还是参考博主的讲解,学习这个知识

  FPGA中FIFO的实现可以使用分布式资源或者BLOCK RAM,当使用FIFO缓冲空间较小时,我们选择使用Distributed RAM;当使用FIFO缓冲空间较大时,我们选择使用BLOCK RAM资源;这是一般的选择原则。而我们在声明我们的mem类型的变量时,通过添加约束(*ram_style = “distributed”)/(*ram_style = “block”)来指定。

(*ram_style = "distributed"*) reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];

或者:

(*ram_style = "block"*) reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
————————————————
版权声明:本文为CSDN博主「李锐博恩」的原创文章
原文链接:https://blog.csdn.net/Reborn_Lee/article/details/106619999

  这个点我想补充一下,在底层是如何实现的,VIVADO中资源之间的关系,FPGA 主要资源为: CLB、DSP、Block RAM、CMTs、GT以及XADC等等。一个CLB由2个Slice组成,Slice分为SLICEM和SLICEL,一个CLB里最多有一个SLICEM,即一个CLB可由两个SLICEL或一个SLICEL加一个SLICEM组成。SLICEL可用于逻辑,算术运算, SLICEM除了用于逻辑,算术运算外,还可配置成分布式RAM或32位的移位寄存器。

二.异步FIFO

2.1 在FIFO判满的时候有两种方式:

  • 通过计数器的方式

  也就是上面同步FIFO中的方式,优点是:逻辑简单,便于理解,且能够获得当前FIFO中的数据量;缺点:资源使用多,代码量大

  • 通过将读写指针增加一位

  增加的一位用来表示轮数,如果两个的附加位相同说明处于同一轮,如果附加位不同,则处于不同的轮数,此时,对于深度为2n的FIFO,需要的读/写指针位宽为(n+1)位,如对于深度为8的FIFO,需要采用4bit的计数器,0000~1000、1001~1111,MSB作为折回标志位,而低3位作为地址指针。
  判空满的逻辑如下图展示更加清晰

  • 当在同一轮,且读写指针相同,则为空;空的情况可能是复位,或者读指针赶上写指针
  • 当不在同一轮,但读写指针(地址)相同,则表示写指针转了一圈,追上都指针,判满

2.2 异步FIFO为什么要使用格雷码

2.2.1 介绍格雷码

在这里插入图片描述

  自然二进制数在表示一个连续变化的数值时,可能会有多个位同时发生变化,每个位翻转(变化)的频率是比较高的,这在某些应用场合,如在FPGA内部跨时钟域传输数据时,是十分不利的。
  上图展示了格雷码,十进制0-15的格雷码的对应关系,格雷码有两个特性,一个是循环特性,一个是单布特性。

  • 循环特性:当格雷码表示的数,当最大值表示位最小值时,只有以为发生翻转。可以看到0-7,8-15除过最高位不同,上下是对称的,7到8的格雷码只变了一位,0-15也只变了一位。
  • 单步特性:当表示来连续变换的数值时,仅有一个位发生翻转,保证传输的稳定性,较少传输误码率。

不过格雷码也有一个缺点,那就是相比于自然二进制码来说,它是一种无权码(而自然二进制码实际上是“8421”码,因此很难直接进行比较和数学运算,所以一般都需要将采集到的以格雷码为表示形式的数据先转换成自然二进制码,然后再参与运算。

2.2.2 格雷码在异步FIFO中的应用

  异步FIFO通过对比读写指针来进行满空判断,但是读写地址属于不同的时钟域,在比较之前需要将读写地址进行同步处理,然后进行判空满操作后才能够读写。这样会存在两个问题:第一个是跨时钟域容易发生亚稳态,导致判断出错。第二个问题是时钟是不同频,或者读快写慢,或者读慢写快,这时候尽管进行了地址同步,也可能有一定的滞后性。以读慢写快为例,在写入端需要判满,因为写的时钟快,所以对于同步后的读端的地址不会遗漏,读的地址只存在等于小于当前读地址,所以满标志只会提前产生。在读端,同步写的地址,写时钟满,必然会遗漏一部分地址,且及有可能写地址小于真实地址,所以,空标志也会提前产生,尽管地址有遗漏,但时对于FIFO的逻辑操作不会产生影响。这里不管是采用二进制编码还是格雷码编码,都会存在同步后的读写地址不符合实际情况,但是依然能够保证FIFO功能的正确性,需要注意gray码只是在相邻两次跳变之间才会出现只有1位数据不一致的情形,超过两个周期则不一定,所有地址总线bus 偏差一定不能超过一个周期,否则可能出现gray码多位数据跳变的情况,这个时候gray码就失去了作用,因为这时候同步后的地址已经不能保证只有1位跳变了。

  可以看到二进制编码的地址跨时钟同步最主要的问题是要消除亚稳态以及减少跳变。binary编码的地址总线在跳变时极易产生毛刺,因为binary编码是多位跳变,在实现电路时不可能做到所有的地址总线等长,address bus 偏差必然存在,而且写地址和读地址分属不同时钟域异步,这样地址总线在进行同步过程中出错不可避免,比如写地址在从0111到1000转换时4条地址线同时跳变,这样读时钟在进行写地址同步后得到的写地址可能是0000-1111的某个值,这个完全不能确定,所以用这个同步后的写地址进行FIFO空判断的时候难免出错。

  通过上面的分析可得,异步FIFO采用地址增加额外一位来判满,然后通过将二进制地址转换位格雷码,降低读写地址在连续变换时的位翻转概率,跨时钟域,然后大拍消除亚稳态,然后在转换为格雷码,进行地址比较,判断满空状态。

2.2.2 格雷码判满

  格雷码解决了一个问题,但同时也带来了另一个问题,即格雷码如何判断空与满?
  **对于“空”的判断依然是两者完全相等(**包括增加的位);

  对于“满”的判断由于于gray码除了MSB外,具有镜像对称的特点,存储深度为8,最多MSB表示的读写之间之间最大的差距是一轮,所以,格雷码的表示的深度是16。例如,当读指针指向7,写指针指向8时,除了MSB,其余位皆相同,但这个判满的条件显然不成立。因此不能单纯的只检测最高位了,在gray码上判断为满必须同时满足以下3条:

  1.wptr和同步过来的rptr的MSB不相等,因为wptr必须比rptr多折回一次。
  2.wptr与rptr的次高位不相等,如上图位置7和位置15,转化为二进制对应的是0111和1111,MSB不同说明多折回一次,111相同代表同一位置。
  3.剩下的其余位完全相等。
在这里插入图片描述

2.4 二进制与格雷码之间的转换

2.4.1 二进制码转换为格雷码的方法

  该过程也称为格雷码的编码,方法是从二进制码的最右边一位(最低位)起,依次将每一位与左边一位进行异或运算,作为对应格雷码该位的值,而最左边一位(最高位)不变。 对应公式如下:

g[n] <= b[n] //其中g[n]代表的是n位的格雷码,b[n]代表的是二进制码

g[n] <= g[n] ^ (g[n}] >> 1)

例如,将自然二进制码“10110”转换为格雷码,可以形象的用下图表示其转换过程:

在这里插入图片描述
简单说来,就是对二进制码右移移位,与其本身相异或即可。

2.4.2 格雷码转换为二进制码的方法

  该过程也称为格雷码的解码,方法是从格雷码左边第二位(次高位)起,将每一位与其左边一位解码后的值异或,作为该位解码后的值,而最左边一位(最高位)的解码结果就是它本身。对应公式如下:

b[n] <= g[n] //其中g[n]代表的是n位的格雷码,b[n]代表的是二进制码
b[n] <= g[n]^b[n+1] //只需要计算n-1位 的即可
例如,将格雷码“11101”转换为自然二进制码,可以形象的用下图表示其转换过程:
在这里插入图片描述
代码实现的过程要注意当二进制的最高位和格雷码的最高位是相同的,不需要计算直接赋值。

module gray2bin # (
	parameter N = 4;
)(
	input [N-1:0] gray,
	output [N-1:0] bin
	);
	
	assign bin[N-1] = gray[N-1];
	
	genvar i;
	generate
	for(i = N-2; i >= 0 ;i++)begin:gray_2_bin
		bin[i] <= gray[i] ^ bin[i+1];
	end
	endgenerate
endmodule

2.3 实现框图

在这里插入图片描述
  图和代码是对应的,以左边的写端为例输入到fifo_mem的地址为waddr[ADDR_SIZE-1 : 0],waddr[ADDR_SIZE:0] 转换格雷码为waddr_gray,在读端打两拍分为为:wr_gray2,wr_gray2。

2.5 实现及仿真代码

`timescale 1ns / 1ps
module asy_FIFO_gray#(
    parameter DATA_WIDTH = 8,                            //数据位宽
    parameter DATA_DEPTH = 8                            //数据深度
    )(
        input  wire                     wclk,
        input  wire                     wrst_n,
        input  wire                     winc,            //有效时写入数据
        input  wire  [DATA_WIDTH-1 : 0] wdata,
        output wire                     wfull,

        input  wire                     rclk,
        input  wire                     rrst_n,
        input  wire                     rinc,
        output reg   [DATA_WIDTH-1 : 0] rdata,
        output wire                      rempty
    );

    localparam  ADDR_SIZE = $clog2(DATA_DEPTH);     //ARR_SIZE = 3;
    reg  [ADDR_SIZE : 0] waddr,raddr;               //注意这里的地址宽度[3:0],是5
    wire [ADDR_SIZE : 0] waddr_gray, raddr_gray;    //转换后的格雷码位宽为[5:0]

    reg  [ADDR_SIZE : 0] wr_gray1, wr_gray2;           //读时钟域———打拍后的格雷码
    reg  [ADDR_SIZE : 0] rw_gray1, rw_gray2;           //写时钟域———打拍后的格雷码




   //二进制转换为格雷码 
    assign waddr_gray = waddr ^ (waddr >> 1) ;           //逻辑移位与算术移位的右移符号分别为“>>”和“>>>”,左移同理,逻辑移位不考虑符号位
    assign raddr_gray = raddr ^ (raddr >> 1) ;           //左移和右移都只补零;算术移位考虑符号位,左移补零,右移补符号位



    //将读端的读格雷码地址同步到写端
    //主要功能是将读写端的格雷码进行打拍,r_to_w
    //首先要注意格雷码的位宽大于地址位宽一位,所以rptr位宽为6,addrsize = 5,[5;0]
    //1
    always @(posedge wclk or negedge wrst_n)begin
        if (!wrst_n) 
            {rw_gray2,rw_gray1} <= 0;
        else 
            {rw_gray2,rw_gray1} <= {rw_gray1,raddr_gray};
    end


    //将写端的写格雷码地址同步到读端
    //主要功能是将读写端的格雷码进行打拍,w_to_r
    //2
    always @(posedge rclk or negedge rrst_n)begin
        if (!rrst_n) 
            {wr_gray2,wr_gray1} <= 0;
        else 
            {wr_gray2,wr_gray1} <= {wr_gray1,waddr_gray};
    end



    //存储器读写,写信号的有效需要判断你,读总是有效的,读出当前地址内的数据
    reg [DATA_DEPTH-1 : 0] fifo_mem [DATA_WIDTH-1 : 0];
    wire  wclken, rclken;


    //写端,当写有效时,写入数据
    always@(posedge wclk or negedge wrst_n) begin
		if(wrst_n == 0) begin
			waddr <= 0;
		end
		else if(wclken) begin
			fifo_mem[waddr[ADDR_SIZE-1 : 0]] <= wdata;
            waddr <= waddr + 1;
		end
	end

    //读端,当读有效时,读出数据
    always@(posedge rclk or negedge rrst_n) begin
		if(rrst_n == 0) begin
			raddr <= 0;
		end
		else if(rclken) begin
			rdata <= fifo_mem[raddr[ADDR_SIZE-1 : 0]] ;
            raddr <= raddr + 1;
		end
	end


    //判满逻辑
    //wire wfull;
    localparam  ADDRSIZE = $clog2(DATA_DEPTH);     //ARR_SIZE = 3;
    assign wfull = wrst_n? (waddr_gray=={~rw_gray2[ADDRSIZE:ADDRSIZE-1], rw_gray2[ADDRSIZE-2:0]}) : 1'b0;

    //判空逻辑
    //wire rempty;
    assign rempty = rrst_n ? (raddr_gray == wr_gray2): 1'b0;

    assign wclken = winc & !wfull;
    assign rclken = rinc & !rempty;
endmodule

  其中需要注意的有如下几点:

  • 首先重要的是fifo_mem的地址深度,这里便于解释深度设置为8,但其中因为存在覆盖写,所以在格雷码的转换时Waddr要能够表示16深度的地址,所以,Waddr设置位宽为[3:0],但是在fifo_mem的输入地址要取waddr[2:0],这可以在代码的写端和读端可以有体现
  • 同步地址的时钟应该使用本域时钟
`timescale 1ns / 1ps
module sim_asy_FIFO_gray();
    parameter DATA_WIDTH = 8;
	parameter DATA_DEPTH = 8;

    reg                      wclk;
    reg                      wrst_n;
    reg                      winc;           //有效时写入数据
    reg   [DATA_WIDTH-1 : 0] wdata;
    wire                     wfull;

    reg                      rclk;
    reg                      rrst_n;
    reg                      rinc;
    wire  [DATA_WIDTH-1 : 0] rdata;
    wire                     rempty;


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

	initial begin
		rclk = 0;
		forever begin
			#10 rclk = ~rclk;
		end
	end

	initial begin
		wrst_n = 1;
		rrst_n = 1;
		winc = 0;
		rinc = 0;
		#30 
		wrst_n = 0;
		rrst_n = 0;
        #30
		wrst_n = 1;
		rrst_n = 1;


		//write data into fifo buffer
		@(negedge wclk) 
		wdata = $random;
		winc = 1;

		repeat(8) begin
			@(negedge wclk) 
			wdata = $random; // write into fifo 8 datas in all;
		end
        
        // read parts
		@(negedge wclk) 
		winc = 0;

		@(negedge rclk) 
		rinc = 1;

		repeat(8) begin
			@(negedge rclk);  // read empty 
		end 
		@(negedge rclk)
		rinc = 0;

		//write full
		# 80

		@(negedge wclk)
		winc = 1;
		wdata = $random;

		repeat(15) begin
		@(negedge wclk)
			wdata = $random;
		end
               
		@(negedge rclk) 
		rinc = 1;
		
		repeat(8) begin
			@(negedge rclk);  // read empty 
		end 
		@(negedge rclk)
		rinc = 0;
		
		@(negedge wclk)
		winc = 0;


		#50 $finish;
	end

	asy_FIFO_gray #(
			.DATA_WIDTH(DATA_WIDTH),
			.DATA_DEPTH(DATA_DEPTH)
    ) inst_asy_FIFO_gray (
			.wclk(wclk),
			.wrst_n(wrst_n),
			.winc(winc),
			.wdata(wdata),
			.wfull(wfull),

			.rclk(rclk),
			.rrst_n(rrst_n),
			.rinc(rinc),
			.rdata(rdata),
			.rempty(rempty)
		);

endmodule

2.6 仿真图验证

在这里插入图片描述
在这里插入图片描述

2.7 结论

  这篇从原理和实现上都进行了记录,可以看到功能实现正确。如果有问题,可以评论交流。

  • 8
    点赞
  • 62
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值