首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >PCIe bvalid 少保持了一拍,整条 AXI 总线当场去世

PCIe bvalid 少保持了一拍,整条 AXI 总线当场去世

作者头像
FPGA技术江湖
发布2026-07-13 20:35:22
发布2026-07-13 20:35:22
00
举报

摘要: 一次代码小改动后,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 波形之后。

正常版本——写操作
代码语言:javascript
复制
时钟拍  bvalid  bready
  511     1       0     ← bvalid 拉高, bready 还没来
  512     1       1     ← bvalid 保持住了, bready 到了, 握手成功
  513     0       0     ← 写事务正常完成
死锁版本——写操作
代码语言:javascript
复制
时钟拍  bvalid  bready
  512     1       0     ← bvalid 拉高, bready 还没来
  513     0       1     ← bvalid 已经没了! bready 才到, 扑了个空

两个版本里 bready 都是晚来 1 拍。这是 AXI 互联的正常行为,完全合理。

差异在于 bvalid:

  • 正常版本:保持住了等到 bready 来了才拉低
  • 死锁版本:只冒了 1 拍的头bready 还没来就缩回去了

bvalid 和 bready 永远错开,B 通道的握手永远完不成。

然后,连锁反应开始了。


多米诺骨牌:从 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

一个写响应信号少保持了一拍,整条总线就死了。


元凶:那个 else 分支

翻出死锁版本的 bvalid 控制逻辑:

代码语言:javascript
复制
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 也为高(握手完成)后才能拉低。

修复只需要改一行:

代码语言:javascript
复制
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 特别阴险

回头想想,这个 bug 能藏这么久,有四个原因:

第一,症状和根因隔着两个通道。

你看到的是读超时,本能反应是查读通道。读通道一切正常,你就开始怀疑人生。能想到去查写通道的 bvalid,需要对 AXI 互联的内部机制有深入理解。

第二,bvalid 确实拉高过。

它不是没响应,而是响应了但没保持住。如果你在 ILA 里只看 trigger 帧附近几个点,很可能以为"bvalid 正常拉高过了",不会注意到它只保持了 1 拍。

第三,代码"看起来"没问题。

else if (~bvalid) 保持、else 清零——结构工整,逻辑清晰,review 的时候很容易放过。它的问题不在代码结构,而在对协议语义的理解。

第四,仿真大概率抓不到。

如果你的 testbench 用的 BFM 是 bready 立即拉高(很多简化的 BFM 就是这样),那 bvalid 保持 1 拍完全够用,仿真永远通过。只有真实互联的 bready 有延迟时,bug 才会暴露。


AXI 握手三条铁律

从这个 bug 出发,重新审视 AXI 协议,有三条规则是绝对不能违反的:

铁律一:VALID 拉高后,必须保持到 READY 也为高。

这是本次 bug 的直接原因。适用于所有五个通道。违反后果:握手失败,死锁。

铁律二:VALID 不能等 READY 来了才拉。

也就是说,不能写 assign rvalid = rready & data_ready 这种东西。VALID 必须独立于 READY 产生。违反后果:组合环路,活锁。

铁律三:VALID 为高期间,数据必须稳定。

rvalid 为 1 时 rdata 不能变,awvalid 为 1 时 awaddr 不能变。违反后果:数据损坏。

三条规则,五个通道,没有例外。


自查清单:你的代码安全吗

如果你的工程里有手写的 AXI slave 逻辑,建议现在就做一次自查。

必查项(致命级别):

搜索代码中所有 bvalidrvalidawvalidwvalidarvalid 的赋值逻辑。每一个将 VALID 拉低的分支,必须valid & ready 作为条件。如果你看到了不带 ready 的 else 分支直接清零——恭喜你,找到了一颗定时炸弹。

错误写法:

代码语言:javascript
复制
// 无条件清零
else
    bvalid <= 1'b0;

// 变体:看起来有保持逻辑,实际只保持 1 拍
else if (~bvalid)
    bvalid <= bvalid;
else
    bvalid <= 1'b0;

正确写法:

代码语言:javascript
复制
else if (bvalid & bready)
    bvalid <= 1'b0;

建议项(隐患级别):

  • assign wready = wvalid :合法,但意味着零延迟握手、没有反压能力,可能引发时序问题
  • 读通道完全依赖外部模块回应 rd_vld,没有超时兜底:一旦外部模块不响应某个地址,读通道永久死锁
  • 地址在 W 握手而非 AW 握手时采样:可能采到过期地址

最稳妥的方案:别手搓

正常版本为什么不出问题?因为它用了成熟的 axil_ram IP 来管理所有握手信号。用户逻辑只需要覆盖 rdata 和 rvalid 的输出,awready、wready、bvalid、arready 全部交给 IP 处理。

如果你一定要手搓 AXI 握手状态机,三个建议:

  1. 写完后做一次专项 review,只看 VALID 信号的拉低条件
  2. 仿真时给所有 READY 信号加 1~4 拍的随机延迟,别用"立即响应"的 BFM
  3. 跑我们写的 AXI 协议检查脚本,自动扫描常见违规模式

工具:AXI 协议静态检查脚本

踩完坑之后我们写了一个 Python 脚本,可以对 Verilog 代码做静态扫描,自动检测常见的 AXI 协议违规:

代码语言:javascript
复制
# 扫描单个文件
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 的原始代码跑一遍,输出:

代码语言:javascript
复制
[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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FPGA技术江湖 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 现象:数据到了门口,门却打不开
  • 真凶不在读通道,在写通道
    • 正常版本——写操作
    • 死锁版本——写操作
  • 多米诺骨牌:从 B 通道到整条总线
  • 元凶:那个 else 分支
  • 为什么这种 bug 特别阴险
  • AXI 握手三条铁律
  • 自查清单:你的代码安全吗
  • 最稳妥的方案:别手搓
  • 工具:AXI 协议静态检查脚本
  • 最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档