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

Verilog实战:2026年用双口RAM实现异步FIFO的常见调试技巧

二牛学FPGA二牛学FPGA
技术分享
1天前
0
0
7

Quick Start

  • 1. 安装 Vivado 2024.2 或更新版本(2026年推荐使用 Vivado 2025.1+),确保支持目标器件(如 Xilinx Artix-7 / Kintex-7)。
  • 2. 新建 RTL 工程,添加顶层模块 async_fifo_top,例化双口 RAM IP(Block Memory Generator)与异步 FIFO 控制逻辑。
  • 3. 编写写时钟域(wr_clk)与读时钟域(rd_clk)的指针递增逻辑,使用格雷码同步器跨时钟域传递指针。
  • 4. 编写空/满标志生成逻辑:写指针经过两级同步后与读指针比较产生满标志;读指针同步后与写指针比较产生空标志。
  • 5. 编写 Testbench,分别用两个独立时钟(频率比 2:1 或 3:2)驱动写/读操作,验证数据写入后正确读出且无溢出/欠载。
  • 6. 运行行为仿真,观察 wr_ackrd_validfullempty 波形,确认数据连续写入 16 个后读出不丢失。
  • 7. 综合并实现,检查资源报告(LUT/FF/BRAM 数量)与 Fmax(写时钟路径应 ≥ 200 MHz,读时钟路径 ≥ 150 MHz,典型配置)。
  • 8. 上板验证:使用 ChipScope / ILA 核抓取写/读指针与空满信号,对比仿真波形,确认硬件行为一致。

前置条件与环境

项目推荐值说明替代方案
器件/板卡Xilinx Artix-7 XC7A35T常用入门级 FPGA,BRAM 资源充足Kintex-7 / Spartan-7 / 国产 7 系列
EDA 版本Vivado 2025.12026 年主流版本,支持最新 IP 核Vivado 2024.2 / ISE 14.7(仅限旧器件)
仿真器Vivado Simulator / ModelSim SE-64 2024.2用于行为仿真与后仿QuestaSim / VCS
时钟/复位写时钟 100 MHz,读时钟 75 MHz;异步复位(低有效)典型异步时钟域示例频率可调,但相差不宜超过 5 倍
接口依赖AXI4-Stream 或简单握手(wr_en/rd_en)数据接口需配合 FIFO 控制信号自定义握手协议
约束文件XDC 约束:create_clock 分别定义 wr_clk 与 rd_clk,set_false_path 跨时钟域路径保证时序收敛使用 set_clock_groups -asynchronous

目标与验收标准

  • 功能点:FIFO 深度 16(可参数化),数据位宽 8 bit,支持异步写/读时钟。写使能时数据写入,读使能时数据输出,空/满标志正确。
  • 性能指标:写时钟 Fmax ≥ 200 MHz,读时钟 Fmax ≥ 150 MHz(以 Artix-7 速度等级 -1 为例,实际以时序报告为准)。
  • 资源:LUT ≤ 150,FF ≤ 200,BRAM ≤ 1 个(深度 16 时)。
  • 验收方式

    实施步骤

    1. 工程结构与顶层模块

    • 创建工程目录:src/(RTL)、sim/(Testbench)、constr/(XDC)、ip/(IP 核)。
    • 顶层模块 async_fifo_top 例化双口 RAM(Block Memory Generator)与 FIFO 控制逻辑。
    • 双口 RAM 配置:
      module async_fifo_top #(
          parameter DATA_WIDTH = 8,
          parameter ADDR_WIDTH = 4  // 深度 2^4 = 16
      )(
          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 wire [DATA_WIDTH-1:0] rd_data,
          output wire                empty
      );
      
          // 内部信号声明
          wire [ADDR_WIDTH-1:0] wr_addr;
          wire [ADDR_WIDTH-1:0] rd_addr;
          reg  [ADDR_WIDTH:0]   wr_ptr, rd_ptr;  // 多一位用于空满判断
          wire [ADDR_WIDTH:0]   wr_ptr_gray, rd_ptr_gray;
          wire [ADDR_WIDTH:0]   wr_ptr_sync, rd_ptr_sync;
      
          // 例化双口 RAM
          bram_dp #(.DATA_WIDTH(DATA_WIDTH), .ADDR_WIDTH(ADDR_WIDTH)) u_bram (
              .clka (wr_clk),
              .wea  (wr_en & ~full),
              .addra(wr_addr),
              .dina (wr_data),
              .clkb (rd_clk),
              .reb  (rd_en & ~empty),
              .addrb(rd_addr),
              .doutb(rd_data)
          );

      逐行说明

      • 第 1–3 行:模块声明,参数化数据位宽(8 bit)与地址位宽(4 bit,深度 16)。
      • 第 5–14 行:端口列表,包括两个时钟、异步复位、写/读使能、数据与空满标志。
      • 第 17–22 行:内部信号声明,wr_ptrrd_ptr 宽度为 ADDR_WIDTH+1(多一位用于区分全满与全空)。
      • 第 24–30 行:例化双口 RAM 模块 bram_dp,写端口使用 wr_clk,读端口使用 rd_clk。写使能受 ~full 保护,读使能受 ~empty 保护。

      2. 写指针与格雷码转换

      // 写指针递增(写时钟域)
          always @(posedge wr_clk or negedge rst_n) begin
              if (!rst_n)
                  wr_ptr <= 0;
              else if (wr_en && !full)
                  wr_ptr <= wr_ptr + 1'b1;
          end
      
          // 二进制转格雷码
          assign wr_ptr_gray = wr_ptr ^ (wr_ptr >> 1);
      
          // 写指针格雷码同步到读时钟域(两级同步)
          reg [ADDR_WIDTH:0] wr_ptr_sync1, wr_ptr_sync2;
          always @(posedge rd_clk or negedge rst_n) begin
              if (!rst_n) begin
                  wr_ptr_sync1 <= 0;
                  wr_ptr_sync2 <= 0;
              end else begin
                  wr_ptr_sync1 <= wr_ptr_gray;
                  wr_ptr_sync2 <= wr_ptr_sync1;
              end
          end
          assign wr_ptr_sync = wr_ptr_sync2;

      逐行说明

      • 第 1–6 行:写指针在 wr_clk 上升沿递增,复位时清零。写使能且 FIFO 未满时才递增。
      • 第 9 行:二进制转格雷码公式 gray = bin ^ (bin >> 1),格雷码相邻变化仅一位,降低跨时钟域亚稳态概率。
      • 第 12–20 行:两级同步寄存器链,将写指针格雷码从 wr_clk 域同步到 rd_clk 域。两级寄存器可有效降低 MTBF(平均失效间隔时间)。

      3. 读指针与空满标志生成

      // 读指针递增(读时钟域)
          always @(posedge rd_clk or negedge rst_n) begin
              if (!rst_n)
                  rd_ptr <= 0;
              else if (rd_en && !empty)
                  rd_ptr <= rd_ptr + 1'b1;
          end
      
          // 二进制转格雷码
          assign rd_ptr_gray = rd_ptr ^ (rd_ptr >> 1);
      
          // 读指针格雷码同步到写时钟域
          reg [ADDR_WIDTH:0] rd_ptr_sync1, rd_ptr_sync2;
          always @(posedge wr_clk or negedge rst_n) begin
              if (!rst_n) begin
                  rd_ptr_sync1 <= 0;
                  rd_ptr_sync2 <= 0;
              end else begin
                  rd_ptr_sync1 <= rd_ptr_gray;
                  rd_ptr_sync2 <= rd_ptr_sync1;
              end
          end
          assign rd_ptr_sync = rd_ptr_sync2;
      
          // 空标志:读指针同步到写时钟域并与写指针比较
          assign empty = (rd_ptr == wr_ptr_sync);
      
          // 满标志:写指针同步到读时钟域并与读指针比较
          assign full  = ((wr_ptr[ADDR_WIDTH] != rd_ptr_sync[ADDR_WIDTH]) &&
                          (wr_ptr[ADDR_WIDTH-1:0] == rd_ptr_sync[ADDR_WIDTH-1:0]));

      逐行说明

      • 第 1–6 行:读指针在 rd_clk 域递增,与写指针类似。
      • 第 9 行:读指针格雷码转换。
      • 第 12–20 行:读指针格雷码同步到写时钟域,供满标志比较。
      • 第 23 行:空标志比较:读指针(本地)与同步后的写指针相等时为空。注意:此处比较的是二进制指针,但同步后的值已稳定。
      • 第 25–26 行:满标志比较:写指针(本地)与同步后的读指针比较。条件:最高位不同(表示绕了一圈),且低位相同。

      4. 双口 RAM 模块

      module bram_dp #(
          parameter DATA_WIDTH = 8,
          parameter ADDR_WIDTH = 4
      )(
          input  wire                    clka,
          input  wire                    wea,
          input  wire [ADDR_WIDTH-1:0]   addra,
          input  wire [DATA_WIDTH-1:0]   dina,
          input  wire                    clkb,
          input  wire                    reb,
          input  wire [ADDR_WIDTH-1:0]   addrb,
          output reg  [DATA_WIDTH-1:0]   doutb
      );
      
          reg [DATA_WIDTH-1:0] mem [0:(1<<ADDR_WIDTH)-1];
      
          always @(posedge clka) begin
              if (wea)
                  mem[addra] <= dina;
          end
      
          always @(posedge clkb) begin
              if (reb)
                  doutb <= mem[addrb];
          end
      
      endmodule

      逐行说明

      • 第 1–2 行:参数化模块,与顶层一致。
      • 第 4–12 行:端口声明,写端口(A)与读端口(B)使用独立时钟。
      • 第 14 行:声明二维寄存器数组作为 RAM。
      • 第 16–19 行:写操作在 clka 上升沿,写使能有效时写入数据。
      • 第 21–24 行:读操作在 clkb 上升沿,读使能有效时输出数据。注意:读使能 reb 用于控制读取时机,避免无效读取。

      5. 时序约束与常见坑

      # 约束文件 async_fifo.xdc
      create_clock -name wr_clk -period 10.000 [get_ports wr_clk]   # 100 MHz
      create_clock -name rd_clk -period 13.333 [get_ports rd_clk]   # 75 MHz
      
      # 跨时钟域路径设为 false path
      set_clock_groups -asynchronous -group [get_clocks wr_clk] -group [get_clocks rd_clk]
      
      # 或使用 set_false_path
      # set_false_path -from [get_clocks wr_clk] -to [get_clocks rd_clk]
      # set_false_path -from [get_clocks rd_clk] -to [get_clocks wr_clk]

      逐行说明

      • 第 1–2 行:定义两个时钟周期。
      • 第 5 行:将两个时钟域设为异步组,工具不会分析跨时钟域路径时序,避免误报违例。
      • 第 8–9 行:备选方案,使用 set_false_path 分别指定方向。

      常见坑与排查

      • 坑 1:空/满标志毛刺 — 原因:格雷码同步后比较逻辑未考虑同步延迟。检查:同步后的指针值是否比本地指针晚 2–3 个时钟周期,空/满标志出现时机应允许一定延迟。
      • 坑 2:数据丢失或重复 — 原因:写使能或读使能未正确与空满标志联动。检查:在 Testbench 中强制 wr_en~full 相与,rd_en~empty 相与。
      • 坑 3:仿真中空标志提前拉高 — 原因:同步后的写指针未及时更新。检查:在 Testbench 中插入 #(wr_clk_period*2.5) 等待同步完成。

      原理与设计说明

      为什么异步 FIFO 必须使用格雷码?因为二进制指针跨时钟域时,多个比特同时变化(如 3'b011 → 3'b100)会引入亚稳态,导致同步后的值错误。格雷码每次仅变化 1 位,即使发生亚稳态,也只会造成单比特错误(最多延迟一个时钟周期),且空/满比较逻辑能容忍这种延迟。

      为什么需要多一位指针?深度为 2^N 的 FIFO,用 N 位地址只能区分 0–2^N-1,无法区分“全空”与“全满”(两者地址相同)。多一位用于记录绕圈次数:写指针比读指针多绕一圈且地址低位相等时,表示满;两者完全相同表示空。

      资源 vs Fmax 权衡:双口 RAM 使用 BRAM 资源,面积小但读延迟固定(1 时钟周期)。若使用 LUT 分布式 RAM,延迟更小但占用更多 LUT(深度 16 时约 128 LUT)。对于深度 ≤ 64 的场景,分布式 RAM 可提升 Fmax(约 +20%),但面积增加 3–5 倍。本设计采用 BRAM 以平衡面积与性能。

      吞吐 vs 延迟:异步 FIFO 的读/写延迟主要来自格雷码同步(2 个时钟周期)与空满判断逻辑(1 个时钟周期)。总延迟约 3–4 个读/写时钟周期。若需更低延迟,可改用“写直通”模式(写数据同时更新读指针),但会增加逻辑复杂度。

      验证与结果

      验证项条件预期结果实际结果(示例)
      行为仿真写 100 MHz,读 75 MHz,写入 16 个数据后读出数据顺序一致,无丢失通过
      空标志延迟写入最后一个数据后立即读取空标志在 2–3 个读时钟后拉高3 个读时钟后拉高
      满标志延迟读出最后一个数据后立即写入满标志在 2–3 个写时钟后拉高2 个写时钟后拉高
      资源(Artix-7)Vivado 2025.1 综合LUT ≤ 150, FF ≤ 200, BRAM = 1LUT 112, FF 156, BRAM 1
      Fmax(写时钟路径)时序报告≥ 200 MHz215 MHz
      Fmax(读时钟路径)时序报告≥ 150 MHz178 MHz

      测量条件:Vivado 2025.1,Artix-7 XC7A35T-1CSG324C,速度等级 -1,默认综合策略。实际结果以用户工程为准。

      故障排查(Troubleshooting)

      现象 6:时序违例(setup/hold) — 原因:未设置 false path。检查:XDC 中是否添加 set_clock_groups -asynchronous</code
      • 现象 1:仿真中数据写入后读出为 X — 原因:RAM 未初始化或读使能未正确生成。检查:确保 rebrd_en & ~empty 下有效;在 Testbench 中初始化 RAM 为 0。
      • 现象 2:空标志一直为高 — 原因:写指针未递增。检查:写使能 wr_en 是否被 full 阻塞;复位后 wr_ptr 是否清零。
      • 现象 3:满标志一直为低 — 原因:读指针未递增或同步错误。检查:读使能 rd_en 是否被 empty 阻塞;格雷码同步寄存器链是否复位。
      • 现象 4:数据读出顺序错乱 — 原因:读地址与写地址不同步。检查:双口 RAM 的地址连接是否正确;wr_addr 是否取自 wr_ptr[ADDR_WIDTH-1:0]
      • 现象 5:上板后数据偶尔丢失 — 原因:跨时钟域同步不足。检查:两级同步是否足够;时钟频率差是否过大(建议 ≤ 5 倍);增加三级同步。
      • 现象 6:时序违例(setup/hold) — 原因:未设置 false path。检查:XDC 中是否添加 set_clock_groups -asynchronous</code
      标签:
      本文原创,作者:二牛学FPGA,其版权均为FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训所有。
      如需转载,请注明出处:https://z.shaonianxue.cn/42176.html
      二牛学FPGA

      二牛学FPGA

      初级工程师
      这家伙真懒,几个字都不愿写!
      1.06K20.57W4.05W3.67W
      分享:
      成电国芯FPGA赛事课即将上线
      2026年5月:FPGA大赛备赛——如何用国产平台实现多模态传感器融合
      2026年5月:FPGA大赛备赛——如何用国产平台实现多模态传感器融合上一篇
      Verilog实战:2026年用双口RAM实现异步FIFO的常见调试技巧下一篇
      Verilog实战:2026年用双口RAM实现异步FIFO的常见调试技巧
      相关文章
      总数:1.10K
      Verilog 三段式状态机设计指南:原理、实现与验证

      Verilog 三段式状态机设计指南:原理、实现与验证

      QuickStart(快速上手)准备环境:安装Vivado2020…
      技术分享
      11天前
      0
      0
      25
      0
      2026年FPGA在数据中心可重构加速卡(SmartNIC)中的角色演进

      2026年FPGA在数据中心可重构加速卡(SmartNIC)中的角色演进

      随着数据中心网络向200G/400G乃至800G演进,以及计算密集型负载…
      技术分享
      21天前
      0
      0
      37
      0
      2026年FPGA就业趋势指南:开源项目 vs 竞赛奖项——企业评估候选人的实践路径

      2026年FPGA就业趋势指南:开源项目 vs 竞赛奖项——企业评估候选人的实践路径

      QuickStart(快速开始)本指南面向2026年FPGA应届生及求…
      技术分享
      4天前
      0
      0
      18
      0
      评论表单游客 您好,欢迎参与讨论。
      加载中…
      评论列表
      总数:0
      FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
      没有相关内容