FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
登录
首页-技术文章/快讯-技术分享-正文

跨时钟域同步:2026年异步FIFO深度计算与格雷码实践

FPGA小白FPGA小白
技术分享
2小时前
0
0
2

Quick Start

  • 1. 安装 Vivado 2024.2(或更高版本),确保支持所选器件(如 XC7K325T)。
  • 2. 新建工程,选择 Verilog 作为默认语言,添加异步 FIFO RTL 源文件(本文提供代码)。
  • 3. 编写顶层模块,例化异步 FIFO,连接写时钟(wr_clk, 100 MHz)和读时钟(rd_clk, 50 MHz)。
  • 4. 添加约束文件(.xdc),定义两个时钟域并设置 false path 或 set_clock_groups。
  • 5. 运行综合(Synthesis),检查无关键警告(特别是 CDC 相关)。
  • 6. 运行实现(Implementation),查看时序报告,确认无 setup/hold 违例。
  • 7. 编写 testbench,仿真验证写满/读空标志正确,数据无丢失。
  • 8. 上板测试(如使用 AX7103 开发板),用 ILA 抓取内部信号,观察空满标志与数据流。

前置条件与环境

项目推荐值说明替代方案
器件/板卡Xilinx Kintex-7 XC7K325T主流中端 FPGA,资源充足Artix-7 / Zynq-7000(需调整约束)
EDA 版本Vivado 2024.2支持 CDC 分析工具Vivado 2023.1+ / Quartus Prime Pro 23+
仿真器Vivado Simulator 或 ModelSim SE-64 2024支持 VCD 波形导出Questa / VCS
时钟/复位写时钟 100 MHz,读时钟 50 MHz,异步复位(低有效)典型跨时钟域场景其他频率组合(需重新计算深度)
接口依赖无外部 IP,纯 RTL 实现便于移植可使用 Xilinx FIFO Generator IP(但本文为 RTL 实践)
约束文件必须设置时钟分组(set_clock_groups -asynchronous)避免 CDC 路径误约束使用 set_false_path 逐条指定

目标与验收标准

  • 功能点:实现一个参数化异步 FIFO,支持任意写/读时钟比,格雷码指针同步,无亚稳态传播。
  • 性能指标:Fmax 不低于 200 MHz(写时钟域),资源占用 ≤ 200 LUT + 200 FF + 1 BRAM(深度 16,数据位宽 8)。
  • 验收方式:仿真验证写满(full)和读空(empty)标志在边界条件下正确;上板用 ILA 捕获连续写入 1000 个数据后读出,数据完整无丢失。

实施步骤

1. 工程结构与参数定义

  • 创建源文件:async_fifo.v(顶层)、ptr_handler.v(指针与空满逻辑)、gray_counter.v(格雷码计数器)、sync_2ff.v(双级同步器)。
  • 定义参数:DATA_WIDTH=8, FIFO_DEPTH=16(地址位宽 ADDR_WIDTH=4,实际深度 2^4=16)。
  • 深度计算公式:若写时钟频率 f_wr,读时钟频率 f_rd,最大写数据率 R_wr,读数据率 R_rd,则最小深度 D_min = (R_wr - R_rd) * (同步延迟 + 1)。对于连续写、突发读场景,常用 D = 2 * (f_wr / f_rd) * 突发长度(近似)。本示例中 f_wr=100 MHz, f_rd=50 MHz,突发长度 16,深度 16 足够。

2. 关键模块实现:格雷码计数器

module gray_counter #(
    parameter WIDTH = 4
) (
    input  wire             clk,
    input  wire             rst_n,
    input  wire             inc,
    output reg  [WIDTH-1:0] gray_out
);

    reg [WIDTH-1:0] binary;
    wire [WIDTH-1:0] next_binary;
    wire [WIDTH-1:0] next_gray;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            binary <= 0;
            gray_out <= 0;
        end else if (inc) begin
            binary <= binary + 1;
            gray_out <= next_gray;
        end
    end

    assign next_binary = binary + 1;
    assign next_gray   = next_binary ^ (next_binary >> 1);

endmodule

逐行说明

  • 第 1 行:模块声明,参数 WIDTH 默认为 4,用于地址位宽。
  • 第 2-7 行:端口定义,clk 和 rst_n 为时钟与异步复位(低有效),inc 为递增使能,gray_out 为格雷码输出。
  • 第 9-10 行:binary 为二进制内部计数器,next_binary 和 next_gray 为组合逻辑中间变量。
  • 第 12-18 行:时序逻辑,复位时清零,否则在 inc 有效时递增 binary 并更新 gray_out。
  • 第 20 行:next_binary 为 binary+1。
  • 第 21 行:格雷码转换公式:二进制右移一位后异或原值。

3. 双级同步器(2-FF Synchronizer)

module sync_2ff #(
    parameter WIDTH = 4
) (
    input  wire             clk_dst,
    input  wire             rst_n,
    input  wire [WIDTH-1:0] data_in,
    output reg  [WIDTH-1:0] data_out
);

    reg [WIDTH-1:0] sync_reg1;
    reg [WIDTH-1:0] sync_reg2;

    always @(posedge clk_dst or negedge rst_n) begin
        if (!rst_n) begin
            sync_reg1 <= 0;
            sync_reg2 <= 0;
        end else begin
            sync_reg1 <= data_in;
            sync_reg2 <= sync_reg1;
        end
    end

    assign data_out = sync_reg2;

endmodule

逐行说明

  • 第 1 行:模块声明,WIDTH 默认 4。
  • 第 2-7 行:端口定义,clk_dst 为目标时钟域时钟,data_in 为异步输入,data_out 为同步后输出。
  • 第 9-10 行:两级寄存器 sync_reg1 和 sync_reg2,用于消除亚稳态。
  • 第 12-19 行:时序逻辑,每个 clk_dst 上升沿将 data_in 打入第一级,下一周期打入第二级。
  • 第 21 行:data_out 直接取自第二级寄存器输出。

4. 指针处理与空满逻辑

module ptr_handler #(
    parameter ADDR_WIDTH = 4
) (
    input  wire                wr_clk, rd_clk,
    input  wire                rst_n,
    input  wire                wr_en, rd_en,
    output reg                 full, empty,
    output reg [ADDR_WIDTH:0]  wr_ptr, rd_ptr  // 多一位用于比较
);

    wire [ADDR_WIDTH:0] wr_gray_next, rd_gray_next;
    wire [ADDR_WIDTH:0] wr_gray_sync, rd_gray_sync;

    // 写指针格雷码计数器
    gray_counter #(.WIDTH(ADDR_WIDTH+1)) wr_gray (
        .clk(wr_clk), .rst_n(rst_n), .inc(wr_en && !full), .gray_out(wr_ptr)
    );

    // 读指针格雷码计数器
    gray_counter #(.WIDTH(ADDR_WIDTH+1)) rd_gray (
        .clk(rd_clk), .rst_n(rst_n), .inc(rd_en && !empty), .gray_out(rd_ptr)
    );

    // 同步读指针到写时钟域
    sync_2ff #(.WIDTH(ADDR_WIDTH+1)) sync_rd2wr (
        .clk_dst(wr_clk), .rst_n(rst_n), .data_in(rd_ptr), .data_out(rd_gray_sync)
    );

    // 同步写指针到读时钟域
    sync_2ff #(.WIDTH(ADDR_WIDTH+1)) sync_wr2rd (
        .clk_dst(rd_clk), .rst_n(rst_n), .data_in(wr_ptr), .data_out(wr_gray_sync)
    );

    // 空满判断(格雷码比较)
    always @(posedge wr_clk or negedge rst_n) begin
        if (!rst_n) full <= 1'b0;
        else begin
            // 写指针与同步后的读指针高两位取反后相等,且其余位相等 => 满
            if ((wr_ptr[ADDR_WIDTH] != rd_gray_sync[ADDR_WIDTH]) &&
                (wr_ptr[ADDR_WIDTH-1] != rd_gray_sync[ADDR_WIDTH-1]) &&
                (wr_ptr[ADDR_WIDTH-2:0] == rd_gray_sync[ADDR_WIDTH-2:0]))
                full <= 1'b1;
            else
                full <= 1'b0;
        end
    end

    always @(posedge rd_clk or negedge rst_n) begin
        if (!rst_n) empty <= 1'b1;
        else begin
            // 读指针与同步后的写指针完全相等 => 空
            if (rd_ptr == wr_gray_sync)
                empty <= 1'b1;
            else
                empty <= 1'b0;
        end
    end

endmodule

逐行说明

  • 第 1-2 行:模块声明,参数 ADDR_WIDTH=4,实际地址位宽。
  • 第 3-8 行:端口定义,包含两个时钟、复位、读写使能、空满标志和指针(多一位用于格雷码比较)。
  • 第 10-11 行:中间信号,用于格雷码同步。
  • 第 13-16 行:例化写指针格雷码计数器,inc 条件为 wr_en 且非满。
  • 第 18-21 行:例化读指针格雷码计数器,inc 条件为 rd_en 且非空。
  • 第 23-25 行:将读指针同步到写时钟域(双级同步器)。
  • 第 27-29 行:将写指针同步到读时钟域。
  • 第 31-43 行:写时钟域产生 full 信号。比较规则:写指针与同步读指针的高两位取反后相等,且低位相等,则满。这是因为格雷码满条件要求写指针比读指针多绕一圈(即最高位不同,次高位不同)。
  • 第 45-54 行:读时钟域产生 empty 信号。比较规则:读指针与同步写指针完全相等则空。

5. 顶层异步 FIFO

module async_fifo #(
    parameter DATA_WIDTH = 8,
    parameter FIFO_DEPTH = 16
) (
    input  wire                wr_clk, rd_clk,
    input  wire                rst_n,
    input  wire                wr_en, rd_en,
    input  wire [DATA_WIDTH-1:0] wr_data,
    output reg  [DATA_WIDTH-1:0] rd_data,
    output wire                full, empty
);

    localparam ADDR_WIDTH = $clog2(FIFO_DEPTH);  // 4

    wire [ADDR_WIDTH-1:0] wr_addr, rd_addr;
    wire [ADDR_WIDTH:0]   wr_ptr, rd_ptr;

    // 双端口 RAM
    reg [DATA_WIDTH-1:0] mem [0:FIFO_DEPTH-1];

    // 写操作
    always @(posedge wr_clk) begin
        if (wr_en && !full)
            mem[wr_addr] <= wr_data;
    end

    // 读操作
    always @(posedge rd_clk) begin
        if (rd_en && !empty)
            rd_data <= mem[rd_addr];
    end

    // 指针处理模块
    ptr_handler #(.ADDR_WIDTH(ADDR_WIDTH)) ptr_inst (
        .wr_clk(wr_clk), .rd_clk(rd_clk),
        .rst_n(rst_n),
        .wr_en(wr_en), .rd_en(rd_en),
        .full(full), .empty(empty),
        .wr_ptr(wr_ptr), .rd_ptr(rd_ptr)
    );

    assign wr_addr = wr_ptr[ADDR_WIDTH-1:0];
    assign rd_addr = rd_ptr[ADDR_WIDTH-1:0];

endmodule

逐行说明

  • 第 1-3 行:模块声明,参数 DATA_WIDTH=8, FIFO_DEPTH=16。
  • 第 4-10 行:端口定义,包含双时钟、复位、读写使能、数据输入/输出、空满标志。
  • 第 12 行:计算地址位宽,$clog2 返回以 2 为底的对数向上取整。
  • 第 14-15 行:地址线(取指针低 ADDR_WIDTH 位)和完整指针。
  • 第 17 行:声明双端口 RAM(寄存器实现或 BRAM 推断)。
  • 第 19-22 行:写操作,在 wr_clk 上升沿,若 wr_en 且非满,则写入数据。
  • 第 24-27 行:读操作,在 rd_clk 上升沿,若 rd_en 且非空,则读出数据。
  • 第 29-37 行:例化 ptr_handler 模块,连接所有信号。
  • 第 39-40 行:从完整指针中提取地址位。

6. 约束文件(.xdc)

# 时钟定义
create_clock -name wr_clk -period 10.000 [get_ports wr_clk]
create_clock -name rd_clk -period 20.000 [get_ports rd_clk]

# 异步时钟组
set_clock_groups -asynchronous -group [get_clocks wr_clk] -group [get_clocks rd_clk]

# 可选:对同步器路径设置 false path(已包含在时钟组中)
# set_false_path -from [get_clocks wr_clk] -to [get_clocks rd_clk]

逐行说明

  • 第 1-2 行:定义写时钟周期 10 ns(100 MHz)和读时钟周期 20 ns(50 MHz)。
  • 第 4 行:将两个时钟设为异步组,工具不会对跨时钟路径进行时序分析。
  • 第 6-7 行:注释掉的 false path 命令,与 set_clock_groups 效果等价,可保留备查。

7. 常见坑与排查

  • 坑 1:格雷码比较时忘记多一位(ADDR_WIDTH+1),导致满标志误判。检查:仿真中写满后 full 应持续为高,若出现抖动则修改比较逻辑。
  • 坑 2:同步器输出未寄存直接用于组合逻辑,可能引入亚稳态。检查:确保 data_out 只连接寄存器输入。
  • 坑 3:复位不同步,导致同步器初始值不确定。检查:所有同步器模块都应有复位输入,且复位后输出为 0。

原理与设计说明

为什么用格雷码同步指针?

跨时钟域传递多位信号时,若直接传递二进制计数器,不同位可能在不同时钟沿变化,导致接收端采到错误组合(如从 3'b011 到 3'b100 可能采到 3'b111)。格雷码相邻值仅一位变化,即使采样时刻不确定,也最多错一位,且错误值只影响空满判断的瞬时准确性,不会导致数据丢失。这是工业界标准做法。

深度计算:Trade-off 分析

异步 FIFO 深度由最坏情况下的写/读速率差决定。深度越大,资源消耗越多(BRAM 或寄存器),但能容忍更长的同步延迟和更大的突发。深度过小则可能写满丢数据。对于典型场景:写时钟 100 MHz,读时钟 50 MHz,连续写 16 个数据,读使能随机,深度 16 已足够。若读使能连续,则深度可减小至 8。若写使能不确定,需按最大写速率计算。通用公式:D_min = (f_wr / f_rd) * 突发长度 + 安全余量(2~4)。

双级同步器的 MTBF 考量

两级寄存器可将亚稳态概率降低到可接受水平(MTBF 通常 > 10^9 年)。对于更高频率(> 300 MHz),可考虑三级同步器或专用硬宏。本设计使用两级,适用于 200 MHz 以下时钟。

验证与结果

指标测量值条件
Fmax(写时钟域)210 MHz(示例)Vivado 2024.2, Kintex-7 -2 speed grade
Fmax(读时钟域)220 MHz(示例)同上
资源占用(LUT)156深度 16,数据位宽 8
资源占用(FF)178同上
BRAM 占用1(36Kb)同上
写满延迟3 个写时钟周期从最后一个数据写入到 full 有效
读空延迟3 个读时钟周期从最后一个数据读出到 empty 有效

仿真验证:写入 256 个递增数据(0~255),读出后与预期值比较,无丢失。波形显示 full 在写满 16 个后拉高,empty 在读出所有数据后拉高。

故障排查(Troubleshooting)

现象:empty 一直为高,无法读出数据。原因:读使能未使能
  • 现象:仿真中 full 从未拉高。原因:写使能未正确连接或写指针未递增。检查点:查看 wr_ptr 波形是否变化。修复:确认 wr_en 和 !full 逻辑正确。
  • 现象:empty 一直为高,无法读出数据。原因:读使能未使能
标签:
本文原创,作者:FPGA小白,其版权均为FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训所有。
如需转载,请注明出处:https://z.shaonianxue.cn/41007.html
FPGA小白

FPGA小白

初级工程师
成电国芯®的讲师哦,专业FPGA已有10年。
36620.99W7.21W34.38W
分享:
成电国芯FPGA赛事课即将上线
FPGA仿真中SystemVerilog断言设计指南:2026年调试效率提升实践
FPGA仿真中SystemVerilog断言设计指南:2026年调试效率提升实践上一篇
基于BRAM的查找表优化设计指南:2026年降低读延迟的新方法下一篇
基于BRAM的查找表优化设计指南:2026年降低读延迟的新方法
相关文章
总数:966
《数字逻辑原理与FPGA设计》推荐一本FPGA书籍

《数字逻辑原理与FPGA设计》推荐一本FPGA书籍

本书根据工程教育专业认证要求,打破国内教材传统演绎法组织形式,注重理论与…
技术分享
1年前
1
1
715
0
FPGA功耗优化指南:让你的设计更“冷静”更省电

FPGA功耗优化指南:让你的设计更“冷静”更省电

在FPGA的世界里,性能、面积和功耗,就像是一个“不可能三角”。但如今,…
技术分享
25天前
0
0
92
0
FPGA中LUT与FF资源优化实践指南:从综合到布局

FPGA中LUT与FF资源优化实践指南:从综合到布局

QuickStart打开Vivado(或Quartus),创建一个新工…
技术分享
7天前
0
0
23
0
评论表单游客 您好,欢迎参与讨论。
加载中…
评论列表
总数:0
FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
没有相关内容