news 2026/10/1 12:00:04

异步FIFO核心揭秘:格雷码与指针同步

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
异步FIFO核心揭秘:格雷码与指针同步

异步 FIFO 用来在两个不同、互不相关的时钟域之间安全传输数据。

比如:

写时钟域 读时钟域 clk_wr = 100 MHz clk_rd = 65 MHz 数据 ──> [ 写控制 ] ──> RAM ──> [ 读控制 ] ──> 数据 ↑ ↑ wr_clk rd_clk

两个时钟可能频率不同,也可能相位完全没关系,所以不能直接把多位数据从一个时钟域硬塞给另一个时钟域。

假设 FPGA 里面:

模块 A:100 MHz 模块 B:65 MHz

这就有 CDC 问题。

因为data_a可能恰好在clk_65m的采样边沿附近变化,导致:

建立时间违例 保持时间违例 亚稳态

而且如果是:

data_a[13:0]

这样的多位数据,甚至可能出现:

原来:01111111 目标:10000000 实际读到:00001111

也就是各个位并不是同一时刻稳定下来。

异步 FIFO 的作用就是把:

写数据 ↓ 写时钟域 → FIFO → 读时钟域 ↓ 读数据

这个跨时钟域过程处理安全。

异步 FIFO 的组成

一个标准异步 FIFO 大致是:

Async FIFO 写时钟域 读时钟域 wr_clk rd_clk │ │ ▼ ▼ wr_en ──> 写地址 读地址 <── rd_en │ │ ▼ ▼ ┌──────────────────────────────┐ data_in │ Dual Port RAM │ data_out ───────>│ │─────────> └──────────────────────────────┘ ▲ ▲ │ │ Write Pointer Read Pointer │ │ Binary Binary │ │ Gray Gray │ │ └──── CDC ───────────┘

你可以把异步 FIFO 拆成 5 个核心部分:

  1. 双口 RAM
  2. 写指针wptr
  3. 读指针rptr
  4. Gray Code 跨时钟同步
  5. full / empty判断

这里真正难的不是 RAM。

真正难的是指针跨时钟域。

写指针和读指针

假设 FIFO 深度:

DEPTH = 8

地址范围:

0 ~ 7

写指针负责:

写一次 wptr = wptr + 1

读指针一样:

rptr = rptr + 1

为什么指针不能直接跨时钟域?

这就是你前面问过的:

为什么异步 FIFO 使用格雷码?

举个最经典的例子。

二进制计数:

0111 ↓ 1000

理论上是一次变化。

但是实际上有4 bit 同时翻转:

0 → 1 1 → 0 1 → 0 1 → 0

由于 FPGA 内部布线延时不完全一样,另一个时钟域可能看到:

0111 0011 0001 1001 1000

中间状态。

于是对方可能错误判断 FIFO:

空了? 满了? 还有几个数据?

这就危险了。

Gray Code 为什么能解决?

Gray Code 最大特点:

相邻两个数只有 1 bit 发生变化。

例如 3 位 Gray Code:

BinaryGray
000000
001001
010011
011010
100110
101111
110101
111100

例如:

Binary: 011 → 100

变化很多位。

而对应 Gray:

010 → 110

只变化:

bit2

因此跨时钟域的时候,就算恰好采在变化边沿附近,最多也就是:

旧 Gray 或者 新 Gray

不会组合出来一个距离特别远的错误值。

所以异步 FIFO 通常是:

Binary Pointer │ ▼ Gray Pointer │ ▼ 2级寄存器同步 │ ▼ 另一个时钟域

Gray Code 怎么计算?

这个公式你最好记下来,面试非常常见:

gray = binary ^ (binary >> 1);

例如:

Binary = 0110

右移:

0110 0011 ---- XOR 0101

所以:

0110 Binary ↓ 0101 Gray

Verilog 一般这样:

gray_next = bin_next ^ (bin_next >> 1);

为什么还需要“两级同步器”?

即使使用 Gray Code,也仍然存在一个问题:

那唯一变化的 bit 也可能产生亚稳态。

所以:

Gray Pointer ↓ DFF ↓ DFF ↓ 同步完成

例如把读指针同步到写时钟域:

always @(posedge wr_clk or negedge rst_n) begin if(!rst_n) begin rptr_gray_sync1 <= 0; rptr_gray_sync2 <= 0; end else begin rptr_gray_sync1 <= rptr_gray; rptr_gray_sync2 <= rptr_gray_sync1; end end

结构:

读时钟域 写时钟域 rptr_gray ────────────> DFF ──> DFF ↑ ↑ wr_clk wr_clk

第一级寄存器可能亚稳。

第二级给它一个完整时钟周期恢复。

数据本身为什么不用 Gray Code?

这里特别容易误解。

异步 FIFO 里:

数据

并不是直接跨 CDC 同步器。

而是写入:

Dual Port RAM

例如:

wr_clk ↓ 写 RAM[address]

另一个时钟域:

rd_clk ↓ 读 RAM[address]

因此:

数据 → RAM → 数据

而真正需要跨时钟域的是:

写指针 读指针

所以结构实际上是:

数据 ┌──────── RAM ─────────┐ ─────────>│ │─────────> └──────────────────────┘ wptr ─Gray──> rd_clk rptr ─Gray──> wr_clk

empty 怎么判断?

这个比较简单。

在读时钟域:

如果:

读指针 == 已同步过来的写指针

说明:

没有未读数据

所以:

EMPTY

即:

rptr_gray_next == wptr_gray_sync

可以理解成:

写到 5 读到 5

那么:

所有写入的数据都已经读完

FIFO 空。

full 怎么判断?

这个稍微复杂。

因为 FIFO 是环形结构。

假设深度 8:

地址: 0 1 2 3 4 5 6 7 然后又回到 0

如果只比较:

write_addr == read_addr

你根本分不清:

FIFO 空

还是:

FIFO 满

所以指针通常比 RAM 地址多 1 bit。

例如深度:

8 = 2³

RAM 地址是:

3 bit

但 Pointer:

4 bit

即:

Pointer = {wrap_bit, address}

例如:

0000 0001 ... 0111 1000 1001 ...

最高位相当于记录:

我已经绕 FIFO 一圈了。

Binary 情况下怎么理解 full?

假设:

rptr = 0_010

写指针如果到了:

wptr = 1_010

地址一样:

010

但 MSB 不一样。

表示:

写指针比读指针领先整整一圈

所以:

FIFO FULL

直观上:

rptr ↓ [2][3][4][5][6][7][0][1] ↑ wptr 已经绕了一圈

为什么 Gray Code 判断 full 要反转两位?

这是异步 FIFO 里最经典、也是面试最喜欢问的一点。

比如:

wgray_next == {~rgray_sync[ADDRSIZE:ADDRSIZE-1], rgray_sync[ADDRSIZE-2:0]}

很多人第一次看:

为什么是最高两位取反?

原因是:

Binary 指针相差:

FIFO_DEPTH

对应到 Gray Code 之后,并不是简单:

最高位取反

而是 Gray 编码特性导致:

最高两位发生反转

所以 full 的常见判断就是:

assign full_val = wgray_next == {~rgray_sync[PTR_WIDTH-1:PTR_WIDTH-2], rgray_sync[PTR_WIDTH-3:0]};

这个公式建议记住。

一个完整的数据过程

假设:

写时钟 = 100 MHz 读时钟 = 65 MHz FIFO = 8 deep

初始:

wptr = 0 rptr = 0

所以:

EMPTY = 1 FULL = 0

现在写入:

A B C

写指针:

0 → 1 → 2 → 3

RAM:

addr 0 = A addr 1 = B addr 2 = C

此时:

wptr = 3 rptr = 0

读端开始读。

第一次:

读 addr 0 → A rptr = 1

第二次:

读 addr 1 → B rptr = 2

第三次:

读 addr 2 → C rptr = 3

最终:

wptr == rptr

于是:

EMPTY = 1

为什么写快读慢也没关系?

例如:

写:100 MHz 读:65 MHz

短时间内没关系。

因为 FIFO 就是一个:

数据蓄水池

可以想象:

水龙头 100 MHz ↓ ┌─────────────┐ │ │ │ FIFO │ │ │ └─────────────┘ ↓ 65 MHz 排水

但如果:

长期写入速率 > 长期读取速率

FIFO 最终一定:

FULL

所以 FIFO 只能吸收:

瞬时速率差

不能违反长期平均带宽。

同步 FIFO 和异步 FIFO 区别

项目同步 FIFO异步 FIFO
写时钟clkwr_clk
读时钟clkrd_clk
时钟关系相同可以完全无关
Gray Code通常不需要基本需要
CDC无有
指针同步不需要需要
难度较低较高

所以:

同一个 clk ↓ 同步 FIFO

而:

clk_A ≠ clk_B ↓ 异步 FIFO

下面Verilog 异步 FIFO RTL。它包含:

  • 双口存储
  • 写指针 / 读指针
  • Binary → Gray
  • Gray 指针跨时钟域两级同步
  • full
  • empty
module async_fifo #( parameter DATA_WIDTH = 8, parameter ADDR_WIDTH = 4 )( input wire wr_clk, input wire rd_clk, input wire rst_n, input wire wr_en, input wire [DATA_WIDTH-1:0] wr_data, output wire full, input wire rd_en, output reg [DATA_WIDTH-1:0] rd_data, output wire empty ); // FIFO 深度 = 2^ADDR_WIDTH localparam DEPTH = (1 << ADDR_WIDTH); // ----------------------------- // 1. FIFO 存储空间 // ----------------------------- reg [DATA_WIDTH-1:0] mem [0:DEPTH-1]; // 指针需要比地址多 1 bit reg [ADDR_WIDTH:0] wr_bin; reg [ADDR_WIDTH:0] wr_gray; reg [ADDR_WIDTH:0] rd_bin; reg [ADDR_WIDTH:0] rd_gray; wire [ADDR_WIDTH:0] wr_bin_next; wire [ADDR_WIDTH:0] wr_gray_next; wire [ADDR_WIDTH:0] rd_bin_next; wire [ADDR_WIDTH:0] rd_gray_next; // ----------------------------- // 2. 跨时钟同步后的 Gray 指针 // ----------------------------- // 读指针同步到写时钟域 reg [ADDR_WIDTH:0] rd_gray_sync1; reg [ADDR_WIDTH:0] rd_gray_sync2; // 写指针同步到读时钟域 reg [ADDR_WIDTH:0] wr_gray_sync1; reg [ADDR_WIDTH:0] wr_gray_sync2; // ===================================================== // 写时钟域 // ===================================================== // ----------------------------- // 3. 计算下一拍写二进制指针 // ----------------------------- assign wr_bin_next = wr_bin + ((wr_en && !full) ? 1'b1 : 1'b0); // Binary -> Gray assign wr_gray_next = (wr_bin_next >> 1) ^ wr_bin_next; // ----------------------------- // 4. 写数据 // ----------------------------- always @(posedge wr_clk or negedge rst_n) begin if (!rst_n) begin wr_bin <= 0; wr_gray <= 0; end else begin if (wr_en && !full) begin mem[wr_bin[ADDR_WIDTH-1:0]] <= wr_data; end wr_bin <= wr_bin_next; wr_gray <= wr_gray_next; end end // ----------------------------- // 5. 读指针同步到写时钟域 // ----------------------------- always @(posedge wr_clk or negedge rst_n) begin if (!rst_n) begin rd_gray_sync1 <= 0; rd_gray_sync2 <= 0; end else begin rd_gray_sync1 <= rd_gray; rd_gray_sync2 <= rd_gray_sync1; end end // ----------------------------- // 6. Full 判断 // ----------------------------- assign full = (wr_gray_next == {~rd_gray_sync2[ADDR_WIDTH:ADDR_WIDTH-1], rd_gray_sync2[ADDR_WIDTH-2:0]}); // ===================================================== // 读时钟域 // ===================================================== // ----------------------------- // 7. 计算下一拍读二进制指针 // ----------------------------- assign rd_bin_next = rd_bin + ((rd_en && !empty) ? 1'b1 : 1'b0); // Binary -> Gray assign rd_gray_next = (rd_bin_next >> 1) ^ rd_bin_next; // ----------------------------- // 8. 读取数据 // ----------------------------- always @(posedge rd_clk or negedge rst_n) begin if (!rst_n) begin rd_bin <= 0; rd_gray <= 0; rd_data <= 0; end else begin if (rd_en && !empty) begin rd_data <= mem[rd_bin[ADDR_WIDTH-1:0]]; end rd_bin <= rd_bin_next; rd_gray <= rd_gray_next; end end // ----------------------------- // 9. 写指针同步到读时钟域 // ----------------------------- always @(posedge rd_clk or negedge rst_n) begin if (!rst_n) begin wr_gray_sync1 <= 0; wr_gray_sync2 <= 0; end else begin wr_gray_sync1 <= wr_gray; wr_gray_sync2 <= wr_gray_sync1; end end // ----------------------------- // 10. Empty 判断 // ----------------------------- assign empty = (rd_gray_next == wr_gray_sync2); endmodule
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 11:59:48

免费API实战:文本读写与在线通知,让脚本自动存取和推送

做开发这几年&#xff0c;我越来越觉得手边应该备几个“小工具API”——不是那种大而全的云服务&#xff0c;而是够简单、够便宜的轻量接口。今天这篇就聊两类我几乎天天用到的免费API&#xff1a;一类负责文本读写&#xff0c;帮你把一段话或一个JSON对象临时扔到云端&#xf…

作者头像 李华
网站建设 2026/10/1 11:59:23

Linux无线网卡AP模式:hostapd+dnsmasq+NAT配置实战

手里有块开发板、一台装了 Linux 的旧笔记本&#xff0c;或者一台只有网口没有无线模块的工控机&#xff0c;临时要给几台设备组个无线局域网&#xff0c;路由器又不在手边——这种场景我碰过太多次。把 Linux 主机上的无线网卡从 station&#xff08;客户端&#xff09;模式切…

作者头像 李华
网站建设 2026/10/1 11:59:23

AI编程半年后,我发现自己不会写需求了:结构化提示词实战

1. 从“AI 写代码”到“我写不出需求”这件事说起用了半年 AI 编程工具之后&#xff0c;我最大的感受不是“AI 太强了”&#xff0c;而是“我怎么连话都说不清楚了”。这个结论听起来有点反直觉&#xff0c;但如果你真的把 AI 编程助手当成日常主力工具用过一段时间&#xff0c…

作者头像 李华
网站建设 2026/10/1 11:58:41

完全背包问题详解:从状态转移方程推导到正序枚举实现

1. 从“背方程”到“推方程”&#xff1a;完全背包问题的出发点和收益 很多人在学动态规划时都有过这样的阶段&#xff1a;0-1背包刚搞明白&#xff0c;二维数组、逆序枚举、滚动数组都还会写&#xff0c;结果一看到完全背包的状态转移方程就懵了。网上教程习惯直接把结论甩出来…

作者头像 李华
网站建设 2026/10/1 11:58:41

完全背包状态转移方程推导:从枚举到一维正序循环的真相

刷动态规划题的时候&#xff0c;十个新手里有八个会卡在完全背包的状态转移方程上。我自己当年也是这样&#xff1a;盯着 dp[i][j] max(dp[i-1][j], dp[i][j - v[i]] w[i]) 这行代码看了半天&#xff0c;死活想不明白为什么第二项的下标从 i-1 变成了 i &#xff0c;更…

作者头像 李华
网站建设 2026/10/1 11:58:08

WPS加载项 imageMso 图标指南:ID验证、跨版本兜底与故障排查

做 WPS 加载项或者自定义功能区的人&#xff0c;迟早会撞上imageMso这个属性名。它不复杂&#xff0c;就是一个写在 ribbon.xml 里、用来引用宿主内置图标库的字符串属性&#xff0c;但真上手就会发现坑不少&#xff1a;抄来的 ID 在你机器上有图、在同事机器上就是一块空白&am…

作者头像 李华