0%

FPGA数字信号处理之FIFO

本节主要介绍了同步FIFO与异步FIFO的工作原理与硬件设计的关键,并列举了示例代码。

FIFO概述

  • FIFO(First In First Out),是一种先进先出的数据缓存器。其不可寻址,只能顺序写入、顺序读出,数据地址由内部读写地址自动加1完成(这里注意与RAM的区分:RAM支持外部给定地址读写数据),通常硬件上实现的是循环队列

  • FIFO的功能:FIFO是速率匹配中的一个缓冲环节,一般用于连接两个速率不同的数据通路

    image-20231224020051443
  • FIFO分类

    • 同步FIFO
      • 特点:数据写入FIFO的时钟和数据读出FIFO的时钟是同步
      • 作用:作为交互数据的缓冲,相当于一个buffer
    • 异步FIFO
      • 特点:数据写入FIFO的时钟和数据读出FIFO的时钟是异步
      • 作用:实现数据在不同时钟域之间进行传递,或作为不同数据宽度的数据接口(比如写的数据是8个bit,读出的数据是16bit,或者写的是16个bit,读出的数据是8个bit)
    image-20231224020354679
  • FIFO深度与宽度

    • FIFO宽度:用fifo_data_size表示,也就是FIFO存储的每个数据宽度
    • FIFO深度:用fifo_addr_size表示,也就是能存储多少个数据
    image-20231224020607825
  • FIFO中相关信号

    • 时钟、复位:clk,rst_n_i
    • 读使能(读控制):rd_en_i
    • 写使能(写控制):wr_en_i
    • 满信号(满标志):full,当FIFO中的数据满(或接近满),不再能进行数据的写入
    • 空信号(空标志):empty,当FIFO为空(或接近空),不再能进行数据的读出

同步FIFO

1.设计关键

  • “空”和“满”信号的判断

    • 首先来看一个正常循环FIFO的空满信号判断流程:

      image-20231227105716607

    • 由上图可以总结出:

      • 当读指针追上写指针时,FIFO为空
      • 当写指针追上读指针时,FIFO为满
    • 由于FIFO空和满状态时,读写指针都指向相同位置,那么如何较好的判断究竟是空还是满呢?答案是:将地址指针扩展1bit

      • 在深度为8的FIFO中,需要3bit的读写指针来分别指示读写地址3’b000-3’b111这8个地址。若将地址指针扩展1bit,则变成4bit的地址,而地址表示区间则变成了4’b0000-4’b1111。假设不看最高位的话,后面3位的表示区间仍然是3’b000-3’b111,也就意味着最高位可以拿来作为指示位。
        image-20231227111508593
      • 由上图可以总结出:
        • 读写指针相同,FIFO为空状态
        • 读写指针最高位相反,其他位相同,FIFO为满状态

2.源代码

  • sync_fifo.v

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    module sync_fifo #(
    parameter DEPTH = 8,
    parameter WIDTH = 8
    ) (
    input wire clk,
    input wire rst_n,

    input wire i_wen,
    input wire [WIDTH-1:0] i_wdata,

    input wire i_ren,
    output reg [WIDTH-1:0] o_rdata,

    output wire o_empty,
    output wire o_full
    );

    parameter ADDR_WIDTH = $clog2(DEPTH);

    reg [WIDTH-1:0] mem[DEPTH-1:0];

    reg [ADDR_WIDTH:0] wptr, wptr_next;
    reg [ADDR_WIDTH:0] rptr, rptr_next;

    always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
    wptr <= {ADDR_WIDTH{1'b0}};
    rptr <= {ADDR_WIDTH{1'b0}};
    end else begin
    wptr <= wptr_next;
    rptr <= rptr_next;
    end
    end

    always @(*) begin
    wptr_next = wptr;
    rptr_next = rptr;
    if (i_wen && !o_full) begin
    wptr_next = wptr + 1'b1;
    end
    if (i_ren && !o_empty) begin
    rptr_next = rptr + 1'b1;
    end
    end

    integer i;
    always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
    for (i = 0; i < DEPTH; i = i + 1) begin
    mem[i] <= 0;
    end
    end else if (i_wen && !o_full) begin
    mem[wptr[ADDR_WIDTH-1:0]] <= i_wdata;
    end
    end

    always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
    o_rdata <= 0;
    end else if (i_ren && !o_empty) begin
    o_rdata <= mem[rptr[ADDR_WIDTH-1:0]];
    end
    end

    assign o_empty = wptr == rptr;
    assign o_full = wptr == {~rptr[ADDR_WIDTH], rptr[ADDR_WIDTH-1:0]};

    endmodule

3.Testbench

  • sync_fifo_tb.v

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    `timescale 1ns/1ns	//时间单位/精度

    //------------<模块及端口声明>----------------------------------------
    module sync_fifo_tb();

    parameter DATA_WIDTH = 8 ; //FIFO位宽
    parameter DATA_DEPTH = 8 ; //FIFO深度

    reg clk ;
    reg rst_n ;
    reg [DATA_WIDTH-1:0] data_in ;
    reg rd_en ;
    reg wr_en ;

    wire [DATA_WIDTH-1:0] data_out;
    wire empty ;
    wire full ;


    //------------<例化被测试模块>----------------------------------------
    sync_fifo
    #(
    .DEPTH (DATA_WIDTH), //FIFO位宽
    .WIDTH (DATA_DEPTH) //FIFO深度
    )
    sync_fifo_ptr_inst(
    .clk (clk ),
    .rst_n (rst_n ),
    .i_wdata (data_in ),
    .i_ren (rd_en ),
    .i_wen (wr_en ),

    .o_rdata (data_out ),
    .o_empty (empty ),
    .o_full (full )
    );

    //------------<设置初始测试条件>----------------------------------------
    initial begin
    clk = 1'b0; //初始时钟为0
    rst_n <= 1'b0; //初始复位
    data_in <= 'd0;
    wr_en <= 1'b0;
    rd_en <= 1'b0;
    //重复8次写操作,让FIFO写满
    repeat(8) begin
    @(negedge clk)begin
    rst_n <= 1'b1;
    wr_en <= 1'b1;
    data_in <= $random; //生成8位随机数
    end
    end
    //重复8次读操作,让FIFO读空
    repeat(8) begin
    @(negedge clk)begin
    wr_en <= 1'b0;
    rd_en <= 1'd1;
    end
    end
    //重复4次写操作,写入4个随机数据
    repeat(4) begin
    @(negedge clk)begin
    wr_en <= 1'b1;
    data_in <= $random; //生成8位随机数
    rd_en <= 1'b0;
    end
    end
    //持续同时对FIFO读写,写入数据为随机数据
    forever begin
    @(negedge clk)begin
    wr_en <= 1'b1;
    data_in <= $random; //生成8位随机数
    rd_en <= 1'b1;
    end
    end
    end
    //------------<设置时钟>----------------------------------------------
    always #10 clk = ~clk; //系统时钟周期20ns

    endmodule
    • 结果如下:

      image-20231226145844212


异步FIFO

1.设计关键

  • “空”和“满”信号的判断:

    • 异步FIFO也可以用同步FIFO的方式去判断空满状态,但是!!!,异步FIFO存在跨时钟域的问题,它存在亚阈值问题,如果仍然使用二进制编码方式的话,会大大增加出现亚阈值状态的概率

    • 首先,先解释为什么跨时钟域容易出现亚阈值问题(亚阈值问题就是数据不满足触发器的建立时间和保持时间,在时钟跳变沿时采集到了一个不稳定的状态,详情见Reference)

      image-20231227115142508

    • 二进制的7(0111)跳转到8(1000),4位都会发生变化,所以发生亚稳态的概率就比较大,那么我们如何减少发生亚稳态的概率呢?答案是:用格雷码

      • 格雷码是相邻数字之间只有1个bit的变化,那么就会大大减少亚稳态发生的概率

      • 二进制与格雷码的转化:二进制码右移一位^二进制码=格雷码(^代表异或)

      • 如何用格雷码判断空满:

        • 当最高位和次高位相同,其余位相同认为是读空

        • 当最高位和次高位不同,其余位相同认为是写满

  • 跨时钟域的同步问题:因为读指针与写指针是受不同时钟控制的,所以它们之间的比较,需要先同步到一个时钟下(这里的同步都是指使用2个(或者3个,但此类情况不多)FF(触发器)来进行同步(俗称“打两拍”),我的理解是用这两拍的时间先锁存一个指针,然后再将这个指针与经过了2拍之后的另一个指针比较)

    • “写满”的判断:需要将读指针同步到写时钟域,再与写指针判断

      • 假设本来是这样的:

        image-20231227122308994

      • 如果在2拍(写时钟下)时间内,写指针从0010写到0111,此时写指针和读指针(延时两拍后的,并非真实读指针)指向同一地址,FIFO为满

        image-20231227122645638

      • 但其实在这段时间内,可能真实读指针已经读出了数据,所以此时存在“虚满”状态

        image-20231227122919471

    • “读空”的判断:需要将写指针同步到读时钟域,再与读指针判断

      • 假设本来是这样的:

        image-20231227123629433

      • 如果在2拍(读时钟下)时间内,读指针从0010读到0111再到1000,此时写指针和读指针(延时两拍后的,并非真实写指针)指向同一地址,FIFO为空

        image-20231227123723989

      • 但其实在这段时间内,可能真实写指针已经写入了数据,所以此时存在“虚空”状态

        image-20231227124012465

2.源代码

  • async_fifo.v

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    `timescale 1ns / 1ps

    //异步FIFO
    module async_fifo
    #(
    parameter DATA_WIDTH = 'd8 , //FIFO位宽
    parameter DATA_DEPTH = 'd16 //FIFO深度
    )
    (
    //写数据
    input wr_clk , //写时钟
    input wr_rst_n , //低电平有效的写复位信号
    input wr_en , //写使能信号,高电平有效
    input [DATA_WIDTH-1:0] data_in , //写入的数据
    //读数据
    input rd_clk , //读时钟
    input rd_rst_n , //低电平有效的读复位信号
    input rd_en , //读使能信号,高电平有效
    output reg [DATA_WIDTH-1:0] data_out , //输出的数据
    //状态标志
    output empty , //空标志,高电平表示当前FIFO已被写满
    output full //满标志,高电平表示当前FIFO已被读空
    );

    //reg define
    //用二维数组实现RAM
    reg [DATA_WIDTH - 1 : 0] fifo_buffer[DATA_DEPTH - 1 : 0];

    reg [$clog2(DATA_DEPTH) : 0] wr_ptr; //写地址指针,二进制
    reg [$clog2(DATA_DEPTH) : 0] rd_ptr; //读地址指针,二进制
    reg [$clog2(DATA_DEPTH) : 0] rd_ptr_g_d1; //读指针格雷码在写时钟域下同步1拍
    reg [$clog2(DATA_DEPTH) : 0] rd_ptr_g_d2; //读指针格雷码在写时钟域下同步2拍
    reg [$clog2(DATA_DEPTH) : 0] wr_ptr_g_d1; //写指针格雷码在读时钟域下同步1拍
    reg [$clog2(DATA_DEPTH) : 0] wr_ptr_g_d2; //写指针格雷码在读时钟域下同步2拍

    //wire define
    wire [$clog2(DATA_DEPTH) : 0] wr_ptr_g; //写地址指针,格雷码
    wire [$clog2(DATA_DEPTH) : 0] rd_ptr_g; //读地址指针,格雷码
    wire [$clog2(DATA_DEPTH) - 1 : 0] wr_ptr_true; //真实写地址指针,作为写ram的地址
    wire [$clog2(DATA_DEPTH) - 1 : 0] rd_ptr_true; //真实读地址指针,作为读ram的地址

    //地址指针从二进制转换成格雷码
    assign wr_ptr_g = wr_ptr ^ (wr_ptr >> 1);
    assign rd_ptr_g = rd_ptr ^ (rd_ptr >> 1);
    //读写RAM地址赋值
    assign wr_ptr_true = wr_ptr [$clog2(DATA_DEPTH) - 1 : 0]; //写RAM地址等于写指针的低DATA_DEPTH位(去除最高位)
    assign rd_ptr_true = rd_ptr [$clog2(DATA_DEPTH) - 1 : 0]; //读RAM地址等于读指针的低DATA_DEPTH位(去除最高位)


    //写操作,更新写地址
    always @ (posedge wr_clk or negedge wr_rst_n) begin
    if (!wr_rst_n)
    wr_ptr <= 0;
    else if (!full && wr_en)begin //写使能有效且非满
    wr_ptr <= wr_ptr + 1'd1;
    fifo_buffer[wr_ptr_true] <= data_in;
    end
    end
    //将读指针的格雷码同步到写时钟域,来判断是否写满
    always @ (posedge wr_clk or negedge wr_rst_n) begin
    if (!wr_rst_n)begin
    rd_ptr_g_d1 <= 0; //寄存1拍
    rd_ptr_g_d2 <= 0; //寄存2拍
    end
    else begin
    rd_ptr_g_d1 <= rd_ptr_g; //寄存1拍
    rd_ptr_g_d2 <= rd_ptr_g_d1; //寄存2拍
    end
    end
    //读操作,更新读地址
    always @ (posedge rd_clk or negedge rd_rst_n) begin
    if (!rd_rst_n)
    rd_ptr <= 'd0;
    else if (rd_en && !empty)begin //读使能有效且非空
    data_out <= fifo_buffer[rd_ptr_true];
    rd_ptr <= rd_ptr + 1'd1;
    end
    end
    //将写指针的格雷码同步到读时钟域,来判断是否读空
    always @ (posedge rd_clk or negedge rd_rst_n) begin
    if (!rd_rst_n)begin
    wr_ptr_g_d1 <= 0; //寄存1拍
    wr_ptr_g_d2 <= 0; //寄存2拍
    end
    else begin
    wr_ptr_g_d1 <= wr_ptr_g; //寄存1拍
    wr_ptr_g_d2 <= wr_ptr_g_d1; //寄存2拍
    end
    end
    //更新指示信号
    //当所有位相等时,读指针追到到了写指针,FIFO被读空
    assign empty = ( wr_ptr_g_d2 == rd_ptr_g ) ? 1'b1 : 1'b0;
    //当高位相反且其他位相等时,写指针超过读指针一圈,FIFO被写满
    //同步后的读指针格雷码高两位取反,再拼接上余下位
    assign full = ( wr_ptr_g == { ~(rd_ptr_g_d2[$clog2(DATA_DEPTH) : $clog2(DATA_DEPTH) - 1])
    ,rd_ptr_g_d2[$clog2(DATA_DEPTH) - 2 : 0]})? 1'b1 : 1'b0;

    endmodule

3.Testbench

  • async_fifo_tb.v

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    `timescale 1ns / 1ns

    module async_fifo_tb;
    parameter DATA_WIDTH = 8 ; //FIFO位宽
    parameter DATA_DEPTH = 8 ; //FIFO深度

    reg wr_clk ; //写时钟
    reg wr_rst_n ; //低电平有效的写复位信号
    reg wr_en ; //写使能信号,高电平有效
    reg [DATA_WIDTH-1:0] data_in ; //写入的数据

    reg rd_clk ; //读时钟
    reg rd_rst_n ; //低电平有效的读复位信号
    reg rd_en ; //读使能信号,高电平有效
    wire[DATA_WIDTH-1:0] data_out ; //输出的数据
    wire empty ; //空标志,高电平表示当前FIFO已被写满
    wire full ; //满标志,高电平表示当前FIFO已被读空

    //------------<例化被测试模块>----------------------------------------
    async_fifo
    #(
    .DATA_WIDTH (DATA_WIDTH), //FIFO位宽
    .DATA_DEPTH (DATA_DEPTH) //FIFO深度
    )
    async_fifo_inst(
    .wr_clk (wr_clk ),
    .wr_rst_n (wr_rst_n ),
    .wr_en (wr_en ),
    .data_in (data_in ),
    .rd_clk (rd_clk ),
    .rd_rst_n (rd_rst_n ),
    .rd_en (rd_en ),
    .data_out (data_out ),

    .empty (empty ),
    .full (full )
    );

    //------------<设置初始测试条件>----------------------------------------
    initial begin
    rd_clk = 1'b0; //初始时钟为0
    wr_clk = 1'b0; //初始时钟为0
    wr_rst_n <= 1'b0; //初始复位
    rd_rst_n <= 1'b0; //初始复位
    wr_en <= 1'b0;
    rd_en <= 1'b0;
    data_in <= 'd0;
    #5
    wr_rst_n <= 1'b1;
    rd_rst_n <= 1'b1;
    //重复8次写操作,让FIFO写满
    repeat(8) begin
    @(negedge wr_clk)begin
    wr_en <= 1'b1;
    data_in <= $random; //生成8位随机数
    end
    end
    //拉低写使能
    @(negedge wr_clk) wr_en <= 1'b0;

    //重复8次读操作,让FIFO读空
    repeat(8) begin
    @(negedge rd_clk)rd_en <= 1'd1;
    end
    //拉低读使能
    @(negedge rd_clk)rd_en <= 1'd0;
    //重复4次写操作,写入4个随机数据
    repeat(4) begin
    @(negedge wr_clk)begin
    wr_en <= 1'b1;
    data_in <= $random; //生成8位随机数
    end
    end
    //持续同时对FIFO读
    @(negedge rd_clk)rd_en <= 1'b1;
    //持续同时对FIFO写,写入数据为随机数据
    forever begin
    @(negedge wr_clk)begin
    wr_en <= 1'b1;
    data_in <= $random; //生成8位随机数
    end
    end

    end

    //------------<设置时钟>----------------------------------------------
    always #10 rd_clk = ~rd_clk; //读时钟周期20ns
    always #20 wr_clk = ~wr_clk; //写时钟周期40ns

    endmodule
    • 结果如下:

      • 整体结果:

        image-20231226163350937

      • 细节展示:

        image-20231226163304427


Reference

欢迎来到ssy的世界