FPGA基础学习(11) -- FIFO设计(style#1)

FIFO是跨时钟域数据传输中常用的缓存器。一般情况下,自己设计的异步FIFO(无特殊说明以下均简称FIFO)虽然能应付90~99%的场景,但是由于设计缺陷,导致在1%的极端情况下会出问题,还不容易发现,所以设计合理的FIFO至关重要。

对于同步FIFO,因为读写属于同一时钟域,可以直接采用计数的方式来计算FIFO存储空间的动态变化,但是异步FIFO不能这么操作,因为读写时钟域完全有可能频率差异比较大,并且会面临暂稳态的问题。其实FIFO的设计要点,归根结底是设计正确的空/满信号。即数据写满的时候,写时钟域能及时接收到满信号,停止写入;数据读空的时候,读时钟域能及时接收到空信号,停止读出。

学习《Clifford_E._Cummings》经典论文集中的有关异步FIFO的论文,其中介绍了2种FIFO设计方案,本文是源于论文《Simulation and Synthesis Techniques for Asynchronous FIFO Design》中介绍的style#1。论文中引入了一种跨时钟域同步格雷码进行比较的方式来判断空/满。

1. 异步指针

首先来建立对指针的基本认识。

我们知道FIFO实际上由一个异步RAM作为基本的存储单元,再配合外面的控制逻辑实现的。控制逻辑中最重要的就是指针,了解计算机体系结构的都知道,指针无非就是指向存储空间的一个标识。

如上图所示为一个深度为16的FIFO指针示意图。一个FIFO含有一个写指针raddr[3:0]和一个读指针waddr[3:0]。在FIFO中注意以下两点:

  • 读指针指向当前要读取的数据位置(空间);
  • 写指针指向下一个要写入的位置(空间),即下一个数据来的时候才写入该空间;

读写指针在工作中呈现“你追我赶”的情形。比如当raddr = 0,waddr = 7时,代表FIFO中存有7个数据。当raddr追上waddr时,即 raddr = waddr = 7,代表刚才写入的7个数据被读走,此时FIFO为空。在复位这种特殊的情况下,raddr和waddr的初始状态均为0,此时FIFO显然也为空。

假设此时只写入数据,当写到地址15(1111)时,继续写入数据,指针增加会翻转到地址0(0000),当写入到waddr = 7时,读指针也在该位置,即raddr = waddr = 7,此时显然存储空间已满。

那么当raddr = waddr时,到底是空还是满?所以设计中引入“补位”指针的概念,增加一位最高位,代表指针是否经过了一个轮询(翻转)。所以上述指针地址变为5位,当写指针从地址15翻转到0时,指针实际上是从01111变为10000。此时写指针最高位1 代表翻转一次,读指针的最高位依然是0。

因此,我们判断空满的条件是:空的时候全相等,满的时候最高位不等,其余位相等。

2. 格雷码计数

在上节的例子中,我们提到了二进制编码指针的翻转问题,即15->0(1111->0000),在一个写入周期中指针的数据位翻转了4次,在实际使用中这无疑会增加风险,因为4 bit的数据走线延迟不一致,导致同一采样时钟沿上可能在某一位上出现暂稳态。因此,论文提出了一种格雷码指针的编码的方式(格雷码在FIFO设计中还有其他优势,后面再讨论)。

如上图所示,给出了0-15的格雷码,按照上一节提到的最高位补位指示空满的原则,4位格雷码指针可以设计深度为8的FIFO。

假设存储空间位置为0-7,rptr = wptr = 7(0100)时,表示写入的8个数据全部被读出,此时FIFO为空。继续写入1个数据,写指针变为8(1100),按照上一节的结论,当读写指针的最高位不同,其余位相同时,FIFO为满。显然,这一结论在格雷码使用过程中出现了问题。因为从空间7到8,才写入了1个数据,它怎么可能满。

观察格雷码的编码形式可以发现,除最高位以为,0-7和8-15是关于中间位置的“镜像对称”,即除最高位外,7和8一致,6和9一致……0和15一致,如上图所示。假设我们把8-15的次高位取反会出现一种什么样的情况?继续按照上面的例子假设,当rptr = wptr = 7 时,FIFO为空。继续写数据,当格雷码变为1100(次高位取反后的15),表示FIFO中又写入了8个数据,这时候FIFO才是真正的满了。此时rptr =0100,wptr = 1100,此时满足最高位不同,其余位相同,则表示满的原则,但前提是次高位已经取了反。所以综上所述,使用格雷码采用的比较原则是“最高位和次高位不同,其余位相同,则表示FIFO满”。

总结一下,使用格雷码判断空满,原则是:

  • 格雷码各位完全相同,代表空;
  • 最高位和次高位不同,其余位相同,代表满

3. 如何操作空/满

上两节得出了通过判断补位格雷码的关系来操作空/满,具体操作肯定是读指针的格雷码和写指针的格雷码进行比较,但是因为读写时属于两个不同的时钟域,两者的时钟频率可能差异很大,具体如何实现呢?显然一个指针肯定要同步到对方时钟域上进行比较。先给出论文的设计:

  • “空”:读时钟域中,比较读指针和同步过来的写指针,如果两指针相同 ,为“空”;
  • “满”:写时钟域中,比较写时钟和同步过来的读指针,如果两指针最高位和次高位不同,其余位相同,为“满”;

因为是跨时钟域,所以会涉及到读写时钟差异的问题,论文中对两个读者感到疑惑的问题进行了解答

- 问题1:同步格雷码的过程中变化了2次,但只有一次被对方时钟域采集到,会不会造成多位同步出错的问题?

(ps:实际上对原位的问题和解答翻译理解不到位,此处只能我的理解简单说一下)

答案是不会出问题。当然如果在慢时钟上升沿采样的过程中,快时钟域的格雷码发生变化,比如会出一些问题。但是这是另外一个层面的问题(时序暂稳态问题)。实际的问题是,如果快时钟域的格雷码变化了两次或多次,但是慢时钟只采集到第二次的结果,是不产生问题的。因为第一次格雷码的变化已经代表操作正常完成(不管是读还是写),第二次变化仅仅表示当前的状态,就判断当前状态即可。(语无伦次了……算了,这个问题过掉,不清楚作者表达)

- 问题2:假设写时钟频率更高,写指针在与读指针比较的时候,还没来得及产生满信号,写这一头还在不停的写入数据,会不会造成上溢出?(读空是同样的道理)

(ps:这个问题是FIFO操作最常见的问题)

答案是不会出问题。解答这个问题,我们让深刻理解本节开头的原则,即“空信号是在读时钟域产生,满信号是在写时钟域产生”。

首先,我们要明确FIFO的使用场景,不会是连续数据的跨时钟域传输,因为这样必然会丢掉部分数据。所以必然是块数据传递,要么写快读慢,要么写慢读快。

在写快读慢的情形,担心满信号没有及时产生,导致写溢出。满信号是在写时钟域产生,即读指针同步到写时钟域,这个时候写指针是不可能越过读指针的,要么就最高位和次高位不同,其余位相等,产生满信号,这时候立刻停止写入数据了。

那就又有疑问了,在比较指针的时候,读时钟域继续再读,可能读出几个数据了,这个时候产生满信号合适吗?这就是文章中所说的“虚满”,虚满无法就是FIFO空出了几个空间而已,不会导致数据出问题,这是一种保守的设计方法。

同理,读的时候也不会出现,下溢出的情况。但也有“需空”情形。

4. 仿真结果

论文中有详细的源码。为了便于理解其中指针的变化,写了段测试代码用于仿真观察。testbench先写满,再读空,再边写边读。


   module fifo1_sim();
    parameter DSIZE = 8;
    parameter ASIZE = 3;

    wire  [DSIZE-1:0] rdata;
    wire              wfull;
    wire              rempty;

    reg   [DSIZE-1:0] wdata;
    reg   winc, wclk, wrst_n;
    reg   rinc, rclk, rrst_n;

    reg             wr_en;
    reg             rd_en;
    reg             wr_rd;

    initial begin
        wrst_n = 1'b0;
        rrst_n = 1'b0;
        #50;
        wrst_n = 1'b1;
        rrst_n = 1'b1;
    end

    initial begin
        wclk = 1'b0;
        #10;
        forever #5 wclk =~wclk;
    end

    initial begin
        rclk = 1'b0;
        #10;
        forever #10 rclk =~rclk;
    end

    initial begin
        wr_en = 1'b0;
        rd_en = 1'b0;
        wr_rd = 1'b0;
        #100;
        wr_en = 1'b1;
        rd_en = 1'b0;
        wr_rd = 1'b0;
        #150
        wr_en = 1'b0;
        rd_en = 1'b1;
        wr_rd = 1'b0;
        #200
        wr_en = 1'b0;
        rd_en = 1'b0;
        wr_rd = 1'b1;
        #200
        wr_en = 1'b0;
        rd_en = 1'b0;
        wr_rd = 1'b0;

    end

    always @(posedge wclk or negedge wrst_n) begin
        if(wrst_n == 1'b0) begin
            wdata <= 8'h10;
            winc <= 1'b0;
        end else if(wr_en) begin
            wdata <= wdata + 8'd1;
            winc <= 1'b1;
        end else if(wr_rd) begin
            wdata <= wdata + 8'd1;
            winc <= 1'b1;
        end else begin
            wdata <= wdata;
            winc <= 1'b0;
        end
    end

     always @(posedge rclk or negedge rrst_n) begin
        if(rrst_n == 1'b0) begin
            rinc <= 1'b0;
        end else if(rd_en) begin
            rinc <= 1'b1;
        end else if(wr_rd) begin
            rinc <= 1'b1;
        end else begin
            rinc <= 1'b0;
        end
    end

fifo1 #( DSIZE,ASIZE)
fifo1_i
(
    .rdata  (rdata),
    .wfull  (wfull),
    .rempty (rempty),

    .wdata  (wdata),
    .winc   (winc),
    .wclk   (wclk),
    .wrst_n (wrst_n),
    .rinc   (rinc),
    .rclk   (rclk),
    .rrst_n (rrst_n)
);

endmodule

仿真图如下:

5. 特别说明

  • 原文有很多细节描述,实际上并不是理解很透彻。如问题1,还有比较二进制编码和格雷码优劣的论述。
  • 该篇论文(V1.2)感觉有问题,源代码实现的应该是style#2的框图,如下图所示,即二进制编码驱动RAM的地址,格雷码进行指针比较。多说一句,源码中格雷码和RAM的地址是一一对应的标识关系,格雷码不是地址,只是对地址空间的一个描述,用于跨时钟域比较。
  • 网上有帖子争论较大的是,要是读写时钟频率差距过大,比较1000倍,FIFO会不会出问题?从论文的角度分析,不会出问题(可以展开讨论)。从而牵扯另外的一个话题,FIFO最小深度的计算。

原文地址:https://www.cnblogs.com/rouwawa/p/12409150.html

时间: 2024-08-01 02:31:31

FPGA基础学习(11) -- FIFO设计(style#1)的相关文章

python基础学习11(核心编程第二版)部分

# -*- coding: utf-8 -*- # ==================== #File: python #Author: python #Date: 2014 #==================== __author__ = 'Administrator' #执行环境 #可调用对象 """ 许多的python 对象都是我们所说的可调用的,即是任何能通过函数操作符“()”来调用的对象.要调用可调用对象, 函数操作符得紧跟在可调用对象之后.Python 有4

2016-2-5 linux基础学习11

grep, egrep, fgrep grep:根据模式,搜索文本,并将符合模式的文本行显示出来pattern:文本字符和正则表达式的元字组合而成飞匹配条件 grep [options] PATTERN [FILE...]    -i 不区分大小写    --color 给符合标准的查找字符标上颜色    -v 显示没有被模式匹配的行    -o 只显示被模式匹配到的字符串,一个一行     *: 任意长度的字符?:任意单个字符[]:匹配指定范围内的任意单个字符[^]:匹配指定范围外的任意单个字

java基础学习——11、组合

我们已经尝试去定义类.定义类,就是新建了一种类型(type).有了类,我们接着构造相应类型的对象.更进一步,每个类型还应该有一个清晰的接口(interface),供用户使用. 我们可以在一个新类的定义中使用其他对象.这就是组合(composition).组合是在Java中实现程序复用(reusibility)的基本手段之一. 我们下面定义一个Battery类,并用power来表示其电量.一个Battery的可以充电(chargeBattery)和使用(useBattery).我们在随后的Torc

python基础学习11天,作业题

1. 文件a.txt内容:每一行内容分别为商品名字,价钱,个数. apple 10 3 tesla 100000 1 mac 3000 2 lenovo 30000 3 chicken 10 3 通过代码,将其构建成这种数据类型:[{'name':'apple','price':10,'amount':3},{'name':'tesla','price':1000000,'amount':1}......] 并计算出总价钱. 2,有如下文件: ------- alex是老男孩python发起人,

Python基础学习11

函数 代码的一种组织形式,一个函数一般完成一项特定的功能 函数的使用: 函数需要先定义 使用函数,俗称调用 定义一个函数: 函数的定义部分不会被执行 def关键字,后跟一个空格 函数名,自己定义,起名需要遵循便令命名规则,约定俗成,大驼峰命名只给类用 后面括号和冒号不能省,括号内可以有参数 函数内所有代码缩进 语法 def 函数名(参数列表): 函数体 实例: def func1(): # 定义函数,此处未传参数 print('这就是函数!') print('我不玩了.') func1() #

FPGA基础设计(四):IIC协议

很多数字传感器.数字控制的芯片(DDS.串行ADC.串行DAC)都是通过IIC总线来和控制器通信的.不过IIC协议仍然是一种慢速的通信方式,标准IIC速率为100kbit/s,快速模式速率为400kbit/s.本文致力于讲述如何用计数器控制和分频时钟控制两种方式完成IIC的读写操作. IIC协议 ??IIC协议是一种多机通讯,由SDA数据线和SCL时钟线构成串行总线,所有的IIC设备都可以挂载到总线上,但每个设备都有唯一的设备读地址和设备写地址.在使用IIC作为数字接口的芯片datasheet中

适合零基础小白学习的ui设计方法及课程大纲分享

ui界面设计教程学什么?首先想要学习ui设计,你得了解ui界面设计是什么?只有了解清楚了你才能更好的去开展学习. 对于很多从零基础开始学习ui设计的人来说,不知道ui要学会哪些内容才能更好的工作,所以今天就把ui界面设计课程大纲分享给大家.不管是有基础还是没有基础的同学都可以根据这份课程大纲来学习.有条理的学习才能更好的了解和掌握阶段性的学习课程,也能了解自己学到了哪个部分,掌握了哪些技能. 1.PS技术 如果现在还有不会用PS的同学,请先去搜索免费的基础课程来学习,至少要用透左边的工具栏,还有

零基础学习前端1-1配置node及npm环境变量

零基础学习前端1-1配置node及npm环境变量 ## 1-1配置node及npm环境变量 首先:下载node 可以直接去官方网站下载 1.首先从官网下载安装包 https://nodejs.org/ 我这里下载的是windows的 可以进去选择版本 下载后的安装包 2.下一步直接安装,可以选择对应的目录,建议不要选择默认的path 我这里的安装目录:E:\webyikeshuo\node 3.接下来去配置path,"我的电脑"-右键-"属性"-"高级系统

ASP.Net MVC开发基础学习笔记(3):Razor视图引擎、控制器与路由机制学习

首页 头条 文章 频道                         设计频道 Web前端 Python开发 Java技术 Android应用 iOS应用 资源 小组 相亲 频道 首页 头条 文章 小组 相亲 资源 设计 前端 Python Java 安卓 iOS 登录 注册 首页 最新文章 经典回顾 开发 Web前端 Python Android iOS Java C/C++ PHP .NET Ruby Go 设计 UI设计 网页设计 交互设计 用户体验 设计教程 设计职场 极客 IT技术