K7DJ

FPGA异步FIFO:多时钟域设计的核心(含代码示例与案例分析)

7 0 0 0

FPGA异步FIFO:多时钟域设计的核心

什么是异步FIFO?

为什么需要异步FIFO?

异步FIFO的内部结构

格雷码(Gray Code)

异步FIFO的工作原理

异步FIFO的参数选择

常见的错误使用场景分析

代码示例 (Verilog)

实际案例分析

总结

FPGA异步FIFO:多时钟域设计的核心

在FPGA设计中,跨时钟域数据传输是家常便饭。你肯定遇到过这样的场景:一个模块工作在100MHz时钟下,另一个模块工作在150MHz时钟下,它们之间需要交换数据。直接把数据线连起来?那可不行,亚稳态会让你怀疑人生。这时候,异步FIFO(First-In, First-Out)就闪亮登场了。

什么是异步FIFO?

简单来说,异步FIFO就是一个先进先出的数据缓冲区,但它有一个特别的本事:写入和读取可以使用不同的时钟。 想象一下,它就像一个水库,一边进水(写入数据),一边放水(读取数据),两边的水流速度(时钟频率)可以不一样,但水库都能很好地进行调节,保证水流的稳定。

为什么需要异步FIFO?

在多时钟域设计中,异步FIFO主要解决以下几个问题:

  1. 数据同步: 将数据从一个时钟域安全地传递到另一个时钟域,避免亚稳态。
  2. 速率匹配: 当写入和读取时钟频率不同时,FIFO可以作为缓冲区,平衡两侧的数据速率。
  3. 数据位宽转换: 写入和读取的数据位宽可以不同,FIFO可以进行位宽转换(当然,这需要额外的逻辑控制)。

异步FIFO的内部结构

一个典型的异步FIFO通常包含以下几个部分:

  • 存储单元(RAM): 用于存储数据。通常使用双端口RAM实现,一个端口用于写入,另一个端口用于读取。
  • 写指针(Write Pointer): 指示下一个数据应该写入到RAM的哪个地址。
  • 读指针(Read Pointer): 指示下一个应该从RAM的哪个地址读取数据。
  • 写时钟域逻辑: 包括写地址生成、写满标志(Write Full)生成等。
  • 读时钟域逻辑: 包括读地址生成、读空标志(Read Empty)生成等。
  • 跨时钟域同步逻辑: 用于将写指针同步到读时钟域,将读指针同步到写时钟域。这是异步FIFO设计的关键,也是最容易出错的地方。

格雷码(Gray Code)

在跨时钟域同步指针时,通常会使用格雷码。这是因为格雷码有一个重要的特性:相邻两个码字之间只有一位不同。 这样,在进行跨时钟域同步时,即使采样发生偏差,最多只会采样到相邻的格雷码,从而避免了指针跳变导致的数据错误。 如果你对格雷码还不熟悉,可以去找一些相关的资料学习一下,这玩意儿在FPGA设计中经常用到。

异步FIFO的工作原理

  1. 写入数据: 当写使能信号有效,且FIFO未满时,数据被写入到写指针指向的RAM地址,然后写指针递增。
  2. 读取数据: 当读使能信号有效,且FIFO非空时,从读指针指向的RAM地址读取数据,然后读指针递增。
  3. 指针同步: 写指针在写时钟域下递增,然后被同步到读时钟域;读指针在读时钟域下递增,然后被同步到写时钟域。
  4. 空满标志: 写时钟域根据同步过来的读指针和自身的写指针,判断FIFO是否写满;读时钟域根据同步过来的写指针和自身的读指针,判断FIFO是否读空。

异步FIFO的参数选择

设计异步FIFO时,需要根据实际应用场景选择合适的参数。主要有两个参数:

  • 深度(Depth): FIFO可以存储的数据量。深度太小,容易写满,导致数据丢失;深度太大,浪费资源。一般来说,FIFO的深度应该大于等于最大突发数据量(Burst Length)。具体的计算方法可以参考一些专业的资料或者工具,这里就不展开讲了。 给你一个经验公式作为参考吧: FIFO深度 = (最大突发数据量 * (写时钟频率 / 读时钟频率 - 1)) / 2,但这只是一个粗略的估计,具体情况还需要具体分析。
  • 位宽(Width): FIFO一次可以写入/读取的数据位数。位宽应该与应用中需要传输的数据位宽一致。

常见的错误使用场景分析

在实际应用中,很多人会错误地使用异步FIFO,导致各种奇怪的问题。下面列举几个常见的错误:

  1. 直接将二进制计数器作为指针: 这样做在跨时钟域同步时,会导致指针多位同时跳变,造成数据错误。正确的做法是使用格雷码计数器。
  2. 空满标志判断错误: 异步FIFO的空满标志判断比较复杂,需要仔细考虑各种边界情况。常见的错误包括:
    • 没有考虑指针回环(Wrap Around)的情况。
    • 在判断空满标志时,没有使用同步后的指针。
    • 空满标志产生不及时,导致数据丢失或读取错误。
  3. 复位处理不当: 异步FIFO的复位也需要进行跨时钟域同步,否则会导致FIFO状态混乱。常见的错误包括:
    • 没有对读写指针分别进行复位。
    • 复位信号没有进行跨时钟域同步。
  4. 使用不合适的同步策略: 除了格雷码同步,还有其他一些同步策略,例如握手信号同步、DMUX同步等。不同的同步策略适用于不同的场景,选择不合适的同步策略会导致性能下降或者功能错误。
  5. 忽略时序约束: 即使正确实现了异步FIFO的逻辑,如果没有进行正确的时序约束,也可能会导致亚稳态问题。 你需要对跨时钟域路径进行时序约束,例如设置 set_false_path 或者 set_max_delay 等。

代码示例 (Verilog)

下面是一个简单的异步FIFO的Verilog代码示例,仅供参考。这个例子只展示了基本的功能,实际应用中可能需要根据具体需求进行修改。

module async_fifo #(
    parameter DATA_WIDTH = 8,
    parameter DEPTH_BITS = 4 // FIFO深度为2^DEPTH_BITS
) (
    input  wire                  clk_wr,
    input  wire                  rst_wr,
    input  wire                  wr_en,
    input  wire [DATA_WIDTH-1:0] data_in,
    output wire                  full,

    input  wire                  clk_rd,
    input  wire                  rst_rd,
    input  wire                  rd_en,
    output wire [DATA_WIDTH-1:0] data_out,
    output wire                  empty
);

    localparam DEPTH = 1 << DEPTH_BITS;

    // 存储单元
    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];

    // 写指针
    reg [DEPTH_BITS:0] wr_ptr_bin;
    wire [DEPTH_BITS:0] wr_ptr_gray;

    // 读指针
    reg [DEPTH_BITS:0] rd_ptr_bin;
    wire [DEPTH_BITS:0] rd_ptr_gray;

    // 写时钟域同步到读时钟域的写指针
    reg [DEPTH_BITS:0] wr_ptr_gray_sync1;
    reg [DEPTH_BITS:0] wr_ptr_gray_sync2;

    // 读时钟域同步到写时钟域的读指针
    reg [DEPTH_BITS:0] rd_ptr_gray_sync1;
    reg [DEPTH_BITS:0] rd_ptr_gray_sync2;

    // 格雷码转换
    assign wr_ptr_gray = wr_ptr_bin ^ (wr_ptr_bin >> 1);
    assign rd_ptr_gray = rd_ptr_bin ^ (rd_ptr_bin >> 1);

    // 写指针递增
    always @(posedge clk_wr or posedge rst_wr) begin
        if (rst_wr) begin
            wr_ptr_bin <= 0;
        end else if (wr_en && !full) begin
            wr_ptr_bin <= wr_ptr_bin + 1;
        end
    end

    // 读指针递增
    always @(posedge clk_rd or posedge rst_rd) begin
        if (rst_rd) begin
            rd_ptr_bin <= 0;
        end else if (rd_en && !empty) begin
            rd_ptr_bin <= rd_ptr_bin + 1;
        end
    end

    // 写指针同步到读时钟域
    always @(posedge clk_rd or posedge rst_rd) begin
        if (rst_rd) begin
            wr_ptr_gray_sync1 <= 0;
            wr_ptr_gray_sync2 <= 0;
        end else begin
            wr_ptr_gray_sync1 <= wr_ptr_gray;
            wr_ptr_gray_sync2 <= wr_ptr_gray_sync1;
        end
    end

    // 读指针同步到写时钟域
    always @(posedge clk_wr or posedge rst_wr) begin
        if (rst_wr) begin
            rd_ptr_gray_sync1 <= 0;
            rd_ptr_gray_sync2 <= 0;
        end else begin
            rd_ptr_gray_sync1 <= rd_ptr_gray;
            rd_ptr_gray_sync2 <= rd_ptr_gray_sync1;
        end
    end

    // 写满标志
    assign full = (wr_ptr_gray == {~rd_ptr_gray_sync2[DEPTH_BITS], rd_ptr_gray_sync2[DEPTH_BITS-1:0]});

    // 读空标志
    assign empty = (rd_ptr_gray == wr_ptr_gray_sync2);

    // 写入数据
    always @(posedge clk_wr) begin
        if (wr_en && !full) begin
            mem[wr_ptr_bin[DEPTH_BITS-1:0]] <= data_in;
        end
    end

    // 读取数据
    assign data_out = mem[rd_ptr_bin[DEPTH_BITS-1:0]];

endmodule

代码解读:

  • DEPTH_BITS 参数决定了FIFO的深度(2^DEPTH_BITS)。
  • 使用双端口RAM mem 作为存储单元。
  • 分别使用 wr_ptr_binrd_ptr_bin 作为写指针和读指针的二进制计数器。
  • 使用 wr_ptr_grayrd_ptr_gray 作为写指针和读指针的格雷码计数器。
  • 使用两级寄存器 (wr_ptr_gray_sync1, wr_ptr_gray_sync2rd_ptr_gray_sync1, rd_ptr_gray_sync2) 进行跨时钟域同步。
  • 根据同步后的指针和自身的指针判断空满标志。
  • 在写时钟域下写入数据,在读时钟域下读取数据。

实际案例分析

假设我们要设计一个视频处理系统,其中一个模块负责从摄像头采集图像数据,工作在50MHz时钟下;另一个模块负责对图像数据进行处理,工作在75MHz时钟下。两个模块之间需要通过异步FIFO传输图像数据。

设计步骤:

  1. 确定FIFO参数:
    • 假设摄像头输出的图像数据位宽为24位(RGB888),则FIFO的位宽也应该设置为24位。
    • 假设每帧图像大小为640x480像素,最大突发数据量为640 * 24 = 15360 bits。根据经验公式,FIFO深度可以设置为 (15360 * (75/50 - 1)) /2 = 3840。为了留有余量,可以将FIFO深度设置为4096(即DEPTH_BITS = 12)。
  2. 编写FIFO代码: 可以参考上面的代码示例,根据实际需求进行修改。
  3. 仿真验证: 使用仿真工具(例如ModelSim)对FIFO进行仿真验证,确保FIFO功能正确。
  4. 时序约束: 对跨时钟域路径进行时序约束,确保FIFO的时序性能。
  5. 上板测试: 将设计下载到FPGA开发板上进行测试,验证FIFO在实际硬件环境下的工作情况。

总结

异步FIFO是FPGA多时钟域设计中不可或缺的重要组成部分。掌握异步FIFO的设计原理和使用方法,可以帮助你解决跨时钟域数据传输的难题。希望这篇文章能够帮助你更好地理解异步FIFO,并在你的FPGA设计中发挥作用。记住,多看代码,多做实验,多思考,才能真正掌握这项技术。 别忘了,实践出真知!

Apple

Comment

打赏赞助
sponsor

感谢您的支持让我们更好的前行