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


写在前面

一开始是想既然是极简教程,就应该只给出FIFO的概念,没想到还是给出了同步以及异步FIFO的设计,要不然总感觉内容不完整,也好,自己设计的FIFO模块不用去担心因IP核跨平台不通用的缺陷!那我们开始吧。


正文

同步FIFO回顾

上一篇博客讲了同步FIFO的概念以及同步FIFO的设计问题,并给出了同步FIFO的Verilog代码以及VHDL代码,并经过了行为仿真测试,链接如下:

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

$clog2()系统函数使用

这里简单提一下,同步FIFO的代码中用到了一个系统函数$clog2(),这个系统函数的使用方法很简单:

parameter DATA_WIDTH = 8;
parameter DATA_DEPTH = 8;

reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];

reg [$clog2(DATA_DEPTH) - 1 : 0] wr_pointer = 0;
reg [$clog2(DATA_DEPTH) - 1 : 0] rd_pointer = 0;

例如我定义了FIFO缓冲区的深度为DATA_DEPTH = 8,那么其地址(指针)位宽是多少呢?
这时候就可以使用系统函数$clog2()了,位宽可以表示为:

$clog2(DATA_DEPTH)                  // = 3;

指针就可以定义为:

reg [$clog2(DATA_DEPTH) - 1 : 0] wr_pointer = 0;
reg [$clog2(DATA_DEPTH) - 1 : 0] rd_pointer = 0;

综合属性控制资源使用

还有一点需要提的是,我们都知道在FPGA中FIFO的实现可以使用分布式资源或者BLOCK RAM,那么如何掌控呢?
当使用FIFO缓冲空间较小时,我们选择使用Distributed RAM;当使用FIFO缓冲空间较大时,我们选择使用BLOCK RAM资源;这是一般的选择原则。
我们可以通过在设计代码中加入约束条件来控制,之前有写过

Vivado 随笔(1) 综合属性之 ram_style & rom_style?

就上述同步FIFO而言,我们可以在缓冲区定义时候添加如下约束:

(*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];

为了验证是否有用,我们在Vivado中进行验证如下:

当设计中使用BLOCK RAM约束:

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

综合后的电路图如下,可见FIFO缓存区使用的资源为BLOCK RAM;


综合后的电路图

同时给出资源利用率报告:

资源利用情况

可见存在BLOCK RAM ,由于我仅仅综合了一个同步FIFO,因此这个Block RAM一定是FIFO缓冲区消耗的。

当使用Distributed RAM约束时:

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

综合后电路图FIFO缓冲区部分:

综合后的电路图

资源利用率情况;


资源利用情况

可见 ,并未使用BLOCK RAM,而是使用了LUT RAM,也即分布式RAM。
综上,验证了这条约束的有效性。

异步FIFO设计

FIFO用途回顾

再设计异步FIFO电路之前,有必要说明一下FIFO的用途,上篇博文提到:

  • 跨时钟域

FPGA或者ASIC设计内部电路多位数据在不同的时钟域交互,为了数据安全、正确、稳定交互,我们需要设计异步FIFO进行跨时钟域交互。正如之前博客所写:漫谈时序设计(1)跨时钟域是设计出来的,而非约束出来的!
我们在时序分析时候,通常都将跨时钟域路径进行伪路径约束,因此我们必须在设计时候解决跨时钟域数据传输问题,异步FIFO在此起到关键作用。

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

这三条大概讲的都是一个意思,总结起来就是FIFO可以起到数据缓冲或缓存的作用,例如突然数据,我们就需要先将其缓存起来,之后再从FIFO中读出出来进行处理,这样也可以保证数据不会丢失。

引用互联网上其他说法就是:数据写入过快,并且间隔时间长,也就是突发写入。那么通过设置一定深度的FIFO,可以起到数据暂存的功能,且使得后续处理流程平滑。

异步FIFO原理回顾

无论是同步FIFO还是异步FIFO,其大致原理都是一致的,先入先出自然不必多说,关于空满的判断都是通过读写指针之间的关系来判断;还有就是异步FIFO的指针需要进行一定的处理,例如格雷码处理,这样可以减小读指针同步到写指针时钟域,或者写指针同步到读指针时钟域时出现亚稳态的概率,这是因为格雷码每次只有一位变化,这样一位数据在进行跨时钟域传输的时候亚稳态出现的概率会大大减小。同步之后便进行对比,以此来判断FIFO的空满。
那异步FIFO如何判断空满呢?
回答这个问题之前,我想先统一的说明FIFO(同步或者异步)是如何判断空满的?


同步FIFO空满判断示意框图

起始,读写指针都是0,FIFO一定为空;之后对FIFO进行一系列的读写操作,导致读写指针关系发生了变化,可以分为下面两种情况:

  1. 读比写要快,或者说读指针追写指针,如果追上了,也即二者再次相等,则FIFO读空;
  2. 写比读快,或者说写指针弯道超越追读指针,当写指针再次绕到读指针背后并与读指针重合,也即二者相等时,FIFO写满!

在同步FIFO中,我们使用计数的方式进行判断空满,运用的也是这个原理,写一个数据时,计数值加1,读出一个数据时,计数值减1,如下图:

FIFO存取数据原理图

我最喜欢用这幅图来分析FIFO,下面一行一行的分析:

  1. 第一行:写入1个数据,计数值为1;
  2. 第二行:写入5个数据,计数值为6;
  3. 第三行:读出3个数据,计数值为3;
  4. 第四行:写入3个数据,计数值为6;
  5. 第五行:写入2个数据,计数值为8,等于FIFO深度,则表示写满;
  6. 第六行:读出6个数据,计数值为2,表示还剩下两个数据缓存在FIFO中。

如果再接着读2个 数据,则计数值为0,FIFO就被读空了。

好了,我们分析完了同步FIFO是如何判断空满的,下面重点放在异步FIFO的原理上。

我曾写过一篇CDC问题的博客,谈到了异步FIFO的设计:
谈谈跨时钟域传输问题(CDC)

这篇博客中说,同步FIFO可以使用计数方式来判断空满,但是异步FIFO不能,因为写指针和读指针根本不在同一个时钟域,计数器无法处理这样的计数。
那么怎么处理呢?
博客里采用的方法是对读写指针的位宽多添1位,这样可以在读写指针相等时,表示FIFO空,而在写指针和读指针最高位不同,而其他位相等时,也即写指针大于读指针一个FIFO深度的数值,表示FIFO满,这不就是意味着写指针绕了一圈,又追上了读指针了吗?
恰是如此,用来解决不用计数而具体判断FIFO空满的问题。

这只是解决了判断空满的一个问题,也就是确定指针的关系!
那下一个问题就是如何判断?
由于读写指针不在同一个时钟域,二者需要同步到同一个时钟域后进行判断大小。


异步FIFO原理示意图

具体的操作就是在各自的时钟域内进行读写操作,同时:

  1. 判断是否写满时,需要将读指针转换成格雷码形式,再同步到写时钟域,与写指针比较,判断是否写满!
    细心的人恐怕能否发现,这里存在的一个小插曲,当读指针转换成格雷码以及同步到写时钟域的过程中,读写指针可能还都在递增,这样的话,等同步后的读指针与写指针相等时(不包括最高位),实际的读指针可能已经变了,这样的话其实还有几个空间没有写满!但这样设计就有问题吗?没有问题!这叫保守设计,可以增加FIFO的安全性。

下面是判断是否写满的示意图:


判断是否写满的示意图

上面是写满判断的情况,下面给出读空判断的可能情形分析:

  1. 当判断是否读空时,需要把写指针同步到读时钟域,具体过程是先将写指针转换为格雷码,再同步到读时钟域,之后和读指针比较,如果二者相等,则空标志置位!
    还是和第一种情况有同样的插曲,当写指针转换成格雷码以及同步到读时钟域的过程中,写指针和读指针都可能还在递增,这样当二者判断相等的时候,则写指针可能还多写了几个空间,实际上并没有读空。
    那问题来了,这样操作就有问题了吗?同样没有问题,这样也保证来了FIFO的安全,防止被读空。

下面给出手绘示意图:

判断是否读空的示意图

到此,这一种设计方式的异步FIFO算是讲完了,下面就是设计的问题了。

异步FIFO设计

如果你认真分析了上述异步FIFO的实现方式,那么你会分分钟写出实现代码,我的版本如下:

`timescale 1ns / 1ps
////////////////////////////////////////////////////
// Engineer: Reborn Lee
// Module Name: asy_fifo
// https://blog.csdn.net/Reborn_Lee
////////////////////////////////////////////////////


module asy_fifo#(
    parameter DATA_WIDTH = 8,
    parameter DATA_DEPTH = 32
    )(
    input wr_clk,
    input wr_rst,
    input wr_en,
    input [DATA_WIDTH - 1 : 0] wr_data,
    output reg full,

    input rd_clk,
    input rd_rst,
    input rd_en,
    output reg [DATA_WIDTH - 1 : 0] rd_data,
    output reg empty

    );


    // define FIFO buffer 
    reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];

    //define the write and read pointer and 
    //pay attention to the size of pointer which should be greater one to normal

    reg [$clog2(DATA_DEPTH) : 0] wr_pointer = 0, rd_pointer = 0; 

    //write data to fifo buffer and wr_pointer control
    always@(posedge wr_clk) begin
        if(wr_rst) begin
            wr_pointer <= 0;
        end
        else if(wr_en) begin
            wr_pointer <= wr_pointer + 1;
            fifo_buffer[wr_pointer] <= wr_data;
        end

    end

    //read data from fifo buffer and rd_pointer control
    always@(posedge rd_clk) begin
        if(rd_rst) begin
            rd_pointer <= 0;
        end
        else if(rd_en) begin
            rd_pointer <= rd_pointer + 1;
            rd_data <= fifo_buffer[rd_pointer];
        end

    end

    //wr_pointer and rd_pointer translate into gray code

    wire [$clog2(DATA_DEPTH) : 0] wr_ptr_g, rd_ptr_g; 

    assign wr_ptr_g = wr_pointer ^ (wr_pointer >>> 1);
    assign rd_ptr_g = rd_pointer ^ (rd_pointer >>> 1);



    //wr_pointer after gray coding synchronize into read clock region
    reg [$clog2(DATA_DEPTH) : 0] wr_ptr_gr, wr_ptr_grr, rd_ptr_gr, rd_ptr_grr; 

    always@(rd_clk) begin
        if(rd_rst) begin
            wr_ptr_gr <= 0;
            wr_ptr_grr <= 0;
        end
        else begin
            wr_ptr_gr <= wr_ptr_g;
            wr_ptr_grr <= wr_ptr_gr;
        end
    end


    //rd_pointer after gray coding synchronize into  write clock region
    always@(wr_clk) begin
        if(wr_rst) begin
            rd_ptr_gr <= 0;
            rd_ptr_grr <= 0;
        end
        else begin
            rd_ptr_gr <= rd_ptr_g;
            rd_ptr_grr <= rd_ptr_gr;
        end
    end

    // judge full or empty

    always@(posedge rd_clk) begin
        if(rd_rst) empty <= 0;
        else if(wr_ptr_grr == rd_ptr_g) begin
            empty <= 1;
        end
        else empty <= 0;
    end

    always@(posedge wr_clk) begin
        if(wr_rst) full <= 0;
        else if( (rd_ptr_grr[$clog2(DATA_DEPTH) - 1 : 0] == wr_ptr_g[$clog2(DATA_DEPTH) - 1 : 0])
            && ( rd_ptr_grr[$clog2(DATA_DEPTH)] != wr_ptr_g[$clog2(DATA_DEPTH)] ) ) begin
            full <= 1;
        end
        else full <= 0;
    end





endmodule

注意事项

  • 读写指针宽度要是$clog2(DATA_DEPTH) + 1,定义的时候应该定义为:
reg [$clog2(DATA_DEPTH) : 0] wr_pointer = 0, rd_pointer = 0; 
  • 其次,判断空的时候要拿转换为格雷码并且同步到读时钟域之后的写指针与读指针比较,比较代码如下:
always@(posedge rd_clk) begin
        if(rd_rst) empty <= 0;
        else if(wr_ptr_grr == rd_ptr_g) begin
            empty <= 1;
        end
        else empty <= 0;
    end

一定要二者相等的下一个读周期empty信号为1;

  • 对于满full信号,一定要用转换为格雷码且同步到写时钟域之后的读指针与转换为格雷码之后的写时钟比较,比较的条件是最高位不同,但是其他位相同。
always@(posedge wr_clk) begin
        if(wr_rst) full <= 0;
        else if( (rd_ptr_grr[$clog2(DATA_DEPTH) - 1 : 0] == wr_ptr_g[$clog2(DATA_DEPTH) - 1 : 0])
            && ( rd_ptr_grr[$clog2(DATA_DEPTH)] != wr_ptr_g[$clog2(DATA_DEPTH)] ) ) begin
            full <= 1;
        end
        else full <= 0;
    end
  • 最后提出的是转换为格雷码的方式是组合逻辑的方式,即:
//wr_pointer and rd_pointer translate into gray code

    wire [$clog2(DATA_DEPTH) : 0] wr_ptr_g, rd_ptr_g; 

    assign wr_ptr_g = wr_pointer ^ (wr_pointer >>> 1);
    assign rd_ptr_g = rd_pointer ^ (rd_pointer >>> 1);

当然你用时序逻辑也可以哦。

异步FIFO仿真

我们对上述设计进行行为仿真,先给出我的测试文件:


`timescale 1ns/1ps
module asy_fifo_tb;
    parameter DATA_WIDTH = 8;
    parameter DATA_DEPTH = 16;

    reg wr_clk;
    reg wr_rst;
    reg wr_en;
    reg [DATA_WIDTH - 1 : 0] wr_data;
    wire full;

    reg rd_clk;
    reg rd_rst;
    reg rd_en;
    wire [DATA_WIDTH - 1 : 0] rd_data;
    wire empty;

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

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

    initial begin
        wr_rst = 1;
        rd_rst = 1;
        wr_en = 0;
        rd_en = 0;
        #30 
        wr_rst = 0;
        rd_rst = 0;

        //write data into fifo buffer
        @(negedge wr_clk) 
        wr_data = $random;
        wr_en = 1;

        repeat(7) begin
            @(negedge wr_clk) 
            wr_data = $random; // write into fifo 8 datas in all;
        end

        // read parts
        @(negedge wr_clk) 
        wr_en = 0;

        @(negedge rd_clk) 
        rd_en = 1;

        repeat(7) begin
            @(negedge rd_clk);  // read empty 
        end 
        @(negedge rd_clk)
        rd_en = 0;

        //write full
        # 150

        @(negedge wr_clk)
        wr_en = 1;
        wr_data = $random;

        repeat(15) begin
        @(negedge wr_clk)
            wr_data = $random;
        end

        @(negedge wr_clk)
        wr_en = 0;


        #50 $finish;





    end




    asy_fifo #(
            .DATA_WIDTH(DATA_WIDTH),
            .DATA_DEPTH(DATA_DEPTH)
        ) inst_asy_fifo (
            .wr_clk  (wr_clk),
            .wr_rst  (wr_rst),
            .wr_en   (wr_en),
            .wr_data (wr_data),
            .full    (full),
            .rd_clk  (rd_clk),
            .rd_rst  (rd_rst),
            .rd_en   (rd_en),
            .rd_data (rd_data),
            .empty   (empty)
        );


endmodule

仿真波形为:


读写FIFO仿真波形

写FIFO仿真波形

仿真通过,且功能符合预期。

由于本博客写的时候有点长,几乎一天了,所以就到这里吧!不得不说的是,异步FIFO的实现方式肯定不只有这一种,还有很多其他实现方式,各位可以自行尝试。
后面如果有更多有关FIFO的有趣知识或者心得体会,我会继续补充!


参考资料


交个朋友

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342