FPGA 流式实战 · 02 异步 FIFO、Gray 指针与输出槽

从本地二进制地址、跨域 Gray 指针到保守满空判断,解释连续读取与实际容量。

先修知识完成 FPGA AXI / CDC 进阶课程;掌握同步 FIFO、握手、Gray 编码和时序报告基础。

适用环境Basys 3 / Artix-7;板上写 100 MHz、读 50 MHz;Python 3、Icarus Verilog;Vivado 2024.1 命令参考。

验证状态六场景 RTL 测试、两类故障检出及顶层软件模型已验证;性能表为仿真测量,Vivado 与板上 ILA 尚需实测。

本文目录指针所有权保护存储数据逐行理解满空与预取逻辑用长暂停与环回测试验证边界典型故障:错误断言同步 Gray 必须单比特变化

指针所有权保护存储数据

写域拥有写地址,读域拥有读地址,两端只交换已经寄存的 Gray 指针,避免把不断变化的二进制地址直接同步。数据本身保存在双端访问的存储阵列里,接收方只有看到已发布的写指针才允许读取,发送方只有看到已释放的读指针才重用空间。这个所有权协议是数据一致性的基础,物理时序约束仍需另行审查。

跨域同步的是指针,数据通过受所有权保护的 RAM 传递。写域指针bin → 注册 Gray读域同步链两级同步写指针RAM → 输出槽预取释放 RAM 槽返回读指针Gray → 写域同步仅在本域 VALID 与 READY 同时为 1 的时钟沿转移数据
跨域同步的是指针,数据通过受所有权保护的 RAM 传递。(原创示意,非器件引脚图;窄屏可横向滑动)

逐行理解满空与预取逻辑

RAM 深度固定为十六,地址使用四位,完整指针增加一位环回标记。满判断使用下一写 Gray 与同步读 Gray 最高两位取反后的比较,空判断使用下一读 Gray 与同步写 Gray 比较。读指针在预取到输出槽时前进,而不是等下游消费才前进,因此 RAM 空与输出有效可以同时成立;解释波形时必须区分这两个状态。

设计契约与检查依据
项目工程含义
pushs_valid 且 s_ready,写指针推进
fetchRAM 非空,且输出槽为空或本拍被消费
empty=1、m_valid=1合法:RAM 排空但输出槽仍有一拍
总容量最多 17 拍已接受但未消费
同步延迟满空撤销是保守的,不能用异域瞬时指针反算容量

完整手写教学 FIFO;固定 16 个 RAM 槽位,须配套本课程约束和复位契约。

systemverilog
`timescale 1ns/1ps
// 16 RAM entries + 1 registered read-side output slot. Common reset assertion required.
(* KEEP_HIERARCHY="yes" *)
module axis_async_fifo #(parameter INJECT_ERROR=0)(
 input wire s_clk,s_rst,input wire [31:0] s_data,input wire [3:0] s_keep,
 input wire s_last,s_valid,output wire s_ready,
 input wire m_clk,m_rst,output wire [31:0] m_data,output wire [3:0] m_keep,
 output wire m_last,output reg m_valid,input wire m_ready,
 output wire [63:0] debug_w,debug_r);
 (* ram_style="distributed" *) reg [36:0] mem[0:15];
 reg [4:0] wbin,wgray,rbin,rgray;
 (* ASYNC_REG="TRUE" *)reg [4:0] rgray_meta,rgray_sync,wgray_meta,wgray_sync;
 reg full,empty;
 reg [36:0] out_word;
 wire push=s_valid && s_ready;
`ifdef BUG_BACKPRESSURE
 wire fetch=!empty; // Deliberate overwrite of a stalled output slot.
`else
 wire fetch=!empty && (!m_valid || m_ready);
`endif
 wire [4:0] wnext=wbin+push,rnext=rbin+fetch;
 wire [4:0] wgray_next=(wnext>>1)^wnext,rgray_next=(rnext>>1)^rnext;
 assign s_ready=!s_rst && !full;
 assign {m_last,m_keep,m_data}=out_word;
 always @(posedge s_clk)if(push)mem[wbin[3:0]]<={s_last,s_keep,s_data};
 always @(posedge s_clk or posedge s_rst)begin
  if(s_rst)begin wbin<=0;wgray<=0;rgray_meta<=0;rgray_sync<=0;full<=0;end
  else begin
   rgray_meta<=rgray;rgray_sync<=rgray_meta;
   wbin<=wnext;wgray<=wgray_next;
   full<=(wgray_next=={~rgray_sync[4:3],rgray_sync[2:0]});
  end
 end
 always @(posedge m_clk or posedge m_rst)begin
  if(m_rst)begin rbin<=0;rgray<=0;wgray_meta<=0;wgray_sync<=0;empty<=1;m_valid<=0;out_word<=0;end
  else begin
   wgray_meta<=wgray;wgray_sync<=wgray_meta;
   rbin<=rnext;rgray<=rgray_next;empty<=(rgray_next==wgray_sync);
   if(fetch)begin
    out_word<=mem[rbin[3:0]] ^ (INJECT_ERROR?37'h1:37'h0);m_valid<=1;
   end else if(m_ready)m_valid<=0;
  end
 end
 assign debug_w={40'b0,wbin,wgray,rgray_sync,full,s_valid,s_ready,s_last,s_keep,s_rst};
 assign debug_r={2'b0,out_word,rbin,rgray,wgray_sync,empty,m_valid,m_ready,fetch,m_rst,5'b0};
endmodule

用长暂停与环回测试验证边界

长暂停场景让接收端在开始八十个读周期里不接受数据,测试在暂停期间检查已接受拍数恰好为十七,并确认写端实际经历满反压。随后恢复接收,比较所有拍并检查排空后没有额外输出。四千多拍让有限位宽指针多次环回,能揭示只在第一次填充时看不出来的满空和环回错误。

  1. 观察指针

    预期与判定:确认二进制地址只用于本域 RAM 访问。

  2. 填满队列

    预期与判定:暂停接收后 RAM 与输出槽合计 17 拍。

  3. 恢复并环回

    预期与判定:源被正确反压,恢复后逐拍无丢失或重复。

  4. 检查排空

    预期与判定:最后一拍消费后 m_valid 清零,无额外拍。

在解压后的 fpga-stream 目录执行;软件测试需要 Python 3、iverilog 和 vvp。

bash
python3 flow.py sim
# 长暂停场景:MODE=2,前 80 个读周期不接收
# 内部容量检查:sent == 17
# 失败标识:CAPACITY_EXPECTED_17 / FULL_NOT_EXERCISED
# 排空后继续观察 20 个读周期,不能出现 EXTRA_BEAT

典型故障:错误断言同步 Gray 必须单比特变化

Gray 编码保证本地相邻指针更新至多改变一位,但较慢的接收时钟可能跳过多个合法中间状态。因此两个同步后样本之间出现多位差异,并不自动说明 CDC 错误。本实验的一位变化检查只针对本地连续的注册指针,物理位间偏斜则由约束和实现报告审查,不能把这两类检查混为一谈。

完成检查

  • 理解 16 个 RAM 槽与 1 个输出槽。
  • 满空和多次指针环回经过测试。
  • 不会误用同步后 Gray 的单比特断言。

延伸阅读与来源

本文为黄河电子原创教学说明。产品命令与支持范围请以实际安装版本的官方文档为准。

继续阅读

FPGA

从 RTL 到板上运行的完整流程

建立约束、实现、下载与观测的最小工程闭环。

10 分钟
FPGA

第一个工程的引脚与时钟约束

区分电气连接与时间要求,避免“能生成位流就算正确”。

11 分钟
FPGA

如何读懂时序报告

从起点、终点、时钟和裕量找出失败路径的原因。

10 分钟