存讀的完整旅程:深入解析事務(wù)層MRd與CPLD機(jī)制)
有沒(méi)有過(guò)這種經(jīng)歷lspci -vv明明列出了一個(gè) PCIe 設(shè)備BAR 地址也分配了軟件去讀寄存器卻返回全0xFF或者干脆卡在readl里遲遲不返回最后上報(bào)一個(gè)總線錯(cuò)誤。很多人第一反應(yīng)是驅(qū)動(dòng)寫錯(cuò)了、中斷沒(méi)配好卻忽略了 PCIe 事務(wù)層協(xié)議里最基礎(chǔ)的一環(huán)——內(nèi)存讀請(qǐng)求和完成響應(yīng)是不是真的對(duì)上了。這次就用一次內(nèi)存讀的完整旅程把 PCIe 事務(wù)層協(xié)議里最關(guān)鍵的部分逐段拆開(kāi)看完之后再去排這一類問(wèn)題思路會(huì)清晰很多。這篇文章的主場(chǎng)景是 CPU 發(fā)起一次對(duì)設(shè)備映射地址的 32 位 MMIO 讀。一次讀在 PCIe 世界里從來(lái)不只是一個(gè)方向的動(dòng)作先要發(fā)一個(gè)內(nèi)存讀請(qǐng)求Memory Read Request, MRd設(shè)備收到后還必須回一個(gè)完成報(bào)文Completion with Data, CPLD。這中間涉及 TLP 構(gòu)造、流控信用、地址路由、標(biāo)簽匹配、完成拆分、錯(cuò)誤狀態(tài)等一系列機(jī)制。適合正在做 PCIe 驅(qū)動(dòng)開(kāi)發(fā)、FPGA PCIe 接口調(diào)試、或者剛開(kāi)始讀 PCIe 協(xié)議但被規(guī)范文檔繞暈的工程師參考。1. 先畫一條路徑一次內(nèi)存讀在 PCIe 里到底要經(jīng)過(guò)哪些環(huán)節(jié)1.1 為什么用“讀”來(lái)拆事務(wù)層而不是“寫”PCIe 的寫事務(wù)是“單程票”。Posted 報(bào)文發(fā)出去就完事不需要對(duì)方回執(zhí)而讀事務(wù)天然是“一問(wèn)一答”請(qǐng)求發(fā)出后請(qǐng)求方必須等待完成報(bào)文回來(lái)整個(gè)往返過(guò)程才會(huì)結(jié)束。正是這個(gè)“回答”機(jī)制把事務(wù)層的報(bào)文類型、路由方式、標(biāo)簽管理、流控信用、錯(cuò)誤狀態(tài)全都帶了出來(lái)。打個(gè)比方寫事務(wù)就像寄出一個(gè)包裹單號(hào)只是方便你事后查讀事務(wù)則是寄出一張查詢單要求對(duì)方蓋個(gè)回執(zhí)再寄回來(lái)?;貓?zhí)如果丟了、慢了、寄錯(cuò)了人立刻就能體現(xiàn)出異常。所以把一次讀吃透寫、消息、配置讀寫這些事務(wù)的規(guī)則基本也能順勢(shì)理解一大半。1.2 一次讀的完整旅程可以拆成七個(gè)站點(diǎn)不用畫流程圖我用文字把這次讀的經(jīng)過(guò)列出來(lái)后面所有內(nèi)容都是圍繞這條鏈路展開(kāi)的CPU/驅(qū)動(dòng)程序訪問(wèn)一段已映射的 MMIO 地址這個(gè)地址在 Root Complex 里被識(shí)別為 PCIe 域。Root Complex 的事務(wù)層生成一個(gè)內(nèi)存讀請(qǐng)求 TLP也就是 MRd。如果拓?fù)淅镉?SwitchTLP 按目標(biāo)地址被路由到對(duì)應(yīng)的下游端口。端點(diǎn)接收 TLP先由數(shù)據(jù)鏈路層做完整性校驗(yàn)再交給事務(wù)層解析。端點(diǎn)根據(jù)地址命中自己的 BAR 空間把內(nèi)部寄存器或存儲(chǔ)器的數(shù)據(jù)讀出來(lái)。端點(diǎn)構(gòu)造完成報(bào)文 CPLD攜帶數(shù)據(jù)和狀態(tài)返回。Root Complex 根據(jù)請(qǐng)求者 ID 和 Tag 匹配完成最終把數(shù)據(jù)交給 CPU 或系統(tǒng)內(nèi)存。這七個(gè)站點(diǎn)是典型場(chǎng)景實(shí)際拓?fù)淇赡軟](méi)有 Switch直接從 Root Port 到設(shè)備那第 3 步和第 6 步的路由判斷就更簡(jiǎn)單但核心機(jī)制完全一樣。我用一個(gè)表格把每個(gè)站點(diǎn)涉及的關(guān)鍵知識(shí)點(diǎn)先擺出來(lái)階段主要?jiǎng)幼魃婕笆聞?wù)層知識(shí)點(diǎn)Root Complex 發(fā)起地址翻譯、TLP 構(gòu)造BAR、請(qǐng)求頭部、TagSwitch 下行按地址路由地址路由表、內(nèi)存窗口端點(diǎn)接收校驗(yàn)并解析請(qǐng)求LCRC、事務(wù)層接收邏輯端點(diǎn)數(shù)據(jù)源BAR 命中并讀取內(nèi)部寄存器/RAM 映射端點(diǎn)應(yīng)答構(gòu)造 CPLD完成頭、狀態(tài)、拆分規(guī)則Switch 回程按 ID 路由Completer IDRoot Complex 匹配完成隊(duì)列配對(duì)Requester ID Tag1.3 MMIO 讀和 DMA 讀是兩條不同方向的路很多朋友看到“內(nèi)存讀”會(huì)條件反射想到 DMA也就是端點(diǎn)設(shè)備主動(dòng)去讀系統(tǒng)內(nèi)存。這確實(shí)是 PCIe 里最常見(jiàn)的業(yè)務(wù)之一但和本文主講的 MMIO 讀方向正好相反。對(duì)比項(xiàng)MMIO 讀DMA 讀發(fā)起方Root ComplexEndpoint讀取對(duì)象端點(diǎn)的 BAR 空間主機(jī)側(cè)系統(tǒng)內(nèi)存請(qǐng)求頭里的地址含義目標(biāo)設(shè)備內(nèi)部地址主機(jī)物理內(nèi)存地址完成方向Endpoint - Root ComplexRoot Complex - Endpoint常見(jiàn)故障點(diǎn)BAR 沒(méi)使能、設(shè)備不回完成Bus Master Enable 沒(méi)開(kāi)、IOMMU 映射錯(cuò)誤DMA 讀的核心 TLP 機(jī)制和 MMIO 讀其實(shí)完全同源只是“問(wèn)路”的方向反了過(guò)來(lái)。把 MMIO 讀這條鏈路徹底搞懂DMA 讀的請(qǐng)求和完成關(guān)系也可以直接遷移過(guò)去。2. 事務(wù)層的“快遞單”TLP 頭部到底寫了什么2.1 TLP 在 PCIe 分層里的位置PCIe 事務(wù)層的核心產(chǎn)物就是 TLP。事務(wù)層負(fù)責(zé)把 CPU 側(cè)的內(nèi)存訪問(wèn)翻譯成標(biāo)準(zhǔn)報(bào)文在接收側(cè)又把報(bào)文恢復(fù)成內(nèi)存操作。TLP 往下走的時(shí)候數(shù)據(jù)鏈路層會(huì)在前面加一個(gè)序列號(hào)、在末尾加 LCRC物理層再負(fù)責(zé)編碼和高速串行傳送。在這一堆處理中事務(wù)層才是真正有“業(yè)務(wù)邏輯”的地方鏈路層只是保證搬運(yùn)不丟不壞物理層解決傳輸介質(zhì)問(wèn)題。理解這一點(diǎn)很有用平時(shí)用協(xié)議分析儀或者 FPGA 里的 TLP tracer 抓到的數(shù)據(jù)本質(zhì)上都是把物理層符號(hào)解碼、把鏈路層序號(hào)和 CRC 剝掉以后得到的事務(wù)層內(nèi)容。只要你會(huì)看 TLP就等于拿到了真正的主線劇透。2.2 內(nèi)存讀請(qǐng)求頭部字段逐項(xiàng)拆解一個(gè) 32 位地址的 MMIO 讀請(qǐng)求通常使用 3DW 的 TLP 頭部。我按實(shí)際意義把關(guān)鍵字段列出來(lái)字段說(shuō)明一次 32 位讀的典型取值Fmt / Type報(bào)文格式和類型Memory ReadMRd3DW 頭Length本次要讀的總長(zhǎng)度單位 DW1即讀取 4 字節(jié)Requester ID請(qǐng)求發(fā)起方的 BDF如 00:01.0Tag請(qǐng)求編號(hào)用于配對(duì)由發(fā)起方分配的未用號(hào)Last DW BE / First DW BE首尾 DW 內(nèi)哪些字節(jié)有效全有效時(shí)是 0xF / 0xFAddress目標(biāo)地址BAR 基址加偏移TC / Attr / EP / TD服務(wù)質(zhì)量與完整性屬性常規(guī)讀通常為 0Length字段要特別注意對(duì)于讀請(qǐng)求TLP 內(nèi)部并不攜帶數(shù)據(jù)Length 代表的是“希望對(duì)方返回多少 DW”。這和寫請(qǐng)求完全不一樣寫請(qǐng)求是Length個(gè) DW 的數(shù)據(jù)實(shí)際跟著請(qǐng)求走。地址超過(guò) 32 位時(shí)頭部會(huì)變成 4DW地址字段占 64 位。日常很多設(shè)備的 BAR 被系統(tǒng)分配在低 32 位地址空間所以 3DW 頭部出現(xiàn)得最多但在 64 位系統(tǒng)里設(shè)備資源也可能被分配到高地址這時(shí)不能用老眼光去看報(bào)文。First/Last DW BE這組字段容易被人忽略。軟件用readl讀 4 字節(jié)時(shí)這兩個(gè)字段都是0xF表示 4 個(gè)字節(jié)全要。但有些驅(qū)動(dòng)會(huì)用readw或readb操作同一個(gè)地址那時(shí) Byte Enable 就只對(duì)應(yīng)相應(yīng)字節(jié)有效。設(shè)備側(cè)如果不按 Byte Enable 處理就很容易出現(xiàn)“讀回來(lái)總是整字但驅(qū)動(dòng)讀半字時(shí)數(shù)據(jù)錯(cuò)位”的問(wèn)題。2.3 完成報(bào)文 CPLD 的頭部和請(qǐng)求頭有何不同完成報(bào)文是讀請(qǐng)求的響應(yīng)頭部結(jié)構(gòu)有明顯差別。它不再有目標(biāo)地址取而代之的是完成方自己的信息字段說(shuō)明本例典型值Fmt / Type帶數(shù)據(jù)的完成CPLDCompleter ID完成方的 BDF目標(biāo)端點(diǎn)的 BDFRequester ID發(fā)起方 BDF必須和請(qǐng)求頭一致Tag原請(qǐng)求的編號(hào)必須原樣返回Status完成狀態(tài)0成功1UR2CAByte Count本次完成包含多少數(shù)據(jù)字節(jié)等于請(qǐng)求的 Length * 4Lower Address完成數(shù)據(jù)在地址空間的起始信息請(qǐng)求地址低 bit 對(duì)齊后的值Data讀出的有效載荷寄存器里的 4 字節(jié)值這里最關(guān)鍵的一點(diǎn)完成報(bào)文靠Requester ID Tag找到對(duì)應(yīng)的請(qǐng)求而不是靠地址。也就是說(shuō)請(qǐng)求頭攜帶了“到哪里去讀”的地址完成頭則只帶了“回給誰(shuí)”的身份證。后面講回程路由時(shí)這個(gè)機(jī)制會(huì)越來(lái)越重要。3. 出站之旅請(qǐng)求怎么從根端口跑到目標(biāo)設(shè)備3.1 CPU 的 MMIO 訪問(wèn)如何變成 PCIe 報(bào)文一次readl從 CPU 視角看是簡(jiǎn)單的訪存指令但硬件背后很復(fù)雜。系統(tǒng)在枚舉階段已經(jīng)為每個(gè)設(shè)備 BAR 分配了一段物理地址CPU 訪問(wèn)這段地址時(shí)Host Bridge / Root Complex 會(huì)識(shí)別出它屬于 PCIe 域隨后開(kāi)始構(gòu)造 TLP。這個(gè)過(guò)程通常由 RC 硬件自動(dòng)完成不需要驅(qū)動(dòng)介入。驅(qū)動(dòng)拿到ioremap以后的虛擬地址寫下去或者讀出來(lái)RC 會(huì)把 CPU 指令里的地址和長(zhǎng)度翻譯成 3DW 或 4DW 的 MRd。如果目標(biāo)地址小于 4GBRC 通常用 3DW 頭如果落在 64 位地址空間則使用 4DW 頭。這里最值得注意的一點(diǎn)CPU 指令位寬不完全決定 TLP 頭部格式?jīng)Q定因素是目標(biāo)地址范圍和平臺(tái)實(shí)現(xiàn)。所以在排查“讀寄存器數(shù)據(jù)不對(duì)”時(shí)第一步永遠(yuǎn)先確認(rèn) BAR 分配是否正確。PCIe 枚舉過(guò)程里 BIOS 或 OS 會(huì)把 BAR 寫成某一可用地址如果 BAR 里完全沒(méi)有使能內(nèi)存空間位或者地址窗口還沒(méi)建立RC 就沒(méi)法把 CPU 地址和任何設(shè)備對(duì)上自然也無(wú)法構(gòu)造有效的讀請(qǐng)求。3.2 發(fā)送前必須做流控事務(wù)層不是想發(fā)就發(fā)PCIe 的流控機(jī)制和很多總線不一樣。它不靠接收方發(fā)“忙”信號(hào)而是基于信用量Credit。每個(gè)虛擬通道里分了三類信用Posted、Non-Posted、Completion每一類又區(qū)分 Header 和 Data。內(nèi)存讀請(qǐng)求屬于 Non-Posted 類型需要消耗一個(gè) NP Header Credit。由于讀請(qǐng)求本身不攜帶數(shù)據(jù)體所以不需要消耗 NP Data Credit。發(fā)送方的事務(wù)層在把 TLP 交給數(shù)據(jù)鏈路層之前會(huì)檢查對(duì)應(yīng)的 Credit 是否足夠不夠就先等待。這個(gè)機(jī)制特別像酒店辦理入住時(shí)的預(yù)授權(quán)你得先把額度占住后面退房再結(jié)算。接收方收到 TLP 并消化之后會(huì)向發(fā)送方歸還 Credit雙方才能繼續(xù)“轉(zhuǎn)賬”。工程上有一個(gè)很隱蔽的坑當(dāng)接收方?jīng)]有及時(shí)歸還 Credit或者 Credit 初始化時(shí)配置成 0發(fā)送方就會(huì)一直等表現(xiàn)是“請(qǐng)求發(fā)出去了但鏈路上什么都看不到”。這種問(wèn)題在普通軟件調(diào)不見(jiàn)非得上 TLP 抓包才能定位。3.3 Switch 怎么知道該把讀請(qǐng)求轉(zhuǎn)到哪個(gè)下游端口當(dāng)拓?fù)渲杏?PCIe Switch 時(shí)RC 發(fā)出的內(nèi)存讀請(qǐng)求先到達(dá) Switch 的上游端口。Switch 需要根據(jù)地址信息決定往哪個(gè)下游端口轉(zhuǎn)。這個(gè)決定不靠廣播而是靠枚舉階段建立的內(nèi)存窗口。每個(gè)下游端口在配置空間里會(huì)記錄一組 Memory Base 和 Memory Limit本質(zhì)就是一張地址路由表。Switch 拿到 MRd 之后取出請(qǐng)求頭里的地址和各個(gè)下游端口的窗口比對(duì)命中就往對(duì)應(yīng)端口轉(zhuǎn)發(fā)沒(méi)有命中就返回一個(gè) Unsupported Request 完成或者直接丟棄。這和以太網(wǎng)的泛洪轉(zhuǎn)發(fā)完全不同PCIe 的內(nèi)存讀不會(huì)“全員廣播”每跳都必須有唯一判定。這也是 PCIe 拓?fù)湓綇?fù)雜對(duì)地址窗口配置要求越高的原因。3.4 穿過(guò)鏈路層和物理層時(shí)發(fā)生了什么事務(wù)層把 TLP 構(gòu)造好之后數(shù)據(jù)鏈路層會(huì)給它加 2 字節(jié)的序列號(hào)并在尾部算一份 LCRC。接收端檢查這些信息如果發(fā)現(xiàn) CRC 不對(duì)會(huì)通過(guò)鏈路層重傳機(jī)制要求發(fā)送方重發(fā)。物理層則負(fù)責(zé)把數(shù)據(jù)變成串行信號(hào)送到線上期間還有均衡、擾碼、編碼等操作。對(duì)事務(wù)層討論來(lái)說(shuō)這一層只是“可靠的搬運(yùn)工”。但是排障時(shí)要知道鏈路層如果頻繁丟包重傳最終也會(huì)表現(xiàn)為事務(wù)層“請(qǐng)求發(fā)出去了但一直等不到完成”容易被誤判成設(shè)備故障。這時(shí)候需要用lspci -vvv看鏈路帶寬和鏈路狀態(tài)確認(rèn)物理鏈路是否穩(wěn)定。4. 端點(diǎn)收到讀請(qǐng)求以后在干什么4.1 接收側(cè)的順序先拆包再判斷是不是自己的菜端點(diǎn)從線上收到數(shù)據(jù)后物理層先還原出符號(hào)數(shù)據(jù)鏈路層檢查序列號(hào)和 LCRC確認(rèn)無(wú)誤后才把 TLP 提交給事務(wù)層。事務(wù)層接收模塊要做的第一件事是根據(jù)Fmt/Type判斷報(bào)文的類別和方向如果是內(nèi)存讀請(qǐng)求再取出地址和 BAR 空間做匹配。這里有個(gè)容易被驅(qū)動(dòng)程序忽略的點(diǎn)接收側(cè)必須負(fù)責(zé)歸還 Credit。設(shè)備和 RC 一樣會(huì)在內(nèi)部實(shí)現(xiàn)一個(gè)信用接收端口收到 TLP 后按規(guī)則釋放緩沖。如果設(shè)備固件或邏輯設(shè)計(jì)有缺陷收了一個(gè)報(bào)文后 Credit 沒(méi)歸還發(fā)送方就會(huì)慢慢卡住直到所有 Credit 耗盡。我調(diào)試 FPGA 板卡時(shí)遇到過(guò)幾次“開(kāi)始還能正常讀跑了幾百次以后就徹底沒(méi)響應(yīng)”的情況最后都是 Credit 管理狀態(tài)機(jī)寫錯(cuò)了。4.2 BAR 命中以后數(shù)據(jù)源從哪里來(lái)對(duì)端點(diǎn)的內(nèi)部實(shí)現(xiàn)來(lái)說(shuō)BAR 就像一扇門。門打開(kāi)了以后內(nèi)部地址偏移決定訪問(wèn)哪個(gè)寄存器或哪塊 RAM。FPGA 實(shí)現(xiàn) PCIe 時(shí)最常見(jiàn)的做法是把 BAR 空間映射到一組 CSR 寄存器、Block RAM、或者 DMA 描述符區(qū)域。內(nèi)存讀請(qǐng)求攜帶的地址會(huì)被內(nèi)部譯碼成寄存器地址或 RAM 地址再經(jīng)過(guò)組合邏輯把數(shù)據(jù)取出來(lái)。這里有一個(gè)特別值得提醒的細(xì)節(jié)事務(wù)層協(xié)議保證不了數(shù)據(jù)是“新鮮”的保證的只是數(shù)據(jù)能取回。如果設(shè)備內(nèi)部讀取邏輯沒(méi)有做同步比如寄存器還沒(méi)被上游模塊刷新讀請(qǐng)求來(lái)了就直接返回舊數(shù)據(jù)軟件是感知不到的只會(huì)發(fā)現(xiàn)“為什么我寫進(jìn)去的值讀出來(lái)不對(duì)”。這種問(wèn)題通常要靠補(bǔ)同步邏輯或者加入讀時(shí)鐘域處理來(lái)解決。4.3 構(gòu)造完成報(bào)文時(shí)要遵守的拆分規(guī)則端點(diǎn)讀取到數(shù)據(jù)后并不是想怎么回就怎么回。PCIe 協(xié)議對(duì)讀完成有兩個(gè)硬性約束第一個(gè)約束是 Max Payload SizeMPS。如果一次讀請(qǐng)求的 Length 很大而完成者的 MPS 只有 128 字節(jié)它就不能把 256 字節(jié)的數(shù)據(jù)一次性塞進(jìn)一個(gè) CPLD而是要拆成多個(gè)完成報(bào)文。第二個(gè)約束是 Read Completion BoundaryRCB。RCB 定義了完成報(bào)文拆分時(shí)必須對(duì)齊的地址邊界。RC 的完成者可以通過(guò)配置空間把 RCB 選為 64B 或 128B非 RC 的完成者固定是 64B。這也是為什么同一個(gè)設(shè)備在有些平臺(tái)上表現(xiàn)特別好換一個(gè) Root Complex 以后大塊讀性能就掉下來(lái)——很可能是 RCB 策略變了。每個(gè)拆分出來(lái)的 CPLD 都帶有自己的Lower Address和Byte Count請(qǐng)求方拿到這些完成報(bào)文后會(huì)按地址和 Tag 重組數(shù)據(jù)。編寫驅(qū)動(dòng)時(shí)不要假設(shè)第一個(gè)返回的 CPLD 一定包含最前面一段數(shù)據(jù)協(xié)議允許完成者按自己的拆包策略返回所以軟件端如果要處理多完成的情況必須按頭部信息重新拼裝。5. 回程邏輯完成報(bào)文憑什么能找到“原路返回”的路5.1 完成靠 ID 路由不是靠地址路由這是事務(wù)層協(xié)議里最容易被誤解的部分。內(nèi)存讀請(qǐng)求往下走用地址路由大家都好理解但完成報(bào)文頭部沒(méi)有目標(biāo)地址它拿什么路由答案是靠Completer ID和Requester ID。完成報(bào)文從端點(diǎn)發(fā)出時(shí)Completer ID就是端點(diǎn)自己的 BDFRequester ID是當(dāng)初請(qǐng)求方的 BFD。Switch 收到完成報(bào)文后根據(jù) ID 判斷應(yīng)該往上游端口還是下游端口轉(zhuǎn)最終一路轉(zhuǎn)到根端口??梢赃@樣理解地址路由是“按收貨地址送貨”ID 路由則是“按客戶編號(hào)找寄件人”??爝f單上寫了客戶編號(hào)分揀站看一眼編號(hào)就知道這個(gè)回執(zhí)該回到哪去和寄件時(shí)寫的收貨地址沒(méi)有直接關(guān)系。最關(guān)鍵的一點(diǎn)是完成報(bào)文只會(huì)被 Requester ID 對(duì)應(yīng)的那個(gè)請(qǐng)求方接收并處理其他設(shè)備即使看到也直接忽略不能劫持更不能當(dāng)作廣播消息去處理。5.2 Tag 空間不足會(huì)讓讀請(qǐng)求“堵車”PCIe 允許一個(gè)請(qǐng)求方同時(shí)掛多個(gè)未完成的讀請(qǐng)求不要求發(fā)一個(gè)等一個(gè)。每個(gè)未完成請(qǐng)求都要占用一個(gè) Tag。Tag 就像餐飲店排隊(duì)叫號(hào)號(hào)發(fā)完了后面的人只能等。標(biāo)準(zhǔn)里 Tag 字段是 8 位因此理論上最多可以有 256 個(gè)未完成請(qǐng)求。但實(shí)際實(shí)現(xiàn)未必用滿很多老設(shè)備默認(rèn)只支持一小部分 Tag需要通過(guò)配置空間的 Extended Tag 能力開(kāi)啟更多 Tag 位。鏈路兩側(cè)必須都支持并正確配置否則請(qǐng)求方很快就會(huì)把 Tag 耗盡表現(xiàn)為“新請(qǐng)求發(fā)不出去讀吞吐上不去”。軟件層面還有一個(gè)常見(jiàn)誤區(qū)CPU 順序代碼執(zhí)行readl時(shí)RC 內(nèi)部很可能會(huì)串行處理這些 MMIO 讀不會(huì)自動(dòng)把多個(gè) Tag 用滿。不要指望一個(gè) for 循環(huán)里的幾百次readl就能自動(dòng)產(chǎn)生幾百個(gè)并行 outstanding 請(qǐng)求。優(yōu)化大塊讀性能要依賴 DMA 或者 CPU 端的 non-blocking read 機(jī)制不能拿順序讀來(lái)硬剛。5.3 狀態(tài)字段里的 UR、CA 和讀超時(shí)完成報(bào)文的狀態(tài)字段有三種常見(jiàn)編碼需要牢記0表示成功完成1表示 Unsupported RequestUR2表示 Completer AbortCA。當(dāng)設(shè)備收到了一個(gè)無(wú)法識(shí)別的請(qǐng)求比如地址沒(méi)命中任何 BAR它可以返回 UR當(dāng)設(shè)備內(nèi)部功能無(wú)法完成操作比如模塊被復(fù)位或者正在初始化它可以返回 CA。這類錯(cuò)誤完成最后會(huì)通過(guò) RC 轉(zhuǎn)化為軟件可見(jiàn)的總線錯(cuò)誤或者記錄在 AER 寄存器里。驅(qū)動(dòng)如果只看到“讀寄存器超時(shí)”或“總線錯(cuò)誤”卻不深入看完成狀態(tài)很容易把問(wèn)題歸到硬件不穩(wěn)定上去實(shí)際上協(xié)議已經(jīng)給出了非常明確的錯(cuò)誤類型。如果請(qǐng)求發(fā)出后完成報(bào)文一直不回來(lái)就會(huì)進(jìn)入 Completion Timeout。RC 的 Device Control 2 配置空間里可以設(shè)置超時(shí)值不同平臺(tái)默認(rèn)值可能很不相同。更隱蔽的情況是設(shè)備鏈路過(guò)早進(jìn)入低功耗狀態(tài)比如 L1 或熱插拔之后設(shè)備還沒(méi)有完成重新初始化讀請(qǐng)求發(fā)出后卡很久才超時(shí)。這時(shí)需要先看鏈路狀態(tài)而不是盯著驅(qū)動(dòng)代碼。6. 內(nèi)存讀排障實(shí)錄幾個(gè)最常見(jiàn)的深坑6.1 讀寄存器總是得到全 (0xFF) 或全 0看到全 (0xFF) 時(shí)先冷靜不要直接判斷“設(shè)備壞了”。按這個(gè)順序排查用setpci查看 Command 寄存器確認(rèn) Memory Space Enable 是否置 1。用lspci -vvv查看 BAR 值確定地址分配合法且沒(méi)有被其他資源覆蓋。用setpci手動(dòng)讀 BAR確認(rèn)硬件返回的不是0xFFFFFFFF。排除設(shè)備內(nèi)部存在偏移未實(shí)現(xiàn)導(dǎo)致端點(diǎn)返回 FF 的預(yù)期行為。全0的現(xiàn)象通常比全FF更麻煩。全FF大概率是地址沒(méi)命中或信號(hào)懸空全0往往是設(shè)備軟核邏輯直接返回了固定值不是協(xié)議層面的問(wèn)題。這兩種都要結(jié)合設(shè)備手冊(cè)確認(rèn)每個(gè)偏移位的默認(rèn)值。6.2 readl 卡死或者直接總線錯(cuò)誤先查完成路徑readl卡住、進(jìn)而觸發(fā)系統(tǒng) bus error本質(zhì)是 CPU 等了太久仍然沒(méi)有等到 CPLD。按照協(xié)議定位時(shí)注意區(qū)分兩種場(chǎng)景MMIO 讀場(chǎng)景下EP 不需要開(kāi)啟 Bus Master Enable 也能回應(yīng) RC 的讀請(qǐng)求因?yàn)樵撜?qǐng)求由 RC 發(fā)起。此時(shí)如果無(wú)響應(yīng)優(yōu)先檢查端點(diǎn)是否真的收到了 TLP、內(nèi)部譯碼是否命中、完成邏輯是否正常。DMA 讀場(chǎng)景則反過(guò)來(lái)EP 主動(dòng)讀系統(tǒng)內(nèi)存前提是 EP 的 Bus Master Enable 必須打開(kāi)還要有正確的 IOMMU/SMMU 映射。很多 DMA 讀異常根因都是 DMA 映射沒(méi)建立或 BME 沒(méi)置位。如果 TLP 已經(jīng)發(fā)出但一直收不到完成建議直接用帶 TLP tracer 的調(diào)試工具抓一次。確認(rèn) MRd 有沒(méi)有出 RCCPLD 有沒(méi)有從 EP 回來(lái)。如果只看到 MRd 沒(méi)有 CPLD問(wèn)題基本鎖定在端點(diǎn)側(cè)如果 CPLD 已發(fā)出但 RC 沒(méi)匹配上則要考慮 Tag 是否被占用、ID 路由是否正確、完成隊(duì)列是否滿了。6.3 大塊讀性能上不去多半是 MPS 和 RCB 的鍋有時(shí)測(cè)量 PCIe 讀帶寬發(fā)現(xiàn)遠(yuǎn)低于鏈路速率但不是鏈路沒(méi)訓(xùn)練好而是事務(wù)層參數(shù)不合適。最常見(jiàn)兩個(gè)因素第一Max Payload Size 太小。如果 MPS 只有 128B一次 256B 的讀請(qǐng)求會(huì)被拆成多個(gè)完成報(bào)文返回頭部開(kāi)銷占比和事務(wù)數(shù)都會(huì)增加吞吐自然上不去。第二RCB 設(shè)置不合理。RCB 影響完成報(bào)文能否在更寬的邊界上對(duì)齊非 RC 設(shè)備固定 64BRC 側(cè)可以通過(guò)配置選擇 64B 或 128B。用lspci -vvv可以快速看到 MaxPayload 和 RCB 配置。注意鏈路兩側(cè)的 MPS 必須協(xié)商出一個(gè)共同值如果一端支持 256B另一端只到 128B最后按最小側(cè)對(duì)齊。這個(gè)時(shí)候不是換根線或者調(diào)驅(qū)動(dòng)能解決的要看設(shè)備固件和 RC 的配置協(xié)商。癥狀重點(diǎn)檢查項(xiàng)常見(jiàn)根因讀返回全 FFBAR、Command、偏移映射內(nèi)存空間未使能或地址沒(méi)命中讀卡死/超時(shí)TLP 抓包、完成隊(duì)列端點(diǎn)未回完成、Credit 未歸還DMA 讀失敗BME、IOMMU 映射總線主控未打開(kāi)或地址翻譯錯(cuò)誤讀性能低MPS、RCBPayload 過(guò)小或拆分邊界限制最后分享一個(gè)我自己的習(xí)慣真要把 PCIe 事務(wù)層搞透與其背一大堆寄存器名不如實(shí)打?qū)嵶ヒ唤M MMIO 讀的 TLP。在 Linux 里對(duì)某個(gè) BAR 地址執(zhí)行一次devmem同時(shí)在總線上抓包把 MRd 請(qǐng)求頭和 CPLD 完成頭按本文表格逐個(gè)字段展開(kāi)對(duì)比一個(gè)往返看下來(lái)比看十篇文檔都管用。曾經(jīng)有一回我在 FPGA 板卡上故意把一段 BAR 映射內(nèi)部的寄存器做成慢速讀取但沒(méi)有在 RTL 里做讀同步結(jié)果 CPU 讀到的總是上一次的舊數(shù)據(jù)最后就是通過(guò)對(duì)比 TLP 的完成順序發(fā)現(xiàn)的問(wèn)題。如果你也在做 PCIe EP 的設(shè)計(jì)這類事務(wù)層的細(xì)節(jié)早晚會(huì)找上門。