
摘要: 一次代码小改动后,PCIe 读操作全部超时返回 0xFFFFFFFF。ILA 一看,读数据明明准备好了。排查半天发现:问题根本不在读通道,而是写通道的 bvalid 少保持了一个时钟周期,通过 AXI 互联的跨通道依赖,像多米诺骨牌一样把整条总线拽进了死锁。
那天下午,我改了一版 AXI-Lite 的 slave 握手逻辑,综合、上板、跑测试。
终端刷出一行 0xFFFFFFFF。
所有寄存器读出来都是 0xFFFFFFFF。
PCIe 超时的经典症状。
我换回老版本的代码,一切正常。再换回新版本,又是满屏 0xFFFFFFFF。
硬件没动过,综合没报错,区别只在一个文件的几行 Verilog。
到底哪里写错了?
工程背景是一个基于 PCIe 的网卡测试平台,FPGA 是 Xilinx Kintex-7 325T,主机通过 MMIO(也就是 AXI-Lite)访问 FPGA 内部寄存器。
插上 ILA,抓到的读操作现象让人困惑:
第一步,主机发起 AXI-Lite 读请求,arvalid 和 arready 成功握手。没问题。
第二步,rd_en 脉冲送到外部模块,rd_vld_app 正常回应,rdata 收到了正确数据 0x00000009。没问题。
第三步,rvalid 拉高了。
第四步,rready 一直是 0。
rready 是上游 AXI 互联给我的,不是我控制的信号。
数据已经送到了家门口,但门永远不会打开。
为什么?
直觉会把你引向读通道,毕竟出问题的是读操作。但你在读通道上翻来覆去也找不到毛病——因为读通道确实没问题。
转机出现在我对比了两个版本的写通道 ILA 波形之后。
时钟拍 bvalid bready
511 1 0 ← bvalid 拉高, bready 还没来
512 1 1 ← bvalid 保持住了, bready 到了, 握手成功
513 0 0 ← 写事务正常完成

时钟拍 bvalid bready
512 1 0 ← bvalid 拉高, bready 还没来
513 0 1 ← bvalid 已经没了! bready 才到, 扑了个空

两个版本里 bready 都是晚来 1 拍。这是 AXI 互联的正常行为,完全合理。
差异在于 bvalid:
bvalid 和 bready 永远错开,B 通道的握手永远完不成。
然后,连锁反应开始了。
你可能会问:bvalid 是写响应通道的信号,为什么会影响读通道的 rready?
这就涉及到 AXI 互联(Interconnect)内部一个很多人不了解的机制——跨通道资源依赖。
AXI 互联内部维护着事务追踪资源。一笔写事务发出去,互联会分配一个内部槽位来追踪它,直到 B 通道握手完成(bvalid & bready 都为 1)才释放。
当 B 握手永远完不成时,这个槽位永远不释放。槽位满了,互联就没有资源去处理后续的读事务——于是 rready 被压制为 0。
完整的死锁链条:
bvalid 只保持 1 拍就拉低(协议违规) → bready 晚来 1 拍(互联正常行为) → B 通道握手永远完不成 → 互联认为写事务仍在等待响应,内部槽位被占用 → 互联资源锁定,rready 被压制为 0 → rvalid = 1 但 rready = 0,读数据握手完不成 → arready 无法恢复,后续所有读写全部卡死 → PCIe 主机超时,返回 0xFFFFFFFF
一个写响应信号少保持了一拍,整条总线就死了。
翻出死锁版本的 bvalid 控制逻辑:
always @(posedge clk or posedge rst) begin
if (rst)
s_axil_bvalid <= 1'b0;
else if (wvalid & wready)
s_axil_bvalid <= 1'b1;
else if (~s_axil_bvalid)
s_axil_bvalid <= s_axil_bvalid;
else
s_axil_bvalid <= 1'b0; // ← 就是这一行
end
最后那个 else 分支:当 bvalid 为 1 时,不管 bready 是不是 1,下一拍直接拉低。
来理一下这个 if-else 的执行路径:
rst:复位清零,没问题wvalid & wready:写握手完成,bvalid 拉高,没问题~s_axil_bvalid:bvalid 为 0 时保持 0,没问题else:到这里意味着 bvalid 为 1 且没有新的写握手——无条件拉低写这段代码的人大概觉得"bvalid 拉高 1 拍就够了,对面肯定能接住"。
但 AXI 协议白纸黑字写着:不行。
AXI 规范 A3.2.1:一旦 VALID 信号拉高,必须保持到对应的 READY 也为高(握手完成)后才能拉低。
修复只需要改一行:
always @(posedge clk or posedge rst) begin
if (rst)
s_axil_bvalid <= 1'b0;
else if (wvalid & wready)
s_axil_bvalid <= 1'b1;
else if (s_axil_bvalid & bready)
s_axil_bvalid <= 1'b0; // 握手完成才拉低
end
把 else 改成 else if (bvalid & bready)。就这么简单。
回头想想,这个 bug 能藏这么久,有四个原因:
第一,症状和根因隔着两个通道。
你看到的是读超时,本能反应是查读通道。读通道一切正常,你就开始怀疑人生。能想到去查写通道的 bvalid,需要对 AXI 互联的内部机制有深入理解。
第二,bvalid 确实拉高过。
它不是没响应,而是响应了但没保持住。如果你在 ILA 里只看 trigger 帧附近几个点,很可能以为"bvalid 正常拉高过了",不会注意到它只保持了 1 拍。
第三,代码"看起来"没问题。
else if (~bvalid) 保持、else 清零——结构工整,逻辑清晰,review 的时候很容易放过。它的问题不在代码结构,而在对协议语义的理解。
第四,仿真大概率抓不到。
如果你的 testbench 用的 BFM 是 bready 立即拉高(很多简化的 BFM 就是这样),那 bvalid 保持 1 拍完全够用,仿真永远通过。只有真实互联的 bready 有延迟时,bug 才会暴露。
从这个 bug 出发,重新审视 AXI 协议,有三条规则是绝对不能违反的:
铁律一:VALID 拉高后,必须保持到 READY 也为高。
这是本次 bug 的直接原因。适用于所有五个通道。违反后果:握手失败,死锁。
铁律二:VALID 不能等 READY 来了才拉。
也就是说,不能写 assign rvalid = rready & data_ready 这种东西。VALID 必须独立于 READY 产生。违反后果:组合环路,活锁。
铁律三:VALID 为高期间,数据必须稳定。
rvalid 为 1 时 rdata 不能变,awvalid 为 1 时 awaddr 不能变。违反后果:数据损坏。
三条规则,五个通道,没有例外。
如果你的工程里有手写的 AXI slave 逻辑,建议现在就做一次自查。
必查项(致命级别):
搜索代码中所有 bvalid、rvalid、awvalid、wvalid、arvalid 的赋值逻辑。每一个将 VALID 拉低的分支,必须以 valid & ready 作为条件。如果你看到了不带 ready 的 else 分支直接清零——恭喜你,找到了一颗定时炸弹。
错误写法:
// 无条件清零
else
bvalid <= 1'b0;
// 变体:看起来有保持逻辑,实际只保持 1 拍
else if (~bvalid)
bvalid <= bvalid;
else
bvalid <= 1'b0;
正确写法:
else if (bvalid & bready)
bvalid <= 1'b0;
建议项(隐患级别):
assign wready = wvalid :合法,但意味着零延迟握手、没有反压能力,可能引发时序问题正常版本为什么不出问题?因为它用了成熟的 axil_ram IP 来管理所有握手信号。用户逻辑只需要覆盖 rdata 和 rvalid 的输出,awready、wready、bvalid、arready 全部交给 IP 处理。
如果你一定要手搓 AXI 握手状态机,三个建议:
踩完坑之后我们写了一个 Python 脚本,可以对 Verilog 代码做静态扫描,自动检测常见的 AXI 协议违规:
# 扫描单个文件
python axi_protocol_checker.py your_slave.v
# 扫描整个 RTL 目录
python axi_protocol_checker.py ./rtl/
# 导出 Markdown 报告
python axi_protocol_checker.py ./rtl/ -o report.md
支持 8 条规则,覆盖 VALID 保持、VALID 依赖 READY、复位状态、数据稳定性、地址采样时序等。
对本次 bug 的原始代码跑一遍,输出:
[FATAL] [R1] L26 (s_axil_bvalid)
s_axil_bvalid 在 else 分支中无条件拉低,
未等待 bready 握手完成。
修复后再跑,FATAL 消失。
。
一个 else 分支里少了一个 & bready 的条件。
就这么一个条件,让 bvalid 少保持了 1 拍。
这 1 拍,让 B 通道握手永远完不成。
B 通道完不成,AXI 互联的内部事务槽被永久占用。
事务槽满了,rready 被压制为 0。
rready 为 0,读数据握手完不成。
读握手完不成,arready 恢复不了。
所有后续读写全部卡死。
PCIe 超时,系统挂死。
一拍之差,满盘皆输。
AXI 协议不是建议,是契约。你违反了契约,总线不会给你报错——它只会用死锁来惩罚你。而且惩罚的方式,往往让你完全猜不到问题出在哪里。
希望这篇文章能帮你少踩一个坑。
在 FPGA 这个领域,一个人踩过的坑,不该让所有人再踩一遍。
如果觉得有用,转发给你身边还在手搓 AXI 状态机的同事。说不定能救他一个下午。
END