《PCIe 進階之路》第 01 篇 · 不背協議,沿著真實數據路徑學 PCIe 公眾號:芯片進階之路 · 知乎專欄:芯片設計進階之路 · 作者:烓圍瑋未
這篇文章不從 PCIe 三層協議開始背概念,而是只追一筆最普通的 CPU Store: 它怎樣從 SoC 內部的一次 AXI 寫,變成一筆 PCIe Memory Write,再落到另一顆芯片內部的寄存器。

假設軟件里有這樣一行代碼:
*(volatile unsigned int *)0x40001234 = 0x55AA;
如果 0x4000_1234 這段地址不是 DDR,也不是片上 UART、SPI,而是被系統映射成了一個 PCIe Root Complex 的訪問窗口,那么這條看起來再普通不過的 Store 指令,最終可能會讓板子另一端一顆 PCIe Endpoint 芯片中的某個寄存器變成 0x55AA。
這里真正值得想明白的問題是:
CPU 寫的是本地地址,為什么最后能改到另一顆芯片里的寄存器?
很多 PCIe 概念——BAR、ATU、TLP、Memory Write、AXI、Controller、PHY——其實都可以沿著這一個問題串起來。
先看完整路徑。

如果先忽略所有協議細節,整條鏈路可以壓縮成:
CPU / 軟件
↓
MMU
↓
NoC / AXI
↓
PCIe Root Complex
↓
Outbound 地址轉換
↓
Memory Write TLP
↓
PCIe Link
↓
Endpoint Controller
↓
BAR 命中
↓
Inbound 地址轉換
↓
本地 AXI
↓
寄存器 / SRAM / 應用邏輯
理解這條路徑以后,PCIe 就不再是一堆零散名詞,而是一套連續的“事務翻譯系統”。
CPU 本身并不知道“我要發 PCIe”。
它只知道:
我要把數據
0x55AA寫到某個地址。
如果系統開啟了 MMU,那么 CPU 最開始使用的可能還是虛擬地址。經過頁表翻譯后,它得到一個 SoC 物理地址,然后交給片內 NoC / AXI Interconnect。
可以先建立這樣一個模型:
CPU Virtual Address
↓ MMU
SoC Physical Address
↓ NoC / Interconnect
DDR / PCIe / UART / SPI / ...
真正決定這筆訪問去 DDR,還是去 PCIe Controller 的,是 SoC 地址地圖和片內地址譯碼。
例如:
0x0000_0000 ~ 0x3FFF_FFFF → DDR
0x4000_0000 ~ 0x4000_FFFF → PCIe RC Window
0x5000_0000 ~ 0x5000_FFFF → 其他外設
這時 CPU 寫:
0x4000_1234 = 0x55AA
NoC 看見地址落在 PCIe Window,就把這筆 AXI 寫送到 PCIe Root Complex。
這里有一個很重要的工程直覺:
PCIe 訪問首先仍然是 SoC 內部的一筆地址訪問。
所以以后遇到“CPU 寫 PCIe 設備沒有反應”,不要一上來就查 PHY。
第一件事應該先確認:
這筆地址真的打到 PCIe Controller 了嗎?
這時 Controller 收到的是一個 SoC 本地地址,例如:
0x4000_1234
但 Endpoint 在 PCIe 地址空間里對應的地址可能是:
0x8000_1234
這兩個地址顯然不是一個東西。
因此,大多數 SoC / PCIe Controller 實現中都會存在某種 Outbound Address Translation 機制。不同廠商名字可能不同,常見實現會叫:
本質都是一回事:
把本地 SoC 地址空間,映射到 PCIe 地址空間。
看一個最簡單的例子。

假設:
RC 本地 PCIe Window:
0x4000_0000 ~ 0x4000_FFFF
Outbound Target:
0x8000_0000
EP BAR0:
0x8000_0000,大小 64 KB
EP 本地目標地址:
0x2000_0000
CPU 寫:
0x4000_1234
Outbound 地址轉換后:
PCIe Address
= 0x8000_0000
+ (0x4000_1234 - 0x4000_0000)
= 0x8000_1234
于是,同一次訪問已經出現了兩個不同的地址:
CPU / AXI Address = 0x4000_1234
PCIe Address = 0x8000_1234
這就是很多初學者理解 PCIe 地址映射時最容易卡住的地方:
CPU 看到的地址,不一定就是 TLP 里真正攜帶的 PCIe 地址。
到這里為止,我們都還在 SoC 內部。
Controller 收到一筆 AXI 寫后,需要把它“翻譯”成 PCIe 能理解的事務。
對于“CPU 寫 Endpoint 的 BAR Memory Space”這個場景,最典型的是:
Memory Write TLP
簡稱:MWr
Controller 會根據這筆本地事務,構造出 PCIe Transaction Layer Packet。
一個 MWr 至少需要表達:
可以粗略理解為:
AXI Write
Address = 0x4000_1234
Data = 0x0000_55AA
↓
Outbound Translation
↓
PCIe Memory Write TLP
Address = 0x8000_1234
Data = 0x0000_55AA
這里必須建立一個非常清晰的邊界:
AXI 到 Controller 為止。
PCIe 鏈路上不會直接傳:
AWADDR
AWVALID
WVALID
BRESP
這些都是片內 AXI 協議的概念。
Controller 對外送出去的是 PCIe 事務。
這是理解 PCIe 事務類型時非常關鍵的一點。
普通 Memory Write 屬于:
Posted Request
也就是說:
Requester ───────── MWr ─────────→ Completer
正常情況下不會再返回一個 PCIe Completion。
對比 Memory Read:
Requester ───────── MRd ─────────→ Completer
Requester ←──────── CplD ───────── Completer

為什么寫操作這樣設計?
因為如果每一筆 Memory Write 都必須返回 Completion,那么高吞吐寫流量會額外產生大量響應包。
PCIe 選擇了另一種設計:
Memory Write
→ Posted
→ 正常情況下不返回 Completion
而:
Memory Read
→ Non-Posted
→ 必須返回 Completion with Data
這里再強調一個特別容易混淆的點:
PCIe 沒有返回 Completion,不代表 Endpoint 內部沒有 AXI B Response。
Endpoint 內部如果最終把這筆寫轉換成本地 AXI Write,AXI Slave 仍然可能返回 BRESP。
但:
AXI B Response
≠
PCIe Completion
它們屬于完全不同的協議層級。
TLP 生成以后,還要繼續經過 Data Link Layer 和 Physical Layer。
所以真正跨過板級鏈路的是:
TLP
↓
Data Link Layer 處理
↓
Physical Layer 處理
↓
高速差分信號
而不是:
valid / ready
對做 SoC 集成的人來說,可以先把幾個接口粗暴地分成:
AXI / Memory Window
→ SoC 內部普通數據訪問
ECAM
→ 遠端 PCIe 配置空間訪問
DBI(部分 Controller 廠商常用叫法)
→ 本地 Controller 配置/狀態訪問
APB
→ 本地 PHY / PCS 等低速配置
PIPE
→ Controller 與 PCS / PHY 之間的專用接口
需要注意:
DBI、APB、ATU 等具體名字和接口形式會因 IP 廠商而不同,但“本地控制面、遠端配置面、正常數據面、PHY 接口”這幾個邏輯邊界是通用的。
現在這筆 MWr 已經到了 Endpoint。
TLP 里面帶著:
PCIe Address = 0x8000_1234
Endpoint 不能直接把這個地址送進自己的 AXI。
它首先要判斷:
這個地址到底是不是屬于我這個 Function?
這里就輪到 BAR 登場了。
假設:
BAR0 Base = 0x8000_0000
BAR0 Size = 64 KB
那么:
0x8000_1234
落在 BAR0 范圍內。
于是 Controller 知道:
這是一筆命中 BAR0 的 Memory Request。
接下來再做 Inbound 地址轉換。
例如:
PCIe Address = 0x8000_1234
BAR0 Base = 0x8000_0000
得到 BAR 內偏移:
Offset = 0x1234
如果 Endpoint 本地映射目標是:
0x2000_0000
那么最終本地 AXI 地址就是:
0x2000_1234
所以這筆事務完整地經歷了:
CPU Address : 0x4000_1234
↓
PCIe Address : 0x8000_1234
↓
EP AXI Address: 0x2000_1234
這三個地址完全可以不同。
但它們代表的是同一次業務訪問。
經過 BAR 命中和 Inbound Translation 后,Endpoint Controller 最終可以在本地側重新生成一筆 AXI 寫。
例如:
AXI Address = 0x2000_1234
AXI Data = 0x55AA
如果這個地址對應一個控制寄存器,那么寄存器最終被更新。
到這里,一筆 CPU Store 才真正完成了我們關心的數據路徑:

把全文壓縮成一句話:
CPU 的本地地址訪問,被 RC 翻譯成 PCIe Memory Write TLP;TLP 經過鏈路到達 EP 后,再由 BAR 和 Inbound Translation 重新落成本地 AXI 訪問。
這就是 PCIe Controller 做的最核心工作之一:
把兩個不同芯片內部的本地總線事務,通過 PCIe Transaction 連接起來。
因為“CPU 寫了但對端沒反應”并不是一個問題。
它可能是七個完全不同的問題。

建議以后固定按這條路徑查:
檢查:
如果地址地圖錯了,訪問甚至不會進入 PCIe Controller。
重點看:
檢查:
這時才開始看:
重點看:
最后再看:
真正高效的 PCIe Debug 方法不是:
猜“可能是 PHY”“可能是 ATU”。
而是:
跟著一筆事務,看它到底消失在哪一級。
不會。
AXI 是 SoC 內部總線協議。
跨 PCIe Link 傳的是 PCIe 協議數據。
PHY 不負責理解:
這些屬于 Controller / PCIe 協議邏輯。
PHY 更關心:
不準確。
BAR 首先是 Configuration Space 中的 Base Address Register。
它描述/保存的是:
這個 PCIe Function 暴露的一段地址窗口。
真正的 SRAM、寄存器或 DDR,可以位于這段 BAR Window 后面。
Memory Write 不返回事務層 Completion。
但 PCIe 鏈路本身仍然有自己的可靠傳輸機制。
因此:
“沒有 Completion”
≠
“鏈路不做錯誤檢測和重傳”
這是事務層和數據鏈路層職責不同導致的。
如果鏈路已經穩定 Link Up,那么大量“訪問不生效”問題其實發生在:
Address Map
ATU
TLP Generation
BAR
Inbound Translation
AXI
Register
PHY 只是整條路徑的一部分。

以后看到一句:
*(volatile uint32_t *)addr = data;
如果 addr 對應 PCIe Window,腦子里應該自動展開成:
CPU Store
↓
MMU
↓
NoC / AXI
↓
PCIe RC Window
↓
Outbound Translation
↓
Memory Write TLP
↓
PCIe Link
↓
Endpoint BAR Hit
↓
Inbound Translation
↓
Local AXI
↓
Register / SRAM / DDR
這張圖比死記幾十個 PCIe 名詞更重要。
因為后面我們學習:
其實都只是在逐漸把這條主路徑展開。
技術很重要,技術背后的思想更重要。
PCIe 最值得理解的,并不是“規范里有多少種包”,而是:
不同芯片內部有各自獨立的地址空間和總線協議,PCIe Controller 如何把一邊的本地事務,轉換成一筆可以跨芯片傳輸的協議事務,再在另一端重新還原成本地訪問。
一旦這條主線建立起來,很多原本零散的知識點就會自動找到自己的位置。
下一篇繼續沿著這條路徑:
Memory Write 不需要 Completion,但 Memory Read 必須等數據回來。
那么:
下一篇繼續。