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 指针,避免把不断变化的二进制地址直接同步。数据本身保存在双端访问的存储阵列里,接收方只有看到已发布的写指针才允许读取,发送方只有看到已释放的读指针才重用空间。这个所有权协议是数据一致性的基础,物理时序约束仍需另行审查。
逐行理解满空与预取逻辑
RAM 深度固定为十六,地址使用四位,完整指针增加一位环回标记。满判断使用下一写 Gray 与同步读 Gray 最高两位取反后的比较,空判断使用下一读 Gray 与同步写 Gray 比较。读指针在预取到输出槽时前进,而不是等下游消费才前进,因此 RAM 空与输出有效可以同时成立;解释波形时必须区分这两个状态。
| 项目 | 工程含义 |
|---|---|
| push | s_valid 且 s_ready,写指针推进 |
| fetch | RAM 非空,且输出槽为空或本拍被消费 |
| empty=1、m_valid=1 | 合法:RAM 排空但输出槽仍有一拍 |
| 总容量 | 最多 17 拍已接受但未消费 |
| 同步延迟 | 满空撤销是保守的,不能用异域瞬时指针反算容量 |
完整手写教学 FIFO;固定 16 个 RAM 槽位,须配套本课程约束和复位契约。
`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
用长暂停与环回测试验证边界
长暂停场景让接收端在开始八十个读周期里不接受数据,测试在暂停期间检查已接受拍数恰好为十七,并确认写端实际经历满反压。随后恢复接收,比较所有拍并检查排空后没有额外输出。四千多拍让有限位宽指针多次环回,能揭示只在第一次填充时看不出来的满空和环回错误。
- 观察指针
预期与判定:确认二进制地址只用于本域 RAM 访问。
- 填满队列
预期与判定:暂停接收后 RAM 与输出槽合计 17 拍。
- 恢复并环回
预期与判定:源被正确反压,恢复后逐拍无丢失或重复。
- 检查排空
预期与判定:最后一拍消费后 m_valid 清零,无额外拍。
在解压后的 fpga-stream 目录执行;软件测试需要 Python 3、iverilog 和 vvp。
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 的单比特断言。
延伸阅读与来源
本文为黄河电子原创教学说明。产品命令与支持范围请以实际安装版本的官方文档为准。
- Arm IHI0051A · AXI4-Stream Protocol
握手、字节限定与包尾;本文工程与测试为原创实现。
- AMD UG903 · CDC Constraints
跨域最大延时与总线偏斜约束。
- AMD UG474 · Distributed RAM Timing
分布式 RAM 的写入与异步读取时序特性。
- AMD UG908 · Programming and Debugging
ILA、探针文件与采集流程。
继续阅读
FPGA从 RTL 到板上运行的完整流程
建立约束、实现、下载与观测的最小工程闭环。
第一个工程的引脚与时钟约束
区分电气连接与时间要求,避免“能生成位流就算正确”。
如何读懂时序报告
从起点、终点、时钟和裕量找出失败路径的原因。
黄河电子