异步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:

Binary Gray
000 000
001 001
010 011
011 010
100 110
101 111
110 101
111 100

例如:

复制代码
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
写时钟 clk wr_clk
读时钟 clk rd_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

相关推荐
璞致电子2 小时前
基于 P201Pro | 接收端基础知识——Costas环锁定后为何仍会错?DQPSK与前导码相位校正(6)
经验分享·fpga开发·ad9361·软件无线电·sdr·qpsk·pzsdr
璞致电子20 小时前
基于 AD9361 (PZSDR‑P201Pro) 软件无线电实战综述:从板卡选型、硬件平台升级到 GNU Radio QPSK 完整收发链路
经验分享·嵌入式硬件·fpga开发·软件无线电·sdr·璞致电子
炎龙铠甲会合体1 天前
从 SPEC 到上板:在 Zynq-7020 上用 RTL 手写一个 YOLO 卷积加速器(完整教程)
yolo·fpga开发·axi·芯片验证
FPGA小徐1 天前
Vivado 按键消抖与复位模块代码修复详解:KEY_TOP、key、rst_gen 完整实战
fpga开发
炎龙铠甲会合体1 天前
AXI2ICACHE— AXI4-Full 四路组相联指令 Cache 设计与 UVM 验证
fpga开发·面试·芯片验证
bingcheby1 天前
FMQL15/20/45 (复旦微青龙系列 PSOC)新手使用说明
fpga开发
漫漫长远1 天前
低功耗UPF介绍(一)
笔记·嵌入式硬件·学习·fpga开发
自小吃多2 天前
【Verilog/FPGA】存储器(数组)
嵌入式硬件·fpga开发
szxinmai主板定制专家2 天前
RK3588+FPGA异构架构|高速数据采集+边缘AI落地应用全解析
人工智能·嵌入式硬件·fpga开发·架构·zynq