爱吃汽的小橘头像
关注

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

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/orange_life/article/details/166829241

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--