戰(zhàn)調(diào)試)
PCIe 這玩意兒剛接觸的時(shí)候最容易讓人懵圈的不是那些高速信號(hào)、鏈路訓(xùn)練反而是軟件層面那兩個(gè)看起來平平無奇的東西配置空間和 BAR 空間。我見過不少做 FPGA 的兄弟邏輯寫得飛起DMA 也能跑通但一到主機(jī)識(shí)別不到設(shè)備、或者 BAR 地址映射錯(cuò)亂就抓瞎了。問題往往就出在對(duì)這兩個(gè)空間的機(jī)制理解得不夠透。這篇就專門把配置空間和 BAR 空間掰開揉碎講清楚它們各自裝了什么、為什么這么設(shè)計(jì)、實(shí)際調(diào)試中怎么用以及那些文檔里不會(huì)寫的坑。1. 為什么 PCIe 需要配置空間和 BAR 這兩套地址體系要理解配置空間和 BAR得先回到 PCIe 設(shè)計(jì)的一個(gè)根本矛盾上。主機(jī) CPU 訪問內(nèi)存是按物理地址走的而 PCIe 設(shè)備自己內(nèi)部也有一堆寄存器、緩沖區(qū)、控制邏輯這些東西也需要被 CPU 訪問。問題是設(shè)備插到哪個(gè)槽位、系統(tǒng)里掛了多少設(shè)備這些在開機(jī)之前都是未知的。你不可能給每個(gè)設(shè)備預(yù)先分配一段固定的物理地址那樣地址空間早就沖突了。所以 PCIe 采用了一套先枚舉、后分配的機(jī)制。配置空間就是設(shè)備的身份證加簡(jiǎn)歷里面記錄了廠商 ID、設(shè)備 ID、類型、需要多大地址空間等固定信息。主機(jī)在枚舉階段掃描總線讀每個(gè)設(shè)備的配置空間搞清楚你是誰、你要多少資源然后統(tǒng)一分配地址。BAR 空間則是設(shè)備向主機(jī)申請(qǐng)的地盤主機(jī)分配好地址后把基地址寫回 BAR 寄存器設(shè)備就知道哦原來我的寄存器在主機(jī)眼里是這個(gè)地址。這套機(jī)制的核心價(jià)值在于地址無關(guān)性。設(shè)備不需要知道自己會(huì)被映射到哪個(gè)地址它只需要聲明我需要 4KB 的寄存器空間和我需要 16MB 的顯存空間剩下的交給主機(jī)。這也是為什么同一塊 FPGA 板卡插到不同主板上都能正常工作因?yàn)榈刂肥莿?dòng)態(tài)分配的。從拓?fù)浣Y(jié)構(gòu)上看PCIe 是一棵樹根復(fù)合體Root Complex是根下面掛交換機(jī)Switch和端點(diǎn)設(shè)備Endpoint。配置空間和 BAR 的分配是沿著這棵樹逐級(jí)進(jìn)行的。每個(gè)橋設(shè)備包括交換機(jī)端口也有自己的配置空間負(fù)責(zé)管理下游總線的地址窗口。理解這個(gè)層級(jí)關(guān)系對(duì)后面排查 BAR 分配失敗非常關(guān)鍵。很多人把配置空間和 BAR 空間混為一談其實(shí)它們是兩個(gè)完全不同的地址域。配置空間通過 CFG 事務(wù)訪問走的是獨(dú)立的配置讀寫通道BAR 空間則是通過 MEM 或 IO 事務(wù)訪問走的是正常的內(nèi)存映射通道。這個(gè)區(qū)別在調(diào)試時(shí)非常重要。2. 配置空間里到底裝了哪些東西配置空間是 PCIe 設(shè)備的標(biāo)準(zhǔn)寄存器區(qū)域規(guī)范定義每個(gè)功能Function必須有至少 256 字節(jié)的配置空間PCIe 還擴(kuò)展到了 4KB。這 4KB 不是隨便堆的而是按固定偏移劃分成一個(gè)個(gè)有明確用途的字段。理解這些字段的布局是讀懂枚舉日志和排查識(shí)別問題的前提。2.1 前 64 字節(jié)設(shè)備身份與命令控制配置空間的前 64 字節(jié)是所有 PCIe 設(shè)備都必須實(shí)現(xiàn)的這部分叫 Type 0 頭對(duì)于端點(diǎn)設(shè)備或 Type 1 頭對(duì)于橋設(shè)備。里面最關(guān)鍵的幾個(gè)字段Vendor ID 和 Device ID偏移 0x00 和 0x02廠商 ID 由 PCI-SIG 統(tǒng)一分配設(shè)備 ID 由廠商自己定義。主機(jī)枚舉時(shí)第一件事就是讀這兩個(gè)值如果是 0xFFFF說明這個(gè)位置沒有設(shè)備或者設(shè)備沒準(zhǔn)備好。Command 寄存器偏移 0x04控制設(shè)備是否響應(yīng) MEM 訪問、IO 訪問、總線主控等。很多新手遇到設(shè)備能識(shí)別但訪問不了 BAR的問題十有八九是 Command 寄存器里的 Memory Space Enable 位沒置起來。Status 寄存器偏移 0x06反映設(shè)備狀態(tài)比如是否支持能力列表、是否有錯(cuò)誤等。Revision ID 和 Class Code偏移 0x08 和 0x09Revision 是版本號(hào)Class Code 標(biāo)識(shí)設(shè)備類型比如 0x020000 是以太網(wǎng)控制器0x010802 是 NVMe 存儲(chǔ)控制器。操作系統(tǒng)就是靠 Class Code 來加載對(duì)應(yīng)驅(qū)動(dòng)的。Header Type偏移 0x0E標(biāo)識(shí)這是 Type 0 還是 Type 1 頭以及是否是多功能設(shè)備。BAR0 到 BAR5偏移 0x10 到 0x24六個(gè)基地址寄存器這是配置空間和 BAR 空間的交匯點(diǎn)后面單獨(dú)展開講。Capabilities Pointer偏移 0x34指向能力列表的偏移PCIe 的各種高級(jí)功能都掛在能力列表里。我實(shí)際調(diào)試中遇到最多的情況是FPGA 加載了錯(cuò)誤的比特流Vendor ID 和 Device ID 讀出來是默認(rèn)值或者全 F主機(jī)直接判定為無效設(shè)備。所以每次上電先確認(rèn)這兩個(gè) ID 是否正確是最基本的排查動(dòng)作。2.2 能力列表PCIe 的高級(jí)功能入口從偏移 0x34 開始配置空間里掛了一條鏈表叫能力列表Capability List。每個(gè)能力項(xiàng)都有一個(gè) 8 位的 Capability ID 和一個(gè)指向下一項(xiàng)的指針。PCIe 設(shè)備必須實(shí)現(xiàn)的能力包括Power Management CapabilityID 0x01電源管理相關(guān)控制設(shè)備在不同電源狀態(tài)間切換。MSI CapabilityID 0x05消息信號(hào)中斷替代傳統(tǒng)的 INTx 中斷。現(xiàn)代 PCIe 設(shè)備基本都用 MSI 或 MSI-X。PCI Express CapabilityID 0x10這是 PCIe 特有的包含設(shè)備類型、鏈路狀態(tài)、鏈路能力等信息。調(diào)試鏈路降速、降寬問題時(shí)就是讀這里的 Link Status 寄存器。MSI-X CapabilityID 0x11支持更多中斷向量NVMe 和高速網(wǎng)卡常用。能力列表的遍歷方式是從 Capabilities Pointer 開始讀第一個(gè)能力的 ID 和 Next Pointer然后順著指針一直走直到 Next Pointer 為 0。這個(gè)遍歷邏輯在寫驅(qū)動(dòng)或者調(diào)試工具時(shí)經(jīng)常用到。2.3 擴(kuò)展配置空間4KB 里的后半部分PCIe 把配置空間從 256 字節(jié)擴(kuò)展到了 4KB前 256 字節(jié)保持和 PCI 兼容后面的 3.75KB 是 PCIe 擴(kuò)展配置空間。這里最重要的是擴(kuò)展能力列表Extended Capability List從偏移 0x100 開始每個(gè)擴(kuò)展能力有 16 位的 ID。常見的擴(kuò)展能力包括Advanced Error ReportingAER高級(jí)錯(cuò)誤報(bào)告能精確報(bào)告是哪種錯(cuò)誤、發(fā)生在哪個(gè)層級(jí)。排查掉卡、鏈路錯(cuò)誤時(shí)必看。Secondary PCI Express Extended Capability包含鏈路均衡相關(guān)的寄存器調(diào)試鏈路穩(wěn)定性時(shí)用得到。SR-IOV Capability單根 IO 虛擬化網(wǎng)卡和 FPGA 加速卡常用。TPHTLP Processing Hints提示 TLP 的處理方式對(duì)性能優(yōu)化有幫助。訪問擴(kuò)展配置空間需要用 PCIe 的配置事務(wù)傳統(tǒng)的 CF8/CFC 端口只能訪問前 256 字節(jié)。在 Linux 下可以用lspci -vvv看到擴(kuò)展能力的詳細(xì)信息或者用setpci直接讀寫。3. BAR 空間設(shè)備向主機(jī)申請(qǐng)的地址地盤BAR 是 Base Address Register 的縮寫直譯就是基地址寄存器。它位于配置空間里但它的作用遠(yuǎn)不止存一個(gè)地址這么簡(jiǎn)單。BAR 是設(shè)備和主機(jī)之間關(guān)于地址空間的一份合同設(shè)備通過 BAR 聲明自己需要多大的空間、是什么類型的空間主機(jī)通過 BAR 告訴設(shè)備你的空間被映射到了這個(gè)地址。3.1 BAR 的探測(cè)機(jī)制寫全 1 再讀回BAR 的探測(cè)機(jī)制是 PCIe 里一個(gè)非常巧妙的設(shè)計(jì)。主機(jī)在枚舉時(shí)并不知道設(shè)備需要多大空間它的做法是向 BAR 寫入全 10xFFFFFFFF。讀回 BAR 的值。讀回的值中低位為 0 的位數(shù)就代表了空間大小的對(duì)齊要求。舉個(gè)例子如果一個(gè) BAR 讀回來是 0xFFFFF000說明低 12 位是 0設(shè)備需要 4KB 的空間。如果讀回來是 0xFF000000說明低 24 位是 0需要 16MB 空間。這個(gè)機(jī)制的好處是設(shè)備不需要額外的寄存器來聲明大小BAR 自己就能表達(dá)。這里有個(gè)細(xì)節(jié)容易踩坑BAR 的最低位表示空間類型。bit 0 為 1 表示 IO 空間為 0 表示 MEM 空間。MEM 空間里 bit 1 和 bit 2 表示是否支持 64 位地址和是否可預(yù)取。所以實(shí)際計(jì)算大小時(shí)要把這些控制位排除掉。比如一個(gè) 64 位 MEM BAR低 4 位是控制位從 bit 4 開始才是地址對(duì)齊信息。寫全 1 探測(cè) BAR 大小時(shí)一定要先保存原來的值探測(cè)完再恢復(fù)。有些設(shè)備的 BAR 在探測(cè)過程中如果被破壞可能導(dǎo)致設(shè)備進(jìn)入異常狀態(tài)。雖然規(guī)范說這是安全的但實(shí)際硬件實(shí)現(xiàn)千奇百怪謹(jǐn)慎為上。3.2 MEM BAR 和 IO BAR 的區(qū)別與選擇PCIe 支持兩種 BAR 類型MEM BAR 和 IO BAR。MEM BAR 映射到系統(tǒng)的內(nèi)存地址空間CPU 可以用普通的 load/store 指令訪問IO BAR 映射到 IO 地址空間需要專門的 IN/OUT 指令訪問?,F(xiàn)代 PCIe 設(shè)備幾乎都用 MEM BAR原因很簡(jiǎn)單IO 空間只有 64KB資源緊張而且訪問效率低。MEM 空間可以很大支持 64 位地址訪問方式統(tǒng)一。PCIe 規(guī)范雖然保留了 IO 事務(wù)但明確不推薦新設(shè)計(jì)使用。不過在實(shí)際項(xiàng)目中有些老舊的 FPGA 設(shè)計(jì)或者兼容性要求可能還會(huì)實(shí)現(xiàn) IO BAR。我的建議是除非有明確的兼容性需求否則一律用 MEM BAR而且優(yōu)先用 64 位 BAR。MEM BAR 還有一個(gè)屬性叫可預(yù)取Prefetchable。如果 BAR 聲明為可預(yù)取主機(jī)可以把它映射到帶緩存的內(nèi)存區(qū)域CPU 讀取時(shí)可以利用緩存提高性能。但可預(yù)取的前提是讀操作沒有副作用讀和寫之間沒有嚴(yán)格的順序依賴。對(duì)于 FIFO、狀態(tài)寄存器這類有副作用的地址絕對(duì)不能聲明為可預(yù)取否則會(huì)出現(xiàn)讀一次數(shù)據(jù)就丟一次的問題。3.3 64 位 BAR 的配對(duì)使用一個(gè) 64 位 BAR 需要占用兩個(gè)連續(xù)的 BAR 位置。比如 BAR0 和 BAR1 組成一個(gè) 64 位 BARBAR0 存低 32 位BAR1 存高 32 位。BAR0 的 bit 2 置 1 表示這是 64 位 BAR此時(shí) BAR1 不再是一個(gè)獨(dú)立的 BAR而是作為高 32 位地址寄存器。這個(gè)配對(duì)關(guān)系在寫驅(qū)動(dòng)和做地址映射時(shí)特別容易搞錯(cuò)。我見過有同事在設(shè)備樹里只寫了 BAR0 的地址結(jié)果高 32 位沒配訪問直接飛到錯(cuò)誤的內(nèi)存區(qū)域系統(tǒng)當(dāng)場(chǎng)掛掉。正確的做法是先讀 BAR0 判斷 bit 2 是否為 1如果是說明是 64 位 BAR需要同時(shí)處理 BAR0 和 BAR1。對(duì)于 FPGA 開發(fā)者來說在 IP 核里配置 BAR 時(shí)也要注意這個(gè)配對(duì)。Xilinx 的 XDMA IP 和 Intel 的 PCIe Hard IP 都有 BAR 配置選項(xiàng)選 64 位 BAR 時(shí)它會(huì)自動(dòng)占用兩個(gè) BAR 編號(hào)。4. 從枚舉到映射配置空間和 BAR 是怎么被主機(jī)處理的理解了配置空間和 BAR 各自的內(nèi)容接下來要把它們串起來看看主機(jī)從上電到設(shè)備可用到底經(jīng)歷了什么。這個(gè)過程叫枚舉Enumeration是 PCIe 系統(tǒng)啟動(dòng)的核心流程。4.1 枚舉的完整流程枚舉是從根復(fù)合體開始沿著 PCIe 樹逐級(jí)掃描的過程。大致步驟如下掃描總線 0根復(fù)合體首先掃描自己下面的總線 0讀取每個(gè)可能的設(shè)備號(hào)0 到 31和功能號(hào)0 到 7的配置空間。讀 Vendor ID如果讀回來的 Vendor ID 是 0xFFFF說明這個(gè)位置沒有設(shè)備跳過。如果是有效值說明發(fā)現(xiàn)了一個(gè)設(shè)備。判斷設(shè)備類型讀 Header Type如果是 Type 1說明是橋設(shè)備需要繼續(xù)掃描它下面的總線如果是 Type 0說明是端點(diǎn)設(shè)備。分配總線號(hào)對(duì)于橋設(shè)備主機(jī)分配一個(gè)總線號(hào)范圍給它下面的子樹。探測(cè) BAR 大小對(duì)每個(gè)端點(diǎn)設(shè)備寫全 1 探測(cè)每個(gè) BAR 需要多大空間。分配地址主機(jī)根據(jù)所有設(shè)備的需求統(tǒng)一分配 MEM 和 IO 地址空間把基地址寫回 BAR。使能設(shè)備設(shè)置 Command 寄存器使能 MEM 訪問、IO 訪問和總線主控。配置中斷分配中斷號(hào)配置 MSI/MSI-X。加載驅(qū)動(dòng)操作系統(tǒng)根據(jù) Class Code 和 Vendor/Device ID 匹配驅(qū)動(dòng)。這個(gè)過程在 Linux 下可以用lspci -vvv看到結(jié)果在 Windows 下可以用設(shè)備管理器查看資源分配情況。調(diào)試時(shí)如果設(shè)備沒被識(shí)別就要順著這個(gè)流程一步步查是 Vendor ID 沒讀到還是 BAR 分配失敗還是 Command 寄存器沒使能。4.2 地址窗口與橋的轉(zhuǎn)發(fā)規(guī)則PCIe 樹里的每個(gè)橋設(shè)備都有三個(gè)地址窗口寄存器Memory Base/Limit、Prefetchable Memory Base/Limit、IO Base/Limit。這些寄存器定義了橋下面子樹使用的地址范圍。當(dāng) CPU 發(fā)起一個(gè)內(nèi)存訪問時(shí)根復(fù)合體首先判斷這個(gè)地址落在哪個(gè)橋的窗口里然后把事務(wù)轉(zhuǎn)發(fā)給對(duì)應(yīng)的橋。橋再往下判斷直到到達(dá)目標(biāo)設(shè)備。如果地址不在任何窗口里事務(wù)就會(huì)被丟棄或者報(bào)錯(cuò)。這個(gè)機(jī)制解釋了為什么 BAR 分配失敗會(huì)導(dǎo)致設(shè)備完全無法訪問。如果主機(jī)沒有正確配置橋的地址窗口即使設(shè)備的 BAR 被分配了地址事務(wù)也到不了設(shè)備。我在調(diào)試一塊多級(jí)交換機(jī)的板卡時(shí)就遇到過交換機(jī)端口的 Memory Limit 設(shè)小了導(dǎo)致下游設(shè)備的 BAR 地址超出了窗口范圍設(shè)備能識(shí)別但訪問就報(bào)錯(cuò)。排查這類問題時(shí)可以用lspci -vvv查看每個(gè)橋的窗口配置對(duì)比設(shè)備的 BAR 地址是否落在窗口內(nèi)。也可以用setpci手動(dòng)調(diào)整窗口寄存器驗(yàn)證。4.3 Linux 下的資源分配與 sysfs 接口Linux 內(nèi)核在啟動(dòng)時(shí)會(huì)做一次完整的 PCIe 枚舉分配資源。如果 BIOS 已經(jīng)分配好了內(nèi)核一般會(huì)沿用 BIOS 的分配結(jié)果除非有沖突或者用pcirealloc參數(shù)強(qiáng)制重新分配。在 Linux 下每個(gè) PCIe 設(shè)備在/sys/bus/pci/devices/下有一個(gè)目錄目錄名是domain:bus:device.function的格式。這個(gè)目錄里有很多有用的文件config配置空間的二進(jìn)制內(nèi)容可以用hexdump查看。resource0、resource1等BAR 空間的內(nèi)存映射文件可以用mmap映射到用戶空間直接訪問。enable寫入 1 使能設(shè)備。driver指向當(dāng)前綁定的驅(qū)動(dòng)。對(duì)于 FPGA 開發(fā)者來說resource0特別有用。你可以寫一個(gè)簡(jiǎn)單的用戶態(tài)程序mmap這個(gè)文件就能直接讀寫 FPGA 的寄存器不需要寫內(nèi)核驅(qū)動(dòng)。這在調(diào)試階段非常方便。# 查看設(shè)備的 BAR 分配情況 lspci -vvv -s 01:00.0 | grep -A 10 Region # 用 setpci 讀取配置空間 setpci -s 01:00.0 0x04.w # 查看 sysfs 下的資源文件 ls -l /sys/bus/pci/devices/0000:01:00.0/resource*5. FPGA 開發(fā)中配置空間與 BAR 的實(shí)戰(zhàn)要點(diǎn)對(duì)于做 FPGA 的工程師來說配置空間和 BAR 不是抽象概念而是每天都要打交道的實(shí)際配置。無論是用 Xilinx 的 XDMA、Intel 的 PCIe Hard IP還是自己寫 PCIe 核BAR 的規(guī)劃都直接影響系統(tǒng)的易用性和性能。5.1 BAR 規(guī)劃把什么放進(jìn)哪個(gè) BAR一個(gè) PCIe 設(shè)備最多有 6 個(gè) BAR怎么分配這些 BAR 是有講究的。常見的規(guī)劃方式BAR0控制寄存器和狀態(tài)寄存器通常幾 KB 到幾十 KB。這部分訪問頻繁但數(shù)據(jù)量小。BAR1DMA 描述符區(qū)域或者大塊數(shù)據(jù)緩沖區(qū)可能需要 MB 級(jí)別。BAR2/BAR3如果做成 64 位 BAR和 BAR0/BAR1 配對(duì)使用。BAR4/BAR5預(yù)留給擴(kuò)展功能或者第二個(gè)功能。我的一般原則是把訪問頻繁的小寄存器放在一個(gè) BAR 里把大塊數(shù)據(jù)緩沖區(qū)單獨(dú)放一個(gè) BAR。這樣做的好處是小 BAR 可以映射成非預(yù)取的保證讀寫順序大 BAR 可以映射成預(yù)取的利用緩存提高吞吐。另外BAR 的大小要按 2 的冪次對(duì)齊。如果你聲明需要 3KB實(shí)際會(huì)占用 4KB。所以規(guī)劃時(shí)要留余量但也不要浪費(fèi)太多地址空間。在資源緊張的系統(tǒng)里BAR 大小直接影響能不能枚舉成功。5.2 配置空間在 IP 核里的實(shí)現(xiàn)用 Xilinx XDMA IP 時(shí)配置空間的大部分字段是 IP 自動(dòng)生成的但有幾個(gè)地方需要手動(dòng)配置Vendor ID 和 Device ID在 IP 配置界面里填寫或者通過參數(shù)傳遞。Class Code決定操作系統(tǒng)加載哪個(gè)驅(qū)動(dòng)比如 0x020000 是以太網(wǎng)0x010802 是 NVMe。BAR 配置選擇每個(gè) BAR 的大小和類型XDMA 支持配置 BAR 為 MEM 或 IO32 位或 64 位。MSI/MSI-X選擇中斷方式XDMA 支持 MSI-X可以配置中斷向量數(shù)量。Intel 的 PCIe Hard IP 類似但配置方式不同。它的配置空間是通過 Avalon-MM 接口暴露的可以在邏輯里動(dòng)態(tài)修改某些字段。這在需要?jiǎng)討B(tài)改變?cè)O(shè)備 ID 或者 BAR 大小的場(chǎng)景下很有用。自己寫 PCIe 核的話配置空間的實(shí)現(xiàn)就更靈活了但也更容易出錯(cuò)。最常見的問題是 BAR 大小聲明和實(shí)際實(shí)現(xiàn)不匹配導(dǎo)致主機(jī)分配了地址但設(shè)備不響應(yīng)。我的經(jīng)驗(yàn)是配置空間里的 BAR 大小一定要和邏輯里地址譯碼的范圍嚴(yán)格一致差一個(gè)字節(jié)都可能出問題。5.3 DMA 與 BAR 的配合DMA 是 FPGA 加速卡的核心功能而 DMA 和 BAR 的配合有幾個(gè)關(guān)鍵點(diǎn)DMA 描述符的存放位置描述符可以放在 BAR 空間里由主機(jī)寫入FPGA 讀取也可以放在主機(jī)內(nèi)存里FPGA 通過 DMA 讀取。前者簡(jiǎn)單后者靈活。地址轉(zhuǎn)換FPGA 發(fā)出的 DMA 讀寫請(qǐng)求地址是主機(jī)物理地址。如果 FPGA 邏輯里用的是虛擬地址或者偏移地址需要做轉(zhuǎn)換。XDMA IP 提供了地址轉(zhuǎn)換功能可以配置 AXI 地址到 PCIe 地址的映射。BAR 空間作為 DMA 目標(biāo)主機(jī)可以通過 BAR 空間直接讀寫 FPGA 的緩沖區(qū)這種方式叫 PIOProgrammed IO適合小數(shù)據(jù)量。大數(shù)據(jù)量還是要用 DMA。我做過一個(gè)高速 ADC 采集的項(xiàng)目ADC 數(shù)據(jù)先寫入 FPGA 的 DDR然后通過 DMA 搬到主機(jī)內(nèi)存。BAR 空間里放的是控制寄存器和 DMA 描述符數(shù)據(jù)緩沖區(qū)不映射到 BAR而是通過 DMA 直接訪問主機(jī)內(nèi)存。這樣設(shè)計(jì)的好處是 BAR 空間很小枚舉容易而且 DMA 帶寬不受 BAR 大小限制。5.4 調(diào)試 BAR 訪問問題的排查鏈路BAR 訪問出問題是最常見的 PCIe 調(diào)試場(chǎng)景。我總結(jié)了一個(gè)排查鏈路按順序走基本能定位問題確認(rèn)設(shè)備被識(shí)別lspci能不能看到設(shè)備Vendor ID 和 Device ID 對(duì)不對(duì)確認(rèn) BAR 被分配lspci -vvv看 Region 字段有沒有Memory at xxxx的分配結(jié)果如果顯示Region 0: Memory at unassigned說明 BAR 沒分配成功。確認(rèn) Command 寄存器setpci -s xx:xx.x 0x04.w讀出來Memory Space Enable 位bit 1是不是 1確認(rèn)橋窗口如果設(shè)備掛在橋下面檢查橋的 Memory Base/Limit 是否覆蓋了設(shè)備的 BAR 地址。確認(rèn)地址譯碼用setpci往 BAR 地址寫一個(gè)值再讀回來看是否一致。如果不一致說明地址譯碼有問題。確認(rèn) FPGA 邏輯如果前面都正常但讀寫數(shù)據(jù)不對(duì)就要查 FPGA 邏輯里的地址譯碼和寄存器實(shí)現(xiàn)。這個(gè)鏈路我用了很多次大部分 BAR 問題都能在前三步定位。第四步和第五步涉及橋和地址譯碼稍微復(fù)雜一些但只要有l(wèi)spci和setpci兩個(gè)工具也能查清楚。有一個(gè)坑特別隱蔽有些主板的 BIOS 會(huì)把 BAR 分配到一個(gè)和系統(tǒng)內(nèi)存重疊的地址導(dǎo)致訪問 BAR 時(shí)實(shí)際訪問到了內(nèi)存。這種情況在lspci -vvv里看不出來需要用cat /proc/iomem查看系統(tǒng)的內(nèi)存映射確認(rèn) BAR 地址沒有落在 System RAM 區(qū)域。6. 那些文檔里不會(huì)寫的踩坑經(jīng)驗(yàn)配置空間和 BAR 的規(guī)范寫得很清楚但實(shí)際硬件和軟件實(shí)現(xiàn)里有很多規(guī)范沒覆蓋的細(xì)節(jié)。這些細(xì)節(jié)往往就是調(diào)試時(shí)卡住的地方。6.1 BAR 大小探測(cè)的邊界情況寫全 1 探測(cè) BAR 大小時(shí)有一個(gè)邊界情況如果設(shè)備實(shí)現(xiàn)的 BAR 大小是 0也就是不需要任何地址空間寫全 1 讀回來還是全 1。這時(shí)候主機(jī)應(yīng)該跳過這個(gè) BAR不分配地址。但有些主機(jī)會(huì)誤判給一個(gè)大小為 0 的 BAR 分配地址導(dǎo)致后續(xù) BAR 分配錯(cuò)位。還有一種情況是 BAR 大小不是 2 的冪次。規(guī)范要求 BAR 大小必須是 2 的冪次但有些 FPGA 設(shè)計(jì)為了省地址空間聲明了一個(gè)非 2 冪次的大小。這種行為在枚舉時(shí)可能被主機(jī)糾正也可能導(dǎo)致分配失敗。我的建議是老老實(shí)實(shí)按 2 的冪次來別耍小聰明。6.2 熱插拔場(chǎng)景下的 BAR 重新分配PCIe 熱插拔是一個(gè)復(fù)雜的功能涉及到 BAR 的重新分配。當(dāng)設(shè)備被熱插入時(shí)主機(jī)需要重新枚舉這條總線給新設(shè)備分配 BAR 地址。如果之前的地址空間不夠可能還需要調(diào)整已有設(shè)備的 BAR 分配。這個(gè)過程在 Linux 下由pciehp驅(qū)動(dòng)處理。實(shí)際使用中熱插拔失敗最常見的原因是地址空間不足。特別是 32 位 MEM 空間在很多系統(tǒng)上已經(jīng)很緊張了。如果新設(shè)備需要大塊 32 位 BAR很可能分配失敗。解決辦法是盡量用 64 位 BAR把地址需求放到 64 位空間里。另外熱插拔時(shí)橋窗口的配置也需要更新。如果橋的 Memory Limit 沒有擴(kuò)展到覆蓋新設(shè)備的 BAR 地址設(shè)備雖然被識(shí)別但訪問會(huì)失敗。這個(gè)問題在調(diào)試熱插拔時(shí)經(jīng)常遇到需要手動(dòng)或者通過腳本調(diào)整橋窗口。6.3 鏈路降速與 BAR 訪問的關(guān)系PCIe 鏈路降速比如從 Gen3 降到 Gen1或者降寬從 x4 降到 x1通常被認(rèn)為是物理層問題但實(shí)際上它也會(huì)影響 BAR 訪問。鏈路降速后配置空間和 BAR 的訪問延遲會(huì)增加如果軟件里有超時(shí)機(jī)制可能會(huì)誤判為設(shè)備無響應(yīng)。更嚴(yán)重的是鏈路不穩(wěn)定可能導(dǎo)致配置空間讀取錯(cuò)誤比如 Vendor ID 讀出來是 0xFFFF 或者隨機(jī)值。這種情況下主機(jī)會(huì)認(rèn)為設(shè)備不存在或者設(shè)備故障直接跳過枚舉。排查這類問題時(shí)不能只看 BAR 分配還要檢查鏈路狀態(tài)寄存器確認(rèn)鏈路是否穩(wěn)定在預(yù)期的速度和寬度。AER高級(jí)錯(cuò)誤報(bào)告是排查這類問題的利器。使能 AER 后任何鏈路錯(cuò)誤都會(huì)被記錄到 AER 寄存器里包括錯(cuò)誤類型、發(fā)生時(shí)間、涉及的 TLP 等。在 Linux 下可以用dmesg看到 AER 報(bào)告的錯(cuò)誤信息。6.4 不同操作系統(tǒng)對(duì) BAR 分配的差異Windows 和 Linux 在 BAR 分配策略上有一些差異這些差異在跨平臺(tái)開發(fā)時(shí)需要注意Windows對(duì) 32 位 BAR 的分配比較保守如果 32 位空間不足可能會(huì)拒絕分配導(dǎo)致設(shè)備無法啟動(dòng)。Windows 對(duì) 64 位 BAR 的支持較好但需要設(shè)備正確聲明。Linux默認(rèn)沿用 BIOS 分配如果 BIOS 分配不合理可以用pcirealloc強(qiáng)制重新分配。Linux 對(duì)資源不足的處理更靈活但重新分配可能導(dǎo)致設(shè)備編號(hào)變化。我遇到過一塊 FPGA 卡在 Windows 下 BAR 分配失敗但在 Linux 下正常。原因是 Windows 的 32 位 MEM 空間被其他設(shè)備占滿了而這塊卡的 BAR 只支持 32 位。后來把 BAR 改成 64 位Windows 下也正常了。這個(gè)案例說明BAR 的類型選擇不只是技術(shù)問題還要考慮目標(biāo)操作系統(tǒng)的資源管理策略。6.5 配置空間讀寫失敗的常見原因配置空間讀寫失敗通常表現(xiàn)為讀回來全 F 或者全 0。常見原因包括設(shè)備未完成鏈路訓(xùn)練鏈路還沒建立配置空間自然讀不到。等鏈路訓(xùn)練完成后再讀。設(shè)備電源未就緒有些設(shè)備需要外部電源電源沒上或者時(shí)序不對(duì)配置空間不可訪問。配置事務(wù)路由錯(cuò)誤總線號(hào)、設(shè)備號(hào)、功能號(hào)不對(duì)事務(wù)被路由到了錯(cuò)誤的位置。設(shè)備處于復(fù)位狀態(tài)復(fù)位沒釋放設(shè)備不響應(yīng)配置事務(wù)。時(shí)鐘未穩(wěn)定PCIe 參考時(shí)鐘不穩(wěn)定鏈路訓(xùn)練反復(fù)失敗。排查時(shí)可以用示波器看參考時(shí)鐘和復(fù)位信號(hào)用協(xié)議分析儀抓配置事務(wù)確認(rèn)事務(wù)是否到達(dá)設(shè)備。軟件層面可以用lspci -xxxx讀原始配置空間看是否全 F。7. 從理解到落地把配置空間和 BAR 用起來講了這么多原理和坑最后回到實(shí)際使用上。配置空間和 BAR 不是只用來理解的它們?cè)谌粘i_發(fā)和調(diào)試中有一堆實(shí)際用途。7.1 用 sysfs 和工具快速查看設(shè)備信息Linux 下查看 PCIe 設(shè)備信息最常用的工具是lspci配合不同的參數(shù)可以看到不同層次的信息命令作用lspci列出所有設(shè)備的基本信息lspci -t以樹形顯示拓?fù)浣Y(jié)構(gòu)lspci -vvv顯示詳細(xì)配置空間信息包括 BAR、能力列表、鏈路狀態(tài)lspci -xxxx以十六進(jìn)制顯示完整配置空間setpci讀寫配置空間寄存器lspci -vvv -s 01:00.0查看指定設(shè)備的詳細(xì)信息除了lspci/sys/bus/pci/devices/下的文件系統(tǒng)接口也很重要。每個(gè)設(shè)備的config文件是配置空間的二進(jìn)制鏡像resource文件是 BAR 空間的映射信息。寫腳本自動(dòng)化調(diào)試時(shí)直接讀這些文件比解析lspci輸出更可靠。7.2 用戶態(tài)直接訪問 BAR 空間對(duì)于 FPGA 調(diào)試用戶態(tài)直接訪問 BAR 空間是最方便的方式。基本步驟找到設(shè)備的 sysfs 路徑比如/sys/bus/pci/devices/0000:01:00.0/。打開resource0文件用mmap映射到用戶空間。通過映射后的指針直接讀寫寄存器。#include stdio.h #include stdlib.h #include fcntl.h #include sys/mman.h #include unistd.h int main() { int fd open(/sys/bus/pci/devices/0000:01:00.0/resource0, O_RDWR | O_SYNC); if (fd 0) { perror(open); return -1; } // 假設(shè) BAR0 大小為 64KB size_t bar_size 64 * 1024; volatile unsigned int *bar mmap(NULL, bar_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (bar MAP_FAILED) { perror(mmap); close(fd); return -1; } // 讀偏移 0x00 的寄存器 unsigned int value bar[0]; printf(Register at offset 0x00: 0x%08X\n, value); // 寫偏移 0x04 的寄存器 bar[1] 0x12345678; munmap((void *)bar, bar_size); close(fd); return 0; }這段代碼的關(guān)鍵點(diǎn)是O_SYNC標(biāo)志和volatile指針。O_SYNC保證寫操作立即生效不會(huì)被緩存volatile防止編譯器優(yōu)化掉看似冗余的讀寫。對(duì)于有副作用的寄存器這兩個(gè)都很重要。7.3 在 FPGA 邏輯里實(shí)現(xiàn)配置空間和 BAR如果自己寫 PCIe 邏輯配置空間的實(shí)現(xiàn)需要覆蓋規(guī)范要求的字段。最小實(shí)現(xiàn)包括Vendor ID、Device ID、Revision ID、Class CodeHeader Type、Cache Line Size、Latency TimerBAR0 到 BAR5Command 和 Status 寄存器Capabilities Pointer 和能力列表BAR 的實(shí)現(xiàn)需要做地址譯碼當(dāng) PCIe 事務(wù)的地址落在 BAR 范圍內(nèi)時(shí)把事務(wù)轉(zhuǎn)發(fā)到內(nèi)部邏輯否則返回 URUnsupported Request響應(yīng)。地址譯碼的邏輯不復(fù)雜但要注意幾點(diǎn)BAR 的地址范圍要和配置空間里聲明的大小一致。64 位 BAR 要同時(shí)比較高 32 位和低 32 位地址。預(yù)取 BAR 和非預(yù)取 BAR 的處理方式不同預(yù)取 BAR 可以接受更大的突發(fā)長度。地址譯碼的時(shí)序要滿足 PCIe 的延遲要求不能引入太多組合邏輯。7.4 配置空間和 BAR 在虛擬化場(chǎng)景下的變化在虛擬化環(huán)境里配置空間和 BAR 的處理會(huì)多一層。虛擬機(jī)監(jiān)控器Hypervisor需要把物理設(shè)備的配置空間和 BAR 空間映射到虛擬機(jī)里讓虛擬機(jī)里的操作系統(tǒng)以為自己在直接訪問硬件。這個(gè)映射過程涉及到地址轉(zhuǎn)換虛擬機(jī)里的 BAR 地址是虛擬地址Hypervisor 需要把它轉(zhuǎn)換成物理地址。如果設(shè)備支持 SR-IOV每個(gè)虛擬功能VF都有自己的配置空間和 BARHypervisor 需要管理這些資源的分配。在虛擬化場(chǎng)景下調(diào)試 PCIe 問題要注意區(qū)分是物理設(shè)備的問題還是虛擬化層的問題??梢韵仍谒拗鳈C(jī)上確認(rèn)設(shè)備正常再在虛擬機(jī)里排查。lspci在虛擬機(jī)里看到的設(shè)備信息可能和宿主機(jī)不同因?yàn)?Hypervisor 可能修改了某些字段。8. 寫在最后配置空間和 BAR 空間是 PCIe 軟件接口的基石理解了它們就理解了 PCIe 設(shè)備是怎么被主機(jī)發(fā)現(xiàn)、配置和訪問的。我剛開始接觸 PCIe 的時(shí)候也覺得這些東西瑣碎不如高速信號(hào)和 DMA 來得刺激。但后來發(fā)現(xiàn)大部分調(diào)試問題都出在這些瑣碎的地方。Vendor ID 讀不對(duì)、BAR 分配失敗、Command 寄存器沒使能這些問題看起來簡(jiǎn)單但如果沒有系統(tǒng)的理解排查起來就是碰運(yùn)氣。實(shí)際項(xiàng)目中我養(yǎng)成了一個(gè)習(xí)慣拿到一塊新的 PCIe 板卡先不急著跑功能而是用lspci -vvv把配置空間完整看一遍確認(rèn) BAR 分配、鏈路狀態(tài)、能力列表都正常。這個(gè)習(xí)慣幫我提前發(fā)現(xiàn)了很多問題比如 BAR 大小聲明錯(cuò)誤、鏈路降速、MSI 配置不對(duì)等?;ㄊ昼娮鲞@個(gè)檢查比后面花幾個(gè)小時(shí)排查要?jiǎng)澦愕枚?。?duì)于 FPGA 開發(fā)者來說配置空間和 BAR 的規(guī)劃應(yīng)該在設(shè)計(jì)初期就確定好而不是等到調(diào)試時(shí)再改。BAR 的大小、類型、數(shù)量Class Code 的選擇MSI-X 向量的數(shù)量這些都會(huì)影響后續(xù)的驅(qū)動(dòng)開發(fā)和系統(tǒng)集成。前期多花點(diǎn)時(shí)間規(guī)劃后期少踩很多坑。