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

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

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

Quick Start

  • 准备环境:安装 Vivado 2024.x(或更高版本),确认支持目标器件(如 Xilinx Artix-7 / Kintex-7 系列)。
  • 创建工程:新建 RTL 工程,添加双口 RAM 模块(可例化 Xilinx 原语 RAMB18E1RAMB36E1)和异步 FIFO 顶层模块。
  • 编写异步 FIFO 核心逻辑:用双口 RAM 作为存储体,写时钟域产生写指针与满标志,读时钟域产生读指针与空标志;使用格雷码跨时钟域同步指针。
  • 编写仿真测试台:生成写时钟(100 MHz)和读时钟(150 MHz),随机写入 200 个数据,然后连续读取,验证数据顺序与完整性。
  • 运行行为仿真:在 Vivado 中启动仿真,观察 wptrrptrwfullrempty 波形,确认无毛刺、无数据丢失。
  • 综合与实现:运行综合,检查资源利用率(LUT、FF、BRAM);运行实现,检查时序裕量(Setup/Hold)。
  • 上板验证:将 FIFO 连接到 UART 或 VIO 核,向 FIFO 写入已知序列,读出并与预期比对。预期现象:写入 0xAA、0xBB、0xCC,读出顺序不变。
  • 验收点:仿真中 wfull 在 FIFO 写满时准确拉高,rempty 在读空时准确拉高;上板后数据无错位、无丢失。

前置条件与环境

项目推荐值说明替代方案
器件/板卡Xilinx Artix-7 XC7A35T入门级 FPGA,含 BRAM 资源Kintex-7 / Spartan-7;Altera Cyclone V
EDA 版本Vivado 2024.2支持 SystemVerilog-2012,格雷码综合优化好Vivado 2023.x / 2025.x;Quartus Prime 23.x
仿真器Vivado Simulator (xsim)内置于 Vivado,免配置ModelSim / Questa / Verilator(仅仿真)
时钟/复位写时钟 100 MHz,读时钟 150 MHz;异步复位,高有效写时钟频率 < 读时钟,避免读指针同步滞后导致满标志误判写时钟 > 读时钟时需额外处理“almost full”
接口依赖标准 FIFO 接口:wclk, wr_en, wdata, wfull; rclk, rd_en, rdata, rempty无特殊总线协议,通用性强AXI-Stream 接口(需额外转换逻辑)
约束文件XDC 约束:写时钟周期 10 ns,读时钟周期 6.667 ns;false path 跨时钟域路径必须约束跨时钟域路径为 set_false_path,避免时序分析误报set_clock_groups -asynchronous 约束

目标与验收标准

  • 功能点:双口 RAM 实现的异步 FIFO 在写时钟域和读时钟域之间正确传递数据,无丢失、无重复、无顺序错乱。
  • 性能指标:Fmax 满足写时钟 ≥ 100 MHz、读时钟 ≥ 150 MHz;BRAM 资源占用 ≤ 1 个(深度 16–256 可配置)。
  • 验收方式
    • 仿真波形:wfull 在写入深度个数据后拉高,rempty 在读出所有数据后拉高;格雷码指针过渡时无亚稳态(通过多次仿真验证)。
    • 上板测试:通过 UART 或 VIO 读取 FIFO 内容,与写入序列完全一致。
    • 时序报告:Setup Slack > 0,Hold Slack > 0,跨时钟域路径无时序违规。

实施步骤

工程结构

  • 顶层模块:async_fifo_top.v,例化双口 RAM 和 FIFO 控制逻辑。
  • 双口 RAM 模块:dpram.v,使用 Xilinx 原语 RAMB36E1(深度 512,宽度 8)。
  • FIFO 控制模块:fifo_ctrl.v,包含写指针、读指针、格雷码转换、同步器、满/空标志生成。
  • 仿真测试台:tb_async_fifo.v,生成写/读时钟,驱动写/读使能,自动比对数据。

关键模块:双口 RAM 例化

module dpram #(
    parameter ADDR_WIDTH = 9,
    parameter DATA_WIDTH = 8
)(
    input  wire                wclk,
    input  wire                wen,
    input  wire [ADDR_WIDTH-1:0] waddr,
    input  wire [DATA_WIDTH-1:0] wdata,
    input  wire                rclk,
    input  wire                ren,
    input  wire [ADDR_WIDTH-1:0] raddr,
    output reg  [DATA_WIDTH-1:0] rdata
);

    (* ram_style = "block" *) reg [DATA_WIDTH-1:0] mem [0:(1&lt;&lt;ADDR_WIDTH)-1];

    always @(posedge wclk) begin
        if (wen) mem[waddr] &lt;= wdata;
    end

    always @(posedge rclk) begin
        if (ren) rdata &lt;= mem[raddr];
    end

endmodule

逐行说明

  • 第 1–4 行:模块声明,参数化地址宽度(9 位,深度 512)和数据宽度(8 位),便于复用。
  • 第 5–12 行:端口列表,写端口和读端口完全独立,时钟不同。
  • 第 14 行:声明存储体 mem,使用 ram_style = "block" 属性强制综合工具推断为 BRAM,而非分布式 RAM。
  • 第 16–18 行:写操作:在写时钟上升沿,若写使能有效,将数据写入地址。
  • 第 20–22 行:读操作:在读时钟上升沿,若读使能有效,输出地址对应的数据。注意:此处为读后输出(read-after-write)行为,与 FIFO 读操作兼容。

关键模块:异步 FIFO 控制

module async_fifo #(
    parameter DSIZE = 8,
    parameter ASIZE = 4  // 深度 2^ASIZE = 16
)(
    input  wire              wclk, wrst_n,
    input  wire              wr_en,
    input  wire [DSIZE-1:0]  wdata,
    output wire              wfull,
    input  wire              rclk, rrst_n,
    input  wire              rd_en,
    output wire [DSIZE-1:0]  rdata,
    output wire              rempty
);

    wire [ASIZE-1:0] waddr, raddr;
    reg  [ASIZE:0]   wptr, rptr;          // 二进制指针(多 1 位用于满/空判断)
    reg  [ASIZE:0]   wq2_rptr, wq1_rptr;  // 读指针同步到写时钟域
    reg  [ASIZE:0]   rq2_wptr, rq1_wptr;  // 写指针同步到读时钟域
    wire [ASIZE:0]   wgray, rgray;         // 格雷码
    wire [ASIZE:0]   wptr_next, rptr_next;

    // 双口 RAM 例化
    dpram #(.ADDR_WIDTH(ASIZE), .DATA_WIDTH(DSIZE)) u_dpram (
        .wclk (wclk), .wen (wr_en), .waddr (waddr), .wdata (wdata),
        .rclk (rclk), .ren (rd_en), .raddr (raddr), .rdata (rdata)
    );

    // 写指针逻辑
    always @(posedge wclk or negedge wrst_n) begin
        if (!wrst_n) wptr &lt;= 0;
        else if (wr_en &amp;&amp; !wfull) wptr &lt;= wptr_next;
    end
    assign waddr = wptr[ASIZE-1:0];
    assign wptr_next = wptr + 1;

    // 读指针逻辑
    always @(posedge rclk or negedge rrst_n) begin
        if (!rrst_n) rptr &lt;= 0;
        else if (rd_en &amp;&amp; !rempty) rptr &gt; 1) ^ wptr;
    assign rgray = (rptr &gt;&gt; 1) ^ rptr;

    // 跨时钟域同步:写时钟域同步读指针
    always @(posedge wclk or negedge wrst_n) begin
        if (!wrst_n) begin wq1_rptr &lt;= 0; wq2_rptr &lt;= 0; end
        else begin wq1_rptr &lt;= rgray; wq2_rptr &lt;= wq1_rptr; end
    end

    // 跨时钟域同步:读时钟域同步写指针
    always @(posedge rclk or negedge rrst_n) begin
        if (!rrst_n) begin rq1_wptr &lt;= 0; rq2_wptr &lt;= 0; end
        else begin rq1_wptr &lt;= wgray; rq2_wptr &lt;= rq1_wptr; end
    end

    // 满标志生成
    assign wfull = (wptr_next[ASIZE] != wq2_rptr[ASIZE]) &amp;&amp;
                   (wptr_next[ASIZE-1:0] == wq2_rptr[ASIZE-1:0]);

    // 空标志生成
    assign rempty = (rptr_next == rq2_wptr);

endmodule

逐行说明

  • 第 1–4 行:模块参数化,DSIZE 数据宽度,ASIZE 地址宽度(深度 = 2^ASIZE)。
  • 第 5–14 行:端口声明,包含写/读时钟、复位、使能、数据、满/空标志。
  • 第 16–19 行:内部信号:wptr/rptr 为二进制指针(多 1 位用于判断满/空);wq2_rptr/rq2_wptr 为两级同步器输出;wgray/rgray 为格雷码。
  • 第 21–26 行:例化双口 RAM,地址宽度为 ASIZE(不含最高位),数据宽度为 DSIZE
  • 第 28–32 行:写指针更新:复位清零,非满且写使能时递增。地址取指针低 ASIZE 位。
  • 第 34–38 行:读指针更新:复位清零,非空且读使能时递增。地址取指针低 ASIZE 位。
  • 第 40–41 行:二进制转格雷码:gray = (bin >> 1) ^ bin,保证相邻值仅 1 位变化。
  • 第 43–47 行:写时钟域同步读指针:两级寄存器 wq1_rptrwq2_rptr,打两拍降低亚稳态概率。
  • 第 49–53 行:读时钟域同步写指针:同理,两级同步。
  • 第 55–56 行:满标志:当写指针的下一个值(格雷码)的最高位与同步后的读指针最高位不同,且低位相同时,表示写指针追上了读指针(绕了一圈)。
  • 第 58 行:空标志:当读指针的下一个值等于同步后的写指针时,表示读指针追上了写指针。

时序约束

# 写时钟约束
create_clock -name wclk -period 10.000 [get_ports wclk]
# 读时钟约束
create_clock -name rclk -period 6.667 [get_ports rclk]
# 跨时钟域路径设为 false path(同步器内部已处理)
set_false_path -from [get_clocks wclk] -to [get_clocks rclk]
set_false_path -from [get_clocks rclk] -to [get_clocks wclk]

逐行说明

  • 第 1–2 行:定义写时钟周期 10 ns(100 MHz)和读时钟周期 6.667 ns(150 MHz)。
  • 第 3–4 行:将两个时钟域之间的所有路径设为 set_false_path,因为跨时钟域数据已经通过同步器处理,时序工具不应分析这些路径的建立/保持时间。

常见坑与排查

  • 坑 1:满/空标志误判——原因:指针同步延迟导致满标志提前拉高或空标志延迟拉低。排查:在仿真中增加写/读使能间隔,观察 wfullrempty 是否在正确时刻变化。
  • 坑 2:格雷码转换错误——原因:二进制指针位宽不匹配,或转换公式写错。排查:打印 wptrwgray 的二进制值,确认相邻值仅 1 位变化。
  • 坑 3:同步器级数不足——原因:仅用一级寄存器同步,亚稳态概率高。排查:检查同步器是否为两级(或三级),仿真中增加随机抖动测试。
  • 坑 4:复位不同步——原因:写/读复位不同时释放,导致指针初始状态不一致。排查:确保 wrst_nrrst_n 在各自时钟域内同步释放(或使用异步复位同步释放电路)。

原理与设计说明

为什么用双口 RAM 实现异步 FIFO?双口 RAM 天然支持两个独立时钟域的读写操作,无需额外仲裁逻辑。相比寄存器堆(LUT+FF),BRAM 在深度 > 16 时面积效率更高(1 个 BRAM36 可存储 512×8 位)。

为什么用格雷码同步指针?二进制指针跨时钟域时,多位同时变化会导致亚稳态和错误采样。格雷码每次仅 1 位变化,即使采样到亚稳态,也只会导致 1 位错误,且同步器打两拍后错误概率极低。这是异步 FIFO 设计的经典方法,trade-off 是增加了格雷码转换逻辑(少量 LUT),但换来了跨时钟域可靠性。

满/空标志的边界条件:满标志在写时钟域生成,使用“写指针下一个值”与“同步后的读指针”比较,确保写操作不会覆盖未读数据。空标志在读时钟域生成,使用“读指针下一个值”与“同步后的写指针”比较,确保读操作不会读出无效数据。注意:由于同步延迟,满标志可能提前拉高(写操作被阻塞),但不会延迟拉高(不会溢出);空标志可能延迟拉低(读操作被阻塞),但不会提前拉低(不会读空)。

验证与结果

项目结果(示例)测量条件
Fmax(写时钟)≥ 150 MHzVivado 2024.2,Artix-7 -1 速度等级
Fmax(读时钟)≥ 200 MHz同上
资源占用LUT: 48, FF: 72, BRAM36: 1深度 16,数据宽度 8
延迟(写→读)3–5 个读时钟周期写使能到读数据有效,含同步器延迟
数据完整性100% 正确(仿真 10^5 次随机操作)写/读时钟比 1:1.5,随机使能间隔

注意:以上结果为典型配置下的示例值,实际值取决于器件型号、综合选项和布局布线。建议读者在自己的工程中运行时序分析确认。

故障排查(Troubleshooting)

  • 现象:仿真中 wfull 始终为 0——原因:写指针未递增(wr_en!wfull 条件不满足)。检查:确认 wr_en 有效且 wfull 确实为低。
  • 现象:rempty 始终为 1——原因:读指针未递增,或同步后的写指针始终为 0。检查:确认 rd_en 有效且 rempty 为低;检查同步器输出 rq2_wptr 是否为非零。
  • 现象:数据读出顺序错乱——原因:双口 RAM 的读地址与写地址不同步。检查:确认 raddr 来自读指针低 ASIZE 位,waddr 来自写指针低 ASIZE 位。
  • 现象:上板后 FIFO 溢出或读空——原因:满/空标志时序错误。检查:在仿真中增加最坏情况(写连续、读连续)测试;检查约束中是否设置了 false path。
  • 现象:综合报告显示大量 LUT 用于存储——原因:双口 RAM 未推断为 BRAM。检查:确认 ram_style 属性正确;检查地址/数据宽度是否超出 BRAM 支持范围(如深度 > 1024)。
  • 现象:时序分析报告显示跨时钟域路径违规——原因:未设置 false path。检查:在 XDC 中添加 set_false_pathset_clock_groups -asynchronous
  • 现象:仿真中数据丢失(某些位置为 X)——原因:未初始化 BRAM 或指针。检查:在仿真中给 wrst_nrrst_n 施加复位;检查 BRAM 初始化文件(.coe)是否存在。
  • 现象:上板后 FIFO 工作但偶尔出错——原因:亚稳态导致同步器输出错误。检查:增加同步器级数(三级);降低时钟频率测试是否复现。
  • 现象:写使能有效但 wfull 拉高过快——原因:满标志比较逻辑错误。检查:确认 wptr_nextwq2_rptr 的比较条件(最高位不同且低位相同)。
  • 现象:读使能有效但 rempty 拉低过慢——原因:同步器延迟导致空标志更新滞后。检查:这是正常行为,只要不导致读空即可。若需更快的空标志,可增加“almost empty”逻辑。
标签:
本文原创,作者:二牛学FPGA,其版权均为FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训所有。
如需转载,请注明出处:https://z.shaonianxue.cn/42166.html
二牛学FPGA

二牛学FPGA

初级工程师
这家伙真懒,几个字都不愿写!
1.06K20.57W4.05W3.67W
分享:
成电国芯FPGA赛事课即将上线
FPGA时序约束中set_false_path与set_clock_groups的正确用法:设计与实施指南
FPGA时序约束中set_false_path与set_clock_groups的正确用法:设计与实施指南上一篇
Verilog实战:2026年用双口RAM实现异步FIFO的常见调试技巧下一篇
Verilog实战:2026年用双口RAM实现异步FIFO的常见调试技巧
相关文章
总数:1.10K
Vivado仿真中Tcl脚本自动化测试用例设计

Vivado仿真中Tcl脚本自动化测试用例设计

QuickStart步骤一:在Vivado中创建一个仿真工程,添加待测…
技术分享
15天前
0
0
28
0
FPGA图像处理:Sobel边缘检测的流水线优化与资源权衡

FPGA图像处理:Sobel边缘检测的流水线优化与资源权衡

QuickStart步骤一:在Vivado2024.2中新建项目…
技术分享
6天前
0
0
14
0
Verilog实战:2026年用流水线结构优化FFT处理器性能

Verilog实战:2026年用流水线结构优化FFT处理器性能

QuickStart步骤一:在Vivado2025.2中新建项目,器…
技术分享
2天前
0
0
17
0
评论表单游客 您好,欢迎参与讨论。
加载中…
评论列表
总数:0
FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
没有相关内容