FPGA FIFO基本原理之异步FIFO

  • 异步FIFO设计

 

  • 在上一篇文章中我们说到同步FIFO可以使用计数方式来判断空满,但是在设计异步FIFO时却不能通过对FIFO中数据个数计数的方式来进行空满判断,因为写指针和读指针根本不在同一个时钟域,计数器无法处理这样的计数,因为到底是在读时钟域内计数呢还是在写时钟域内计数呢?
  • 不能用计数器的方式,但是我们可以用上一篇文章提到的另一个方法:将读写指针高位扩展一位,通过对读写指针最高位以及其余数值位的比较来判断异步FIFO空满的状态:1.读写指针的高位相同,且数值位相同时,表示FIFO为空,也就是说扩展一位位宽后的读写指针相同时FIFO为空 2.读写指针的高位不同,但数值位相同时,表示FIFO位满,其实就是写指针绕了一圈,又追上了读指针。
  • 上面只是确定了读写指针和空满状态的关系,读写指针不在同一个时钟域,所以我们还需要将其同步到同一个时钟域内。在判断FIFO是否为满时需要将读指针同步到写时钟域去产生满信号。判断FIFO是否为空时需要将写指针同步到读时钟域去产生空信号。
  • 读写指针是多位的,也相当于是多bit的数据了,不太适合直接两级同步,两级同步适用于单比特变化的数据,然后指针数据变化又是序列单次递增的,而这时候格雷码就起到作用了。

  • 格雷码每次都只会发生1比特数据的变化,在异步FIFO中,读写指针的变化可以使用二进制加,然后将变化后的二进制通过组合逻辑转换为格雷码即可。
  • 二进制转格雷码的方法为:

  代码描述为:assign gray_value = binary_value ^ (binary_value>>1)

  • 格雷码转二进制码的方法:

  观察到格雷码与二进制码的最高位一样所以可以利用这一个已经确定的二进制码最高位开始做文章,逐步往其他位推之后的二进制码便是它本身的高一位与该位的格雷码进行异或

  代码描述为:

//假设位宽是N,最高位二进制码和格雷码相同,直接assign 赋值
assign bin[N-1] = gray[N-1];
//其余位是高一位二进制码与自身的格雷码相异或
genvar i;
//注意这里i开始是N-2,直到最后一位0位
for(i=N-2;i>=0;i=i-1)begin:gray_2_bin//注意这里begin后要写名字
    assign bin[i] = bin[i+1]^gray[i];
end
endgenerate
//或者可以用for,不用generate for,网上查到说针对循环赋值来说两者作用是一样的,
//生成的电路也是一样的,只是多次例化要用generate-for,还有就是for循环要放到always块中
//integer i;
//always(*)begin
//    for(i=N-2;i>=0;i=i+1)begin:gray_2_bin
//        bin[i] = bin[i+1]^gray[i];
//    end
//end

格雷码作为异步FIFO中读写指针的编码方式,是无所谓读写指针从慢时钟域到快时钟域还是快时钟域到慢时钟域的。1.慢时钟域到快时钟域,格雷码只翻转一位,等价于单bit的慢到快传输,必然能采到翻转的这一位data,打两拍消除亚稳态即可 2.快时钟域到慢时钟域,格雷码只翻转一位,打两拍不一定能采到翻转的这一位data,但是也只有两种情况,要么就是没采到,保持前一个时钟的值,要么就是采到了,是现在正确的值。画个结构图分类讨论,前一个时钟周期的空满状态是什么,然后现在读写指针采样正确或者是不正确保持前一个时钟的值,就可以发现 对于读 只可能把非empty判定成empty,而不会将empty判定为非empty;同样对于写,只可能把非full判定成full,而不会将full判定为非full,这样是安全的,这也就是所谓的虚空 虚满吧。

当完成了读写指针的二进制码到格雷码转换,并且跨到另外一个时钟域后,就可以进行读写指针的格雷码形式的对比了。

  • 对空的判断:对空的判断条件是二进制完全相同,所以格雷码也就完全相同。
  • 对满的判断:对于二进制,读写指针的最高位不同,但其余数值位相同时,表示FIFO为满。二进制和格雷码最高位一致,所以读写指针格雷码最高位不同。格雷码次高位是二进制最高位跟次高位异或得到的,读写指针二进制次高位相同,所以可以得出读写指针格雷码次高位不同(a和a`分别代表读写二进制码最高位,b代表读写二进制码次高位,相同,a ^ b 和a` ^ b就代表读写指针格雷码次高位,a` ^ b = ab+a`b`表示同或,很明显次高位不同)。而其余位分别是前一位与后一位异或得到(这里格雷码第三位是二进制码第二位与二进制码第三位异或得到,读写指针二进制码从第二位开始就完全相同了),所以读写指针后续的格雷码都相同

风格更优的格雷码计数器

经典异步FIFO电路框图

以下是根据上面框图对主要五个模块进行的代码设计:

  • TOP层
Async_FIFO.v
module Async_FIFO
#(  parameter DSIZE = 8,//FIFO内数据位宽
    parameter ASIZE = 4)//FIFO地址宽度
(
    input   [DSIZE-1:0] wdata   ,//输入数据线
    input   winc                ,//写使能
    input   wclk                ,//写时钟
    input   wrst_n              ,//写复位

    input   rinc                ,//读使能
    input   rclk                ,//读时钟
    input   rrst_n              ,//读复位

    output  [DSIZE-1:0] rdata   ,//输出数据线
    output  wfull               ,//队满信号
    output  rempty               //队空信号
);

wire    [ASIZE-1:0]     waddr      ;//时钟域逻辑电路生成的二进制写地址
wire    [ASIZE-1:0]     raddr      ;//时钟域逻辑电路生成的二进制读地址
wire    [ASIZE  :0]     wptr       ;//本地写指针
wire    [ASIZE  :0]     rptr       ;//本地读指针
wire    [ASIZE  :0]     wq2_rptr   ;//同步写指针
wire    [ASIZE  :0]     rq2_wptr   ;//同步读指针

fifomem #(DSIZE,ASIZE) fifomem_inst(
    .wclk           (wclk       ),
    .wclken         (winc       ),
    .wfull          (wfull      ),
    .waddr          (waddr      ),
    .wdata          (wdata      ),
    .raddr          (raddr      ),

    .rdata          (rdata      )  
);

wptr_full #(ASIZE) wptr_full_inst(
    .wclk           (wclk       ),
    .winc           (winc       ),
    .wrst_n         (wrst_n     ),
    .wq2_rptr       (wq2_rptr   ),
    
    .wptr           (wptr       ),
    .waddr          (waddr      ),
    .wfull          (wfull      )
);

rptr_empty #(ASIZE) rptr_empty_inst(
    .rclk           (rclk       ),
    .rinc           (rinc       ),
    .rrst_n         (rrst_n     ),
    .rq2_wptr       (rq2_wptr   ),
    
    .rptr           (rptr       ),
    .raddr          (raddr      ),
    .rempty         (rempty     )
);

sync_r2w sync_r2w_inst(
    .wclk           (wclk       ),
    .wrst_n         (wrst_n     ),
    .rptr           (rptr       ),    

    .wq2_rptr       (wq2_rptr   ) 
);

sync_w2r sync_w2r_inst(
    .rclk           (rclk       ),
    .rrst_n         (rrst_n     ),
    .wptr           (wptr       ),

    .rq2_wptr       (rq2_wptr   ) 
);

endmodule
  • 写指针同步到读时钟域
sync_w2r.v
 module sync_w2r
#(parameter ADDRSIZE = 4)
(
    
    input                      rclk        ,//读指针
    input                      rrst_n      ,//读复位
    input       [ADDRSIZE:0]   wptr        ,//n+1位格雷码写指针
    output reg  [ADDRSIZE:0]   rq2_wptr     //n+1位同步写指针
);

reg         [ADDRSIZE:0]       rq1_wptr;//二级同步器第一级输出

always @(posedge rclk or negedge rrst_n) begin
    if(!rrst_n)
        {rq2_wptr,rq1_wptr} <= 0;
    else
        {rq2_wptr,rq1_wptr} <= {rq1_wptr,wptr};
end
endmodule
  • 读指针同步到写时钟域
sync_r2w.v
module sync_r2w
#(parameter ADDRSIZE = 4)
(
    
    input                      wclk        ,//写指针
    input                      wrst_n      ,//写复位
    input       [ADDRSIZE:0]   rptr        ,//n+1位格雷码读指针
    output reg  [ADDRSIZE:0]   wq2_rptr     //n+1位同步读指针
);

reg         [ADDRSIZE:0]       wq1_rptr;//二级同步器第一级输出

always @(posedge wclk or negedge wrst_n) begin
    if(!wrst_n)
        {wq2_rptr,wq1_rptr} <= 0;
    else
        {wq2_rptr,wq1_rptr} <= {wq1_rptr,rptr};
end
endmodule
  • 读指针、读地址、empty信号生成模块
rptr_empty.v
 module rptr_empty
#(
    parameter ADDRSIZE = 4
)
(
    input                           rclk        ,//读时钟
    input                           rrst_n      ,//读复位
    input                           rinc        ,//读使能
    input          [ADDRSIZE:0]     rq2_wptr    ,//同步到读时钟域下的写指针

    output  reg    [ADDRSIZE:0]     rptr        ,//读指针格雷码
    output         [ADDRSIZE-1:0]   raddr       ,//二进制读地址
    output  reg                     rempty       //队空
);

reg     [ADDRSIZE:0]    rbin;//读指针二进制码
wire    [ADDRSIZE:0]    rbinnext ;//读指针二进制码递增
wire    [ADDRSIZE:0]    rgraynext;//读指针格雷码

assign raddr = rbin[ADDRSIZE-1:0];//n+1位二进制码后n位可直接用来寻址
assign rbinnext = rbin + (rinc & ~rempty);//读使能且非队空时,读指针二进制码可递增
assign rgraynext = (rbinnext>>1) ^ rbinnext;//读指针二进制码转格雷码

always @(posedge rclk or negedge rrst_n) begin
    if (!rrst_n)
        {rbin,rptr} <= 0;
    else
        {rbin,rptr} <= {rbinnext,rgraynext};
end

assign  rempty_val = (rgraynext == rq2_wptr);//这里的1bit信号rempty_val没有申明就直接使用了,网上查说可以!编译器会自动推导 定义成一根wire

always @(posedge rclk or negedge rrst_n) begin
    if (!rrst_n)
        rempty <= 0;
    else
        rempty <= rempty_val;
end

endmodule
  • 写指针、写地址、full信号生成模块
wptr_full.v
 module wptr_full
#(
    parameter ADDRSIZE = 4
)
(
    input                           wclk        ,//写时钟
    input                           wrst_n      ,//写复位
    input                           winc        ,//写使能
    input          [ADDRSIZE:0]     wq2_rptr    ,//同步到写时钟域下的读指针

    output  reg    [ADDRSIZE:0]     wptr        ,//写指针格雷码
    output         [ADDRSIZE-1:0]   waddr       ,//二进制写地址
    output  reg                     wfull        //队满
);

reg     [ADDRSIZE:0]    wbin;//写指针二进制码
wire    [ADDRSIZE:0]    wbinnext;//写指针二进制码递增
wire    [ADDRSIZE:0]    wgraynext;//写指针格雷码

assign waddr = wbin[ADDRSIZE-1:0];//n+1位二进制码后n位可直接用来寻址
assign wbinnext = wbin + (winc & ~wfull);//写使能且非队满时,写指针二进制码可递增
assign wgraynext = (wbinnext>>1) ^ wbinnext;//写指针二进制码转格雷码

always @(posedge wclk or negedge wrst_n) begin
    if (!wrst_n)
        {wbin,wptr} <= 0;
    else
        {wbin,wptr} <= {wbinnext,wgraynext};
end
//格雷码高两位不同  其余位相同 是队空的判定条件
assign  wfull_val = (wgraynext == {~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]});//这里的1bit信号wfull_val没有申明就直接使用了,网上查说可以!编译器会自动推导 定义成一根wire

always @(posedge wclk or negedge wrst_n) begin
    if (!wrst_n)
        wfull <= 0;
    else
        wfull <= wfull_val;
end

endmodule
  • 伪双口RAM Memory 写入、读出数据,如果在ASIC设计中是没有RAM可用的,因此只能使用寄存器来模拟伪双口ram。寄存器模拟单口ram后,单口ram只有一组地址线和一个时钟,读写同步,但是让右边多路选择器中的读地址作为选择信号,读地址的更新由读指针控制,就可以实现伪双口ram。

fifomem.v
module fifomem
#(  parameter DATASIZE = 8,
    parameter ADDRSIZE = 4)
(
    input                           wclk   ,
    input                           wclken ,
    input                           wfull  ,
    input   [ADDRSIZE-1:0]          waddr  ,
    input   [DATASIZE-1:0]          wdata  ,
    input   [ADDRSIZE-1:0]          raddr  ,

    output  [DATASIZE-1:0]          rdata   
);
`ifdef VENDORRAM//如果定义了该参数,直接使用RAM IP
    vendor_ram mem(
        .clk      (wclk      ),
        .din      (wdata     ),
        .waddr    (waddr     ),
        .wclken   (wclken    ),
        .wclken_n (wfull     ),

        .dout     (rdata     )
    );
`else
    localparam DEPTH = 1 << ADDRSIZE;//FIFO深度为2^4
    reg [DATASIZE-1:0] mem [0:DEPTH-1];
    assign rdata = mem[raddr];//多路选择器直接实现数据读出
    always @(posedge wclk) begin
        if(wclken && !wfull)
            mem[waddr] <= wdata;//使能有效且未写满时则写入
    end
`endif

endmodule
  • 测试文件
tb_Async_FIFO.v
 `include "fifomem.v"
`include "wptr_full.v"
`include "rptr_empty.v"
`include "sync_r2w.v"
`include "sync_w2r.v"
`include "Async_FIFO.v"
`timescale 1ns/1ns
module tb_Async_FIFO;

parameter DSIZE = 8 ;
parameter ASIZE = 4 ;

reg   [DSIZE-1:0]   wdata               ;//输入数据线
reg                 winc                ;//写使能
reg                 wclk                ;//写时钟
reg                 wrst_n              ;//写复位
reg                 rinc                ;//读使能
reg                 rclk                ;//读时钟
reg                 rrst_n              ;//读复位

wire  [DSIZE-1:0]   rdata               ;//输出数据线
wire                wfull               ;//队满信号
wire                rempty              ;//队空信号

initial begin
    {wclk,rclk} = 2'b0;
    wrst_n <= 1'b0;
    rrst_n <= 1'b0;
    #50
    wrst_n <= 1'b1;
    #50
    rrst_n <= 1'b1;  
end

initial begin
    winc  <= 1'b0;
    rinc  <= 1'b0;
    wdata <= 8'b0; 
end
//case1 读时钟比写时钟快
always #12.3 wclk = ~wclk;
always #8.6  rclk = ~rclk;
//异步FIFO验证case:1.连续写入n个数据,再读出n个数据 非空    2.写入n个数据,读出n个数据,读空 empty拉高  3.读出n个数据,写入n个数据,写满 full拉高
initial begin
    #200
    //连续写入9个数据,再读出5个数据 此时队列还剩4个
    send_wr_begin(9);
    send_wr_end;
    send_rd_begin(5);
    send_rd_end;
    //再读出4个数据 此时队列还剩0个,空
    send_rd_begin(4);
    send_rd_end;
    //连续写入12个数据,再读出14个数据 看此时波形如何
    send_wr_begin(12);
    send_wr_end;
    send_rd_begin(14);
    send_rd_end;
    //连续写入15个数据,再读出10个数据 此时队列还剩5个
    send_wr_begin(15);
    send_wr_end;
    send_rd_begin(10);
    send_rd_end;
    //连续写入11个数据,此时队列还剩16个  满
    send_wr_begin(11);
    send_wr_end;
    send_rd_begin(10);
    send_rd_end;
    //读出10个数据,此时还剩6个,再写入12个,看此时波形如何
    send_rd_begin(10);
    send_rd_end;
    send_wr_begin(12);
    send_wr_end;
    #200
    $finish;
end
//读写乱序,连续读 连续写  读写交织

//发送 写指令及数据
task send_wr_begin(
    input [7:0] times
);
begin
    repeat(times)
    @(posedge wclk)begin
        winc  <= 1'b1;
        wdata <= {$random}%256;
    end
end
endtask
//结束写指令及数据
task send_wr_end;
begin
    @(posedge wclk)begin
        winc  <= 1'b0;
        wdata <= 8'b0;
    end
    repeat(2)@(posedge wclk);
end
endtask

//发送 读指令
task send_rd_begin(
    input [7:0] times
);
begin
    repeat(times)
    @(posedge rclk)begin
        rinc  <= 1'b1;
    end
end
endtask
//结束读指令
task send_rd_end;
begin
    @(posedge rclk)begin
        rinc  <= 1'b0;
    end
    repeat(2)@(posedge rclk);
end
endtask

initial begin
    $dumpfile("Async_FIFO.vcd");
    $dumpvars();
end
Async_FIFO #( 
   DSIZE,//FIFO内数据位宽
   ASIZE //FIFO地址宽度
)
Async_FIFO_inst
(
    .wdata      (wdata     ),//输出数据线
    .winc       (winc      ),//写使能
    .wclk       (wclk      ),//写时钟
    .wrst_n     (wrst_n    ),//写复位
    .rinc       (rinc      ),//读使能
    .rclk       (rclk      ),//读时钟
    .rrst_n     (rrst_n    ),//读复位

    .rdata      (rdata     ),//输出数据线
    .wfull      (wfull     ),//队满信号
    .rempty     (rempty    ) //队空信号
);

endmodule

 

本篇参考了该博主的该篇文章:

[1].https://zhuanlan.zhihu.com/p/599914121 

该博主该篇文章以及许多异步FIFO设计参考了国外一篇经典的paper:

[2].Clifford E. Cummings, Simulation and Synthesis Techniques for Asynchronous FIFO Design, 2002

以下也是对异步FIFO做介绍的几篇优秀文章:

[3].https://www.zhihu.com/search?type=content&q=%E5%BC%82%E6%AD%A5FIFO

[4].https://zhuanlan.zhihu.com/p/545512508

[5].https://zhuanlan.zhihu.com/p/404660796

[6].https://zhuanlan.zhihu.com/p/472912568

posted @ 2023-06-09 18:36  million_yh  阅读(479)  评论(0)    收藏  举报