摘要: 一次代碼小改動后,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 消失。
工具開源,拿走不謝。后臺回復 "AXI檢查" 獲取源碼和完整的協議規范檢查文檔。
一個 else 分支里少了一個 & bready 的條件。
就這么一個條件,讓 bvalid 少保持了 1 拍。
這 1 拍,讓 B 通道握手永遠完不成。
B 通道完不成,AXI 互聯的內部事務槽被永久占用。
事務槽滿了,rready 被壓制為 0。
rready 為 0,讀數據握手完不成。
讀握手完不成,arready 恢復不了。
所有后續讀寫全部卡死。
PCIe 超時,系統掛死。
一拍之差,滿盤皆輸。
AXI 協議不是建議,是契約。你違反了契約,總線不會給你報錯——它只會用死鎖來懲罰你。而且懲罰的方式,往往讓你完全猜不到問題出在哪里。
希望這篇文章能幫你少踩一個坑。
在 FPGA 這個領域,一個人踩過的坑,不該讓所有人再踩一遍。
如果覺得有用,轉發給你身邊還在手搓 AXI 狀態機的同事。說不定能救他一個下午。