当前位置: 首页 > news >正文

深入讲讲异步FIFO

一、异步 FIFO 的基本概念

1.1 定义与核心作用

        异步 FIFO(Asynchronous FIFO)是一种读写时钟完全独立的先进先出(First-In-First-Out)数据缓冲器,主要用于跨时钟域数据传输场景。在数字系统中,当两个模块工作在不同时钟频率或相位下时,异步 FIFO 可作为数据中转站,解决数据传输中的时序冲突、速率不匹配问题,避免数据丢失或错误。

1.2 与同步 FIFO 的核心区别

二、异步 FIFO 的基本结构

        异步 FIFO 的核心结构由 5 部分组成:

存储单元(RAM);空满检测逻辑(解决跨时钟域问题);写指针(写时钟域);读指针(读时钟域);读写接口(时钟,数据,使能)

2.1 存储单元(Storage Element)

存储单元是 FIFO 的数据缓冲区,在 FPGA 中通常采用两种实现方式:

  • 块 RAM(Block RAM):适用于深度较大的 FIFO(如深度 > 64),资源利用率高,速度快(FPGA 内置的硬核 RAM)。
  • 分布式 RAM(Distributed RAM):适用于小深度 FIFO(如深度≤32),由 LUT(查找表)拼接而成,灵活性高但资源消耗大。

存储单元的核心参数:

  • 数据位宽(DATA_WIDTH):单次传输的数据宽度(如 8/16/32 位)。
  • 深度(DEPTH):可存储的数据个数(通常为 2ⁿ,便于地址编码)。

2.2 指针(Pointers)

指针用于跟踪 FIFO 的读写位置,分为写指针和读指针:

  • 写指针(Write Pointer):在写时钟(wr_clk)驱动下,指示下一个待写入数据的地址;每完成一次写入(wr_en 有效且非满),指针 + 1。
  • 读指针(Read Pointer):在读时钟(rd_clk)驱动下,指示下一个待读出数据的地址;每完成一次读出(rd_en 有效且非空),指针 + 1。

指针的关键特性
指针采用n+1 位二进制编码(n 为地址位宽),其中低 n 位为实际存储地址,最高位(第 n 位)用于区分 “空” 和 “满” 状态(指示指针是否多循环一圈)。例如:深度为 4(2²)的 FIFO,地址位宽 2 位,指针为 3 位(bit2-bit0)

2.3 空满检测逻辑(Empty/Full Logic)

空满状态是 FIFO 的核心控制信号:

  • 空信号(empty):读时钟域输出,指示 FIFO 无数据可读(防止空读错误)。
  • 满信号(full):写时钟域输出,指示 FIFO 无空间可写(防止满写错误)。

核心挑战:读写指针属于不同时钟域,直接跨域比较会导致亚稳态(Metastability),需通过特殊设计实现准确判断。

三、异步 FIFO 的核心技术难点与解决方案

3.1 跨时钟域同步与亚稳态问题

3.1.1 亚稳态产生原因

当一个时钟域的信号(如读指针)直接进入另一个时钟域(如写时钟域)时,若信号在目标时钟的建立时间(Setup Time) 或保持时间(Hold Time) 窗口内变化,触发器会进入亚稳态(输出不稳定),导致后续逻辑错误。

关于亚稳态数据的跳变:

当输入数据在时钟沿附近的 ([T{su}, Th]) 窗口内发生变化(即数据跳变时间落在时钟沿前 (T{su}) 到时钟沿后 (Th) 的区间内),触发器的输出无法稳定到明确的 0 或 1,而是处于介于高电平与低电平之间的不稳定状态,这种状态称为亚稳态。亚稳态的持续时间是随机的,最终会随机稳定到 0 或 1,但在此期间输出信号是不确定的,可能导致后续逻辑错误。

3.1.2 两级同步器解决方案

为解决亚稳态,异步 FIFO 中采用两级 D 触发器同步器(Two-Stage Synchronizer)将指针从一个时钟域同步到另一个时钟域

  • 一级 DFF:捕获输入信号,可能进入亚稳态,但亚稳态持续时间会随时间衰减。
  • 二级 DFF:在一级 DFF 稳定后采样,将亚稳态概率降低到可接受范围(FPGA 中通常可忽略)。

3.2 格雷码(Gray Code)的应用

3.2.1 为什么需要格雷码?

二进制指针在跨域同步时存在 “多位跳变” 问题(如二进制011100跳变 3 位),同步器可能采样到中间错误状态。而格雷码的相邻数值仅一位不同,可最大限度降低同步错误概率。

3.2.2 二进制与格雷码的转换
  • 二进制转格雷码gray_code = binary_code ^ (binary_code >> 1)
    例:4 位二进制0101→格雷码0101 ^ 0010 = 0111
  • 格雷码转二进制binary_code[MSB] = gray_code[MSB]binary_code[i] = binary_code[i+1] ^ gray_code[i](从高位到低位)。
3.2.3 指针编码规则

异步 FIFO 中,读写指针的二进制值先转换为格雷码,再通过两级同步器跨时钟域传输,确保同步后的值接近真实值。

3.3 空满状态的准确判断

3.3.1 空状态(Empty)判断

空状态定义:读指针追上写指针,FIFO 中无数据。
判断逻辑(读时钟域):
读指针格雷码等于同步到读时钟域的写指针格雷码时,FIFO 为空:
empty = (rd_gray == sync_wr_gray_to_rd_clk)

3.3.2 满状态(Full)判断

满状态定义:写指针比读指针多绕 FIFO 一圈(即多走一个深度),FIFO 中数据已满。
判断逻辑(写时钟域):
写指针格雷码的低 n 位等于同步到写时钟域的读指针格雷码的低 n 位,且最高位相反时,FIFO 为满:
full = (wr_gray == {~sync_rd_gray_to_wr_clk[MSB], sync_rd_gray_to_wr_clk[MSB-1:0]})

示例(深度 = 4,指针 3 位):

  • 写指针二进制 = 4(100)→格雷码 = 110;读指针二进制 = 0(000)→格雷码 = 000。
  • 同步到写时钟域的读指针格雷码为 000,满条件:110 == {~000[2], 000[1:0]} → 110 == 100? 不,修正:满条件应为写指针格雷码与同步读指针格雷码的 “高两位相反,低 n 位相同”。
    正确例:写指针格雷码110(100),同步读指针格雷码010(010)→高两位1 vs 0,低两位10 vs 10→满状态。

3.4 类比FIFO工作过程

给异步 FIFO 起个生活化的名字:“跨节奏仓库”

想象你家楼下有个小仓库,专门用来临时放东西。这个仓库有 4 个格子(编号 0、1、2、3),就像 FIFO 的存储空间。

仓库有两个 “操作员”:

  • 写小哥:负责往仓库里存东西(写操作),他干活的节奏是 “每 2 秒存一个”(写时钟,比如 2Hz)。
  • 读小哥:负责从仓库里取东西(读操作),他干活的节奏是 “每 3 秒取一个”(读时钟,比如 3Hz)。

两人节奏不一样(异步),但要同时干活,还不能出问题:不能把仓库塞满了还硬塞(溢出),也不能仓库空了还硬取(读空)。

核心工具:两个 “位置箭头” 和 “同步小纸条”

为了让两人配合好,他们各有一个 “箭头”(指针):

  • 写箭头:写小哥每次存完东西,箭头就往前挪一格,标记 “下一个要存东西的位置”。
  • 读箭头:读小哥每次取完东西,箭头就往前挪一格,标记 “下一个要取东西的位置”。

但两人节奏不一样,互相看不到对方的箭头,所以需要 “同步小纸条”:

写小哥会把自己的箭头位置写在纸条上,传给读小哥(写指针同步到读时钟域),让读小哥知道 “仓库里现在有多少东西可以取”。

  • 读小哥也会把自己的箭头位置写在纸条上,传给写小哥(读指针同步到写时钟域),让写小哥知道 “仓库还有多少空位置可以存”。

具体过程:读写同时进行的一天

初始状态:仓库是空的,写箭头和读箭头都指着 “0 号格子”(刚开始都从 0 开始)。

第 1 秒:写小哥先干活

写小哥带了个 “苹果”,看了看自己的箭头(指着 0),就把苹果放进 0 号格子。存完后,写箭头往前挪一格,指向 1 号格子。
此时仓库里有:0 号格子(苹果)。
写小哥把 “写箭头现在在 1” 写在纸条上,传给读小哥(同步写指针到读端)。

第 3 秒:读小哥第一次干活(读时钟到了)

读小哥收到纸条,看到写箭头在 1,自己的读箭头还在 0。他知道:“写箭头在 1,我在 0,说明 0 号格子有东西可以取!”
于是他取走 0 号格子的苹果,取完后,读箭头往前挪一格,指向 1 号格子。
此时仓库里:空了 0 号格子,还剩(暂时没新东西)。
读小哥把 “读箭头现在在 1” 写在纸条上,传给写小哥(同步读指针到写端)。

第 4 秒:写小哥第二次干活(写时钟到了)

写小哥收到纸条,看到读箭头在 1,自己的写箭头在 1。他知道:“我的箭头和读箭头没重合,仓库没满,可以存!”
于是把 “香蕉” 放进 1 号格子,存完后写箭头挪到 2 号格子。
仓库里现在有:1 号格子(香蕉)。

第 6 秒:写小哥第三次干活

写小哥又存 “橙子” 到 2 号格子,写箭头挪到 3 号格子。
仓库里:1 号(香蕉)、2 号(橙子)。

第 6 秒:读小哥第二次干活(刚好和写小哥同时)

此时读小哥的读时钟也到了(和写小哥存橙子的动作同时发生)。
读小哥看自己的读箭头在 1,同步过来的写箭头在 3(写小哥刚挪到 3)。他知道:“1 号格子有香蕉可以取!”
于是取走 1 号格子的香蕉,读箭头挪到 2 号格子。
仓库里现在剩:2 号格子(橙子)。

第 8 秒:写小哥第四次干活

写小哥存 “葡萄” 到 3 号格子,写箭头本来要挪到 4 号,但仓库只有 4 个格子(0-3),所以绕回 0 号格子(箭头变成 0)。
仓库里现在有:2 号(橙子)、3 号(葡萄)。
写小哥把 “箭头在 0” 同步给读小哥。

第 9 秒:读小哥第三次干活

读小哥看到写箭头同步后是 0,自己的读箭头在 2。他知道:“2 号格子有橙子,取!”
取走橙子,读箭头挪到 3 号格子。
仓库里剩:3 号格子(葡萄)。

第 10 秒:写小哥第五次干活(和读小哥可能有重叠)

写小哥想存 “西瓜”,看自己的箭头在 0,同步过来的读箭头在 3(读小哥刚挪到 3)。
他判断:“我的下一个要存的位置是 0,读箭头在 3,没重合,仓库没满!” 于是把西瓜存到 0 号格子,写箭头挪到 1 号。
仓库里现在有:3 号(葡萄)、0 号(西瓜)。

第 12 秒:读小哥第四次干活

读小哥取走 3 号格子的葡萄,读箭头挪到 0 号格子。
仓库里剩:0 号格子(西瓜)。

就这样,写小哥每 2 秒存一个,读小哥每 3 秒取一个,动作可能重叠(同时进行),但通过 “箭头标记位置” 和 “同步纸条”,写小哥永远知道仓库有没有空位置(不会存满溢出),读小哥永远知道有没有东西可以取(不会取空出错)。

关键总结

  • 异步 FIFO 就像这个 “跨节奏仓库”,写和读可以按自己的节奏同时干。
  • 指针(箭头)标记当前存 / 取的位置,同步后互相 “看一眼” 对方的位置。
  • 空满判断:读箭头追上写箭头(没东西了)就停读;写箭头快追上读箭头(仓库快满了)就停写。
  • 这样即使节奏不同、动作同时发生,数据也不会丢、不会乱~

同步指针的速度(同步操作)和存放东西的速度(读写操作)完全没关系,两者各走各的节奏,互不影响快慢。

四、FPGA中FIFO的应用

 一。FIFO中的FPGA 中主要用于:

  • 跨时钟域数据传输(异步 FIFO)
  • 速率匹配(例如高速发送端→低速接收端)
  • 数据缓冲(突发数据暂存)

核心参数:

  • 深度:可存储的数据个数(如深度 8 表示存 8 个数据)
  • 宽度:每个数据的位宽(如宽度 32bit 表示每个数据 32 位)
  • 空满标志:控制读写操作的关键信号

二、Verilog 代码实现(异步 FIFO)

下面是一个深度为 8、位宽为 8 的异步 FIFO 的完整 Verilog 代码:

module async_fifo #(parameter DATA_WIDTH = 8,   // 数据位宽parameter ADDR_WIDTH = 3    // 地址位宽(2^3=8深度)
)(// 写时钟域input  wire wclk,           // 写时钟input  wire wrst_n,         // 写复位(低有效)input  wire w_en,           // 写使能input  wire [DATA_WIDTH-1:0] w_data,  // 写入数据// 读时钟域input  wire rclk,           // 读时钟input  wire rrst_n,         // 读复位(低有效)input  wire r_en,           // 读使能output reg [DATA_WIDTH-1:0] r_data,   // 读出数据// 状态标志output wire full,           // 满标志output wire empty           // 空标志
);// 内部信号定义reg [ADDR_WIDTH:0] w_ptr_bin;    // 写指针(二进制)reg [ADDR_WIDTH:0] r_ptr_bin;    // 读指针(二进制)reg [ADDR_WIDTH:0] w_ptr_gray;   // 写指针(格雷码)reg [ADDR_WIDTH:0] r_ptr_gray;   // 读指针(格雷码)// 跨时钟域同步的指针reg [ADDR_WIDTH:0] w_ptr_gray_sync1, w_ptr_gray_sync2;reg [ADDR_WIDTH:0] r_ptr_gray_sync1, r_ptr_gray_sync2;// 存储器阵列reg [DATA_WIDTH-1:0] fifo_mem [0:(1<<ADDR_WIDTH)-1];// 二进制转格雷码function [ADDR_WIDTH:0] bin_to_gray(input [ADDR_WIDTH:0] bin);bin_to_gray = bin ^ (bin >> 1);endfunction// 格雷码转二进制function [ADDR_WIDTH:0] gray_to_bin(input [ADDR_WIDTH:0] gray);integer i;reg [ADDR_WIDTH:0] bin;beginbin = gray;for (i = 1; i <= ADDR_WIDTH; i = i + 1)bin = bin ^ (gray >> i);endgray_to_bin = bin;endfunction// 写操作always @(posedge wclk or negedge wrst_n) beginif (!wrst_n) beginw_ptr_bin <= 'b0;w_ptr_gray <= 'b0;endelse if (w_en && !full) beginfifo_mem[w_ptr_bin[ADDR_WIDTH-1:0]] <= w_data;  // 写入数据w_ptr_bin <= w_ptr_bin + 1'b1;                  // 写指针递增w_ptr_gray <= bin_to_gray(w_ptr_bin);           // 更新格雷码指针endend// 读操作always @(posedge rclk or negedge rrst_n) beginif (!rrst_n) beginr_ptr_bin <= 'b0;r_ptr_gray <= 'b0;r_data <= 'b0;endelse if (r_en && !empty) beginr_data <= fifo_mem[r_ptr_bin[ADDR_WIDTH-1:0]];  // 读出数据r_ptr_bin <= r_ptr_bin + 1'b1;                  // 读指针递增r_ptr_gray <= bin_to_gray(r_ptr_bin);           // 更新格雷码指针endend// 两级同步器 - 写指针同步到读时钟域always @(posedge rclk or negedge rrst_n) beginif (!rrst_n) beginw_ptr_gray_sync1 <= 'b0;w_ptr_gray_sync2 <= 'b0;endelse beginw_ptr_gray_sync1 <= w_ptr_gray;w_ptr_gray_sync2 <= w_ptr_gray_sync1;endend// 两级同步器 - 读指针同步到写时钟域always @(posedge wclk or negedge wrst_n) beginif (!wrst_n) beginr_ptr_gray_sync1 <= 'b0;r_ptr_gray_sync2 <= 'b0;endelse beginr_ptr_gray_sync1 <= r_ptr_gray;r_ptr_gray_sync2 <= r_ptr_gray_sync1;endend// 空标志判断(在读时钟域)assign empty = (r_ptr_gray == w_ptr_gray_sync2);// 满标志判断(在写时钟域)assign full = (w_ptr_gray == {~r_ptr_gray_sync2[ADDR_WIDTH:ADDR_WIDTH-1], r_ptr_gray_sync2[ADDR_WIDTH-2:0]});endmodule

三、原语实现

FPGA 厂商提供了专用的存储器原语,例如 Xilinx 的BRAM原语,使用原语可以更高效地实现 FIFO:

module fifo_bram_primitive (input  wire wclk, wrst_n, w_en,input  wire [7:0] w_data,input  wire rclk, rrst_n, r_en,output reg [7:0] r_data,output wire full, empty
);// 地址和计数器reg [2:0] w_addr, r_addr;reg [3:0] count;  // 深度8需要4位计数器// 生成满空标志assign full  = (count == 4'd8);assign empty = (count == 4'd0);// 实例化BRAM原语RAMB16_S8_S8 #(.WRITE_WIDTH_A(8),  // 写端口宽度.WRITE_WIDTH_B(8),  // 读端口宽度.SRVAL_A(8'h00),    // 复位值.SRVAL_B(8'h00)) bram_inst (.CLK_A(wclk),        // 写时钟.EN_A(w_en & !full), // 写使能.WE_A(1'b1),         // 写使能.ADDR_A(w_addr),     // 写地址.DI_A(w_data),       // 写入数据.DO_A(),             // 写端口输出(不用).CLK_B(rclk),        // 读时钟.EN_B(r_en & !empty), // 读使能.WE_B(1'b0),         // 读端口禁止写.ADDR_B(r_addr),     // 读地址.DI_B(8'h00),        // 读端口数据输入(不用).DO_B(r_data)        // 读出数据);// 写地址控制always @(posedge wclk or negedge wrst_n) beginif (!wrst_n)w_addr <= 3'd0;else if (w_en & !full)w_addr <= w_addr + 1'b1;end// 读地址控制always @(posedge rclk or negedge rrst_n) beginif (!rrst_n)r_addr <= 3'd0;else if (r_en & !empty)r_addr <= r_addr + 1'b1;end// 计数器控制(使用写时钟)always @(posedge wclk or negedge wrst_n) beginif (!wrst_n)count <= 4'd0;else begincase ({w_en & !full, r_en & !empty})2'b00: count <= count;      // 无读写2'b01: count <= count - 1;  // 只读2'b10: count <= count + 1;  // 只写2'b11: count <= count;      // 同时读写,计数器不变endcaseendendendmodule

四、IP 核应用

大多数 FPGA 厂商提供了 FIFO 的 IP 核,使用 IP 核可以快速配置和实例化 FIFO

http://www.lryc.cn/news/607504.html

相关文章:

  • 向华为学习——IPD流程体系之IPD术语
  • Java函数式编程之【Stream终止操作】【下】【三】【收集操作collect()与分组分区】【下游收集器】
  • 从零开始:Python编程快速入门指南
  • 实战指南:如何将Git仓库中的特定文件夹及其历史完整迁移到另一个仓库
  • vue+element 实现下拉框共享options
  • 智能客服系统实战:多轮对话与知识库检索完整实现
  • 《n8n基础教学》第三节:模拟一个自动化场景
  • Android使用MediaProjectionManager获取游戏画面和投屏
  • C语言-字符串(定义)、字符串函数(strlen、strcat、strcpy、strcmp、strlwr、strupr)
  • 【string类常见接口】
  • Linux系统编程Day3-- Linux常用操作(续)
  • 基于深度学习的医学图像分析:使用Autoencoder实现医学图像去噪
  • Flask 路由系统:URL 到 Python 函数的映射
  • Coze Studio概览(五)--工作流管理
  • 20250801在Ubuntu24.04.2LTS下编译firefly_itx_3588j的Android12时解决boot.img过大的问题
  • 【lucene】FastVectorHighlighter案例
  • 基于线性规划的储能充放电仿真系统
  • Android Frameworks从零开始
  • JSON 对象在浏览器中顺序与后端接口返回不一致的问题
  • 好未来披露2026财年Q1财报:净利润3128万美元,同比大增174%
  • Day 28:类的定义和方法
  • Git 命令使用指南:从入门到进阶
  • MySQL CPU占用过高排查指南
  • 数据处理四件套:NumPy/Pandas/Matplotlib/Seaborn速通指南
  • Agents-SDK智能体开发[3]之多Agent执行流程
  • SN74LVC1G08DBVR 德州仪器(TI)逻辑芯片IC 电源芯片 ESD保护
  • 智慧社区构建——2
  • C语言(02)——标准库函数大全(持续更新)
  • AI Agent 视角:可执行程序的二进制格式,是一场「结构化语言」与「智能解析」的双向奔赴
  • 一套视频快速入门并精通PostgreSQL