存讀請求的完整旅程)
寫這個系列之前我一直在猶豫事務層協(xié)議到底該怎么講才不至于讓讀者背完一堆字段名、一上板子還是不知道該看哪里。寄存器、TLP類型、路由方式、流量控制每樣拆開都能講幾個小時但拼在一起總是散。后來我換了個思路——既然PCIe存在的意義就是讓數(shù)據(jù)從A點到B點那不如就跟著一個內(nèi)存讀請求完整走一趟。從CPU執(zhí)行一條load指令開始看它怎么變成一個TLP包穿越RC、Switch、鏈路層、對端的EP再帶著數(shù)據(jù)原路返回。這一趟走完事務層協(xié)議的那點骨架基本就立住了。這篇文章是系列的第二篇主題是事務層主角只有一個一次內(nèi)存讀MRd的完整旅程。適合正在調(diào)PCIe驅(qū)動的Linux工程師、用FPGA做PCIe IP核的開發(fā)者以及對PCIe協(xié)議停留在“知道概念、沒串起來”階段的人。1. 故事的起點一次內(nèi)存讀指令如何變成總線事務1.1 別把MMIO當成“讀內(nèi)存”先從一個很基本的認知開始軟件里寫一句data *(volatile uint32_t *)0x88001000;這條指令本身經(jīng)過CPU流水線之后并不會真的走到某個內(nèi)存顆粒里去取數(shù)。它要先去查MMU/TLB把虛擬地址翻譯成物理地址然后由CPU核心把這個物理地址的讀請求發(fā)給互連總線比如Intel的Mesh或AMD的Infinity Fabric最后送進Root Complex。Root ComplexRC拿到這塊物理地址后問自己一個問題這個地址有沒有被映射到某個PCIe設(shè)備上怎么判斷的靠的是枚舉階段建立的地址映射。PCIe設(shè)備插入系統(tǒng)后軟件枚舉也就是網(wǎng)上常說的“pcie枚舉過程”會給每個設(shè)備的BARBase Address Register分配一段地址空間。以Intel系統(tǒng)為例RC內(nèi)部維護著一張“出站地址窗口”O(jiān)utbound Window對照表物理地址落在哪個窗口就說明要發(fā)往哪條下游鏈路、哪個設(shè)備。所以MMIO讀的本質(zhì)是CPU發(fā)起的讀請求到達RC后被翻譯成一個PCIe事務然后通過PCIe鏈路發(fā)出去。RC是CPU與PCIe樹之間的翻譯官它把CPU能理解的地址翻譯成PCIe世界能理解的TLP。1.2 一張全局地圖RC、Switch與EP的角色分工要把PCIe協(xié)議講明白拓撲概念繞不開。一個典型系統(tǒng)長這樣Root Complex ├── 端口0連接NICBDF 01:00.0 ├── 端口1連接PCIe Switch上游口BDF 02:00.0 │ ├── 下游口A連接NVMe SSDBDF 03:00.0 │ └── 下游口B連接FPGABDF 04:00.0 └── 端口2連接顯卡BDF 05:00.0這里的BDF是Bus/Device/Function的縮寫枚舉階段由軟件分配是PCIe世界里設(shè)備的“門牌號”。RC本身也有BDF不同的RC層設(shè)備、甚至RC的每個虛擬口都有獨立的BDF這決定了后續(xù)Completion包怎么回家。在這個地圖里角色分三種。RC是發(fā)起者負責代表CPU發(fā)請求EndpointEP是最終執(zhí)行者收到請求后訪問自己的內(nèi)部資源、返回結(jié)果Switch是郵局不做業(yè)務只按路由規(guī)則把包轉(zhuǎn)給正確的端口。請記住這個郵局的比喻后面講路由時會反復用到。這里有個容易被忽略的點EP不是只能被動接收。DMA場景里是EP自己發(fā)起MRd去讀系統(tǒng)內(nèi)存RC反而變成Completer。所以“發(fā)起者”和“完成者”不是固定身份而是針對某一次事務而言的。本文主線是CPU讀EP所以RC是RequesterEP是Completer。1.3 從CPU Load到TLP請求從哪里誕生的回到那行代碼。假設(shè)地址0x8800_1000落在上圖中FPGA的BAR0里枚舉時分配的范圍是0x8800_0000到0x8800_FFFF。RC收到讀請求后查窗口表發(fā)現(xiàn)目標在端口1的Switch下游于是開始構(gòu)造一個Memory Read請求MRd。構(gòu)造一個TLP需要什么信息目標地址0x8800_1000、請求主體的IDRC自己的BDF通常是00:00.0一類、讀多少字節(jié)CPU這條load讀4字節(jié)即一個DW、以及給這個“在途請求”分配一個唯一的Tag編號。這就是事務層工作的精髓把CPU側(cè)簡單的“讀這塊地址”翻譯成PCIe總線側(cè)標準化的、帶完整上下文的TLP。上下文信息包括“誰發(fā)的、要讀哪里、讀多少、怎么標識這次請求”沒有這些對端EP就算收到請求也無法答復就算答復了RC也不知道該把數(shù)據(jù)送回哪里、匹配給哪個CPU指令。2. 拆開TLP事務層協(xié)議的基本句型2.1 TLP的總體輪廓頭、載荷、尾巴TLP是Transaction Layer Packet的縮寫是事務層交換信息的基本單元。一個TLP由三部分組成TLP Prefix可選、TLP Header固定必須有、Data Payload部分類型才有以及可選的TLP Digest用于端到端CRC校驗。你可以把TLP想象成快遞包裹Header是面單寫著收件人和寄件人信息Data Payload是貨物本身Digest是貼在上面的防偽標簽。對于一次內(nèi)存讀的MRd請求來說它只有一個Header——貨物為空因為你只是去問別人要東西自己身上不用帶東西。真正帶數(shù)據(jù)的包頭在回程的CplD里。事務層和上下層的關(guān)系是事務層負責組裝/拆解TLP、做路由判斷和流量控制數(shù)據(jù)鏈路層在TLP前面加Sequence Number、在尾部加LCRC并負責ACK/NACK重傳物理層再把數(shù)據(jù)流拆成一個個字符Symbol做編碼、串行化發(fā)送。從語義角度你只需要記住TLP在發(fā)送方的事務層出生在對端的事務層才算真正死亡中間經(jīng)過的鏈路層和物理層只負責“運輸”不修改TLP內(nèi)容。2.2 逐字段拆解MRd請求頭MRd請求的Header長度可以是3DW32位地址或4DW64位地址。一個DW是4字節(jié)所以4DW就是16字節(jié)。本文例子用64位地址所以是4DW頭。Header最關(guān)鍵的是前兩個DW和地址字段我把關(guān)鍵字段拆開講字段長度作用例子值Fmt[1:0]2bit頭長度和是否有載荷。MRd 64位是01表示4DW無數(shù)據(jù)01Type[4:0]5bit事務類型。MRd是0000000000TC[2:0]3bit流量類別默認0用于VC映射000TD/EP2bitDigest是否存在、包是否被標記為毒化0/0Attr[1:0]2bit排序/一致性屬性如Relaxed Ordering、No Snoop00Length[9:0]10bit數(shù)據(jù)載荷長度單位是DW。讀4字節(jié)11Requester ID[15:0]16bit請求方BDFRC自己的門牌0000Tag[7:0]8bit請求標簽用于匹配完成包2AFirst/Last DW BE8bit首尾DW的字節(jié)使能表示讀哪些字節(jié)F/FAddress[63:32]32bit目標地址高32位0Address[31:2]30bit目標地址低30位低2位恒為0DW對齊88001000綜合起來這批請求就是一個64位尋址的內(nèi)存讀請求目標地址0x8800_1000讀取1個DW4字節(jié)由BDF 00:00.0的RC發(fā)起Tag編號420x2A。關(guān)于Tag多說一句Tag是8位所以理論上一個Requester最多同時有256個“在途請求”。這個編號就是讓回程的CplD能對上號用的身份證。對CPU讀來說RC要保證同一時刻每個未完成請求的Tag不重復這個在后面章節(jié)展開。2.3 一個具體的MRd例子根據(jù)上面的字段構(gòu)造出來的一幀MRd報文十六進制字節(jié)流大致長這樣20 00 00 01 00 00 2A 0F 88 00 10 00 00 00 00 00我來逐個字節(jié)對上。20是Fmt01、Type00000拼出來的001 0000000代表TC0、TD0、EP0、Attr0000 01是Length表示1個DW00 00是Requester IDBDF00:00.02A是Tag0F是首尾字節(jié)使能都有效讀4字節(jié)全要后面的88 00 10 00 00 00 00 00就是64位地址0x00000000_88001000。這里不糾結(jié)抓包工具的大小端顯示差異你只需要建立起“字段和值能對上”的直覺。真正上手調(diào)試時看到一幀16字節(jié)的Header能一眼認出這是不是MRd、目標地址是什么、Tag是多少這個能力比背協(xié)議快得多。注意一個細節(jié)讀請求的Length和字節(jié)使能是配合使用的。Length告訴對端“這次總共要讀幾個DW”First/Last BE告訴對端“首尾DW里具體要哪幾個字節(jié)”。當Length1時首尾BE都作用于同一個DW所以通常都寫成有效。2.4 Length、MRRS與多DW讀請求的關(guān)系如果CPU一次讀8字節(jié)或者DMA一次讀128字節(jié)MRd的Length會相應變大。但是Length不能無限大它要受MRRSMax Read Request Size約束。MRRS是PCIe設(shè)備能力寄存器里的一個參數(shù)常見值是128B、256B、512B、4KB。它規(guī)定了單個讀請求最多能請求多少數(shù)據(jù)超過就要拆成多個請求。舉個例子軟件想從EP讀取1KB數(shù)據(jù)MRRS是256B那么RC會拆成4個MRd請求每個請求Length64DW256字節(jié)分別分配不同的Tag。對端EP對這4個請求分別返回CplDRC再把4個響應拼起來交給軟件。這個“拆”的過程對軟件透明但你在協(xié)議層抓包時看得清清楚楚。MRRS設(shè)得越大讀性能越好請求數(shù)量少、Tag占用少、完成包數(shù)量少但代價是EP側(cè)內(nèi)部的響應邏輯復雜度上升、緩沖區(qū)要求變大。很多FPGA開發(fā)者喜歡把MRRS設(shè)成128B來簡化設(shè)計代價就是吞吐上不去。這個矛盾后面還會再提。3. 請求的下行之旅地址路由與層層轉(zhuǎn)發(fā)3.1 三種路由方式地址、ID、隱式MRd請求構(gòu)造好后從RC的端口發(fā)出去。在PCIe總線世界里一個包能不能準確到達目標取決于路由方式。一共三種地址路由Memory請求和IO請求用目標地址來路由根據(jù)地址落在哪個設(shè)備的地址窗口決定走向。ID路由Completion請求和Configuration請求用BDF來路由根據(jù)Requester ID/Completer ID確定去向。隱式路由Message請求用系統(tǒng)約定好的特殊路由比如廣播給所有設(shè)備或只給RC不需要具體地址和ID。MRd屬于Memory請求所以用的是地址路由。這也是為什么讀請求的Header里必須攜帶完整的目標地址——它是這一站一站路線的判斷依據(jù)。3.2 地址路由在Switch上的決策過程假設(shè)RC的端口1連著Switch而FPGA在Switch的下游口B。RC發(fā)出的MRd到達Switch的上游口后Switch內(nèi)部要做一次查表這個地址是不是我某個下游端口下設(shè)備的窗口這個表是怎么來的交換機內(nèi)部有多個橋Bridge每個下游端口對應一個PCIe橋。枚舉時軟件通過配置這些橋的Base/Limit寄存器告訴Switch“哪個下游口負責哪一段地址區(qū)間”。比如上游口收到目標地址0x8800_1000的包一查就知道要轉(zhuǎn)到下游口B因為FPGA的BAR0窗口覆蓋了這個地址。如果地址不在任何下游端口的窗口里Switch會把它交給上游口繼續(xù)往上走對RC來說就是回到更高層級的RC端口。如果一路都沒有設(shè)備認領(lǐng)RC會收到一個URUnsupported Request錯誤后面章節(jié)細說。這個“查表轉(zhuǎn)發(fā)”的過程和網(wǎng)絡交換機的MAC地址表非常像只是PCIe交換機轉(zhuǎn)發(fā)的依據(jù)是地址窗口而不是MAC地址。理解了這一點很多路由問題就變得很直觀。3.3 發(fā)送側(cè)的TLP沒被吃掉鏈路層和物理層做了什么MRd從RC的事務層“交給”數(shù)據(jù)鏈路層后事務層的使命就先暫停了。數(shù)據(jù)鏈路層給TLP加上一個序列號Sequence Number和LCRC校驗值然后存到重傳緩沖區(qū)里。發(fā)送過程中如果對端的鏈路層發(fā)現(xiàn)LCRC錯誤會回一個NACK發(fā)送方就從緩沖區(qū)里取出來重發(fā)如果收到ACK說明包已經(jīng)安全到達緩沖區(qū)里的副本就可以釋放。再下一層是物理層。物理層把包加上一些定界符編碼后變成一串比特流通過SerDes差分對高速發(fā)出去。這個過程涉及8b/10b或128b/130b編碼、均衡Equalization就是熱詞里常出現(xiàn)的pcie均衡概念、時鐘恢復等。這些話題是鏈路層/物理層的主角與本文主線無關(guān)就不展開了。有一個值得記住的點數(shù)據(jù)鏈路層只保證“包到了對端鏈路層”不感知TLP語義。它不知道這個包是讀請求還是寫數(shù)據(jù)也不知道目標是哪個EP。TLP只要過了LCRC校驗就原樣交給對端事務層由事務層來做真正的路由裁決。3.4 接收端EP的解析流程與BAR匹配MRd到達FPGA的PCIe硬核后事務層開始驗收。流程可以拆成幾步第一步判斷這個包是不是發(fā)給自己的。FPGA的PCIe IP核對地址路由要做一次BAR匹配地址0x8800_1000落在BAR00x8800_0000~0x8800_FFFF范圍內(nèi)匹配成功接收如果落在所有BAR之外事務層必須立即生成一個錯誤完成包UR送回給請求方絕不能默默丟棄。第二步檢查流控信用。FPGA內(nèi)部維護著接收緩沖區(qū)信用值如果MRd所需的非Posted信用不夠事務層會把這個包掛起等對端發(fā)來信用更新。這一步是防緩沖溢出的閘門。第三步把請求轉(zhuǎn)成內(nèi)部總線操作。FPGA一般會在PCIe硬核后面接一個AXI橋Xilinx/Intel的PCIe IP都這么做把MRd翻譯成AXI總線的讀地址通道請求。從地址偏移量和BAR基地址計算出內(nèi)部寄存器/內(nèi)存的偏移然后驅(qū)動內(nèi)部邏輯把數(shù)據(jù)準備好。這一步對FPGA開發(fā)來說是最容易出現(xiàn)問題的環(huán)節(jié)——PCIe協(xié)議本身沒問題但AXI橋的地址映射、讀響應時序、跨時鐘域處理都可能讓事務層永遠等不到數(shù)據(jù)。3.5 如果地址誰都不認UR和CAEP內(nèi)部訪問目標資源也可能失敗。比如地址映射到了EP內(nèi)部一段沒有實際存儲器的保留地址AXI端返回錯誤。這時PCIe事務層會生成一個**Completer AbortCA**完成包告訴請求方“我認了這個地址但我自己執(zhí)行失敗了”。UR和CA是兩種不同的錯誤語義UR是“沒設(shè)備認領(lǐng)這個請求”CA是“有設(shè)備認領(lǐng)但執(zhí)行失敗”。板卡調(diào)試時看到這兩種錯誤排查方向完全不同。UR優(yōu)先查地址映射、路由窗口、BAR配置CA優(yōu)先查EP內(nèi)部邏輯、AXI訪問狀態(tài)。4. Completion的回程請求的后半段才是最復雜的4.1 EP如何裝配一個CplDFPGA內(nèi)部讀邏輯把4字節(jié)數(shù)據(jù)取回來后會交給PCIe事務層由事務層生成一個**Completion with DataCplD**包。CplD的Header也是3DW關(guān)鍵字段如下字段說明Fmt/TypeCplD是010 01010表示3DW頭帶數(shù)據(jù)TC必須和發(fā)起請求的TC一致Completer ID填寫EP自己的BDF如04:00.0Status成功是000UR是001CA是010Tag必須原樣回填請求里的TagByte Count本次完成包攜帶的字節(jié)數(shù)Lower Address本次返回數(shù)據(jù)在請求起始地址中的低7位偏移注意一個命名坑Cpl Header里那個字段雖然叫“Requester ID”但對Completion來說它填寫的是Completer也就是EP自己的BDF。頭一次看協(xié)議的人很容易被這個字段名帶偏。這里要填你的門牌號不是RC的門牌號。CplD的載荷就是讀取到的數(shù)據(jù)按Length字段指示的DW數(shù)量搬運。一次MRd如果可以返回全部數(shù)據(jù)就一個CplD完成如果數(shù)據(jù)量太大EP可以拆成多個CplD每個帶有自己的Byte Count和Lower Address方便RC重組。4.2 ID路由Completion不靠地址回家CplD沒有地址字段所以它不能用地址路由。它是靠ID路由回家的Switch在收到CplD時讀取Header里的Requester ID也就是RC的BDF查ID路由表看這個ID屬于哪個端口。舉個例子如果RC的Requester ID是00:00.0而CplD從FPGA返回到達Switch的下游口BSwitch一查ID路由表發(fā)現(xiàn)00:00.0在上游方向就把包轉(zhuǎn)到上游口送到RC的端口1。RC的端口1再根據(jù)ID和內(nèi)部端口映射把CplD交給負責CPU讀請求的那個RC部件。這解釋了為什么Enumuration階段BDF分配如此重要BDF不只是“門牌號”它本身就是路由表的一部分。如果枚舉時某個設(shè)備的BDF分配異?;蛘唑?qū)動里讀到了虛假的Device ID那么后續(xù)的Completion路由就會跟著出錯。另一個關(guān)鍵點當系統(tǒng)里RC有多個端口時RC側(cè)自己也必須維護一個“ID→端口”的映射表否則CplD從哪個口回來、該交給哪個CPU核都有可能放錯位置。4.3 RC側(cè)的解包Tag索引與“在途請求表”CplD到達RC事務層后接下來就是匹配過程。RC內(nèi)部維護著一張在途請求表Outstanding Request Table每個未完成的讀請求占一個表項表項的索引就是Tag。RC拿到CplD后先檢查Tag查表找到對應請求再檢查Status和Byte Count把數(shù)據(jù)整理好轉(zhuǎn)成CPU總線能識別的讀返回數(shù)據(jù)。對于多CplD的讀請求比如256B的MRd拆成兩個128B的CplD返回RC要一直等齊所有CplD才能結(jié)束這個表項、釋放Tag。釋放Tag很重要Tag不釋放這個“槽位”就永遠占著后續(xù)新請求沒有Tag可用吞吐就會掉到地板。很多性能問題的根因就在“Tag池被耗盡”。所以排除吞吐問題的時候除了調(diào)MRRS/MPS記得看一眼在途請求深度有沒有打滿。4.4 拆包返回為什么一個讀請求可能拆成多個CplD同一個MRd請求EP可能發(fā)回多個CplD原因有幾種數(shù)據(jù)量超過MPSMax Payload Size。MPS規(guī)定了單個TLP最多攜帶多少字節(jié)載荷讀寫都受此約束。如果請求了256B而MPS只有128BEP必須拆成兩個CplD。EP內(nèi)部生成數(shù)據(jù)的時間不連續(xù)比如從慢速接口取數(shù)可以先返回一部分之后再返回剩余部分。設(shè)備支持“任意字節(jié)數(shù)完成”不是一下子返回全部這是允許的只要最終所有CplD的Byte Count合計等于請求長度。這些CplD可以亂序到達嗎可以。因為每個CplD都帶著Lower Address和Byte CountRC完全可以根據(jù)這兩個字段重組。這也是為什么協(xié)議里專門給CplD設(shè)計了這些字段而不是簡單地按順序堆數(shù)據(jù)。多個不同請求的CplD返回順序也可以和請求順序不一致。RC靠Tag區(qū)分是哪個請求靠Byte Count重組順序不需要對端回來得整整齊齊。4.5 錯誤完成的幾種面孔UR/CA/CRS速查Completion Status字段只有3位常見值如下Status值名稱含義000SC成功完成001UR不支持的請求地址無人認領(lǐng)010CA完成者中止EP自己執(zhí)行失敗011CRS配置請求重試狀態(tài)EP還沒準備好CRS比較特殊它只用于配置請求讀配置空間早期EP固件還在初始化時的情形。RC收到CRS后可能重試重試到超時后放棄。熱詞里提到的“pcie熱插拔功能”就和CRS有密切關(guān)系板卡剛插入、鏈路還在訓練、EP的配置邏輯還沒起來時CRS是保護EP不被過早訪問的機制。但對于內(nèi)存讀請求來說標準規(guī)定不允許返回CRS一般都是UR/CA。拿到一個錯誤完成包后RC事務層會把它記錄到AER錯誤狀態(tài)里如果使能了Advanced Error ReportingCPU則可能收到一個Machine Check或NMI。Linux下調(diào)試時dmesg里看到“PCIE Bus Error: severityUncorrected, Unsupported Request”基本就是UR。5. 看不見的交通燈Tag、流控與并發(fā)限制5.1 Tag是飛行中請求的身份證前面已經(jīng)反復強調(diào)Tag的作用這里把它系統(tǒng)化。Tag是一個8位編號RC每發(fā)一個新讀請求就占一個Tag。在請求得到全部完成包之前這個Tag不能復用。所以RC的并發(fā)度Outstanding Requests上限是256。注意Tag分為非Posted請求和Completion兩類信用空間。實際控制芯片中RC可能限制Tag池更小比如某些Root Complex只支持幾十個在途請求。這就是為什么DMA性能高不上去的瓶頸有時在CPU側(cè)而不是設(shè)備側(cè)。對于FPGA的EP設(shè)計來說Tag處理是個隱藏考點EP收到MRd時除了在CplD中回填Tag之外不需要為Tag做什么保留——RC才是Tag的所有者。但如果EP自己作為Requester發(fā)起DMA讀它就需要管理自己的Tag池了。5.2 流控信用額度維持的秩序流控Flow ControlFC是事務層另一個核心機制??梢园阉斫獬梢粭l雙向的“備菜額度”發(fā)送方每發(fā)一個TLP就要消耗接收方給它預留的一個倉位接收方處理完一個TLP后通過UpdateFC DLLP把額度返還。流控按事務類型獨立管理分三組PostedP、Non-PostedNP、CompletionCPL。每一組又分成Header信用和數(shù)據(jù)信用。MRd是Non-Posted請求消耗NP Header信用CplD是Completion消耗CPL Header和CPL Data信用。鏈接訓練完成后兩端通過InitFC1/InitFC2 DLLP交換信用上限之后正常收發(fā)運行時雙方都按額度來。信用一旦耗盡發(fā)送方必須等不能硬發(fā)。這就是為什么打高吞吐時會看到很多UpdateFC包來回飛——它們在補充“倉位”。流控死鎖是理論上的經(jīng)典問題假如RC發(fā)出大量請求把EP的信用耗盡而EP必須靠發(fā)送CplD才能騰出信用同時CplD又被RC的接收窗口堵住兩邊就互相等。PCIe從協(xié)議層面和驅(qū)動約束層面都做了設(shè)計防止這種死鎖工程師一般不用操心但調(diào)FPGA IP時如果看到“傳輸卡死在FC狀態(tài)”就要往這個方向想。5.3 Posted、Non-Posted與Completion是三條獨立車道PCIe事務按是否需要響應分成三類這是理解事務層行為框架的基礎(chǔ)Posted事務發(fā)完即走不需要對端回復。典型是Memory WriteMWr。因為不需要回確認Post的信用消耗小、效率高適合大流量寫數(shù)據(jù)。Non-Posted事務需要對端回復。典型是MRd和IO請求。Completion事務是Non-Posted的響應典型是Cpl/CplD。這三個類別在流控和排序上是獨立的。MWr不會被MRd阻塞在默認排序規(guī)則下posted請求可以越過前面更早的non-posted請求只要不發(fā)生一致性沖突。這聽起來違反直覺但這是PCIe為了不讓寫操作被慢速讀拖住而特意設(shè)計的。CplD也有排序規(guī)則比如它不能無限期被后續(xù)的Posted Write超越其細節(jié)在PCIe Base Spec的Ordering章節(jié)里定義得非常細。我把排序規(guī)則的核心說清楚保證的是數(shù)據(jù)一致性。一個設(shè)備發(fā)出MWr之后又發(fā)一個MRd如果MWr還沒到MRd先到對端讀到舊數(shù)據(jù)這顯然是錯的。協(xié)議通過給每一類事務定義“能否越過其他事務”的規(guī)則來避免這種情況。調(diào)試時遇到讀回臟數(shù)據(jù)的詭異問題別只查邏輯先把排序規(guī)則捋一遍。5.4 VC與TC給流量分專用的通道Virtual ChannelVC是流控的物理載體Traffic ClassTC是包的優(yōu)先級標簽。默認所有包都是TC0走VC0。TC0/VC0之外的VC需要顯式配置和初始化。為什么有VC因為PCIe想讓不同類型流量物理隔離。比如視頻流的高帶寬低時延數(shù)據(jù)可以和普通CPU讀寫走不同VC互不干擾。配置多VC要求每個VC做流控初始化、仲裁配置復雜度直線上升大多數(shù)系統(tǒng)只用VC0。對常見的主機CPU讀寫場景TC/VC只是背景知識。但如果是做交換機/Switch芯片的或者嵌入式環(huán)境要做確定性時延的VC就是必修課了。本文點到為止。6. 現(xiàn)場排查內(nèi)存讀異常怎么抓6.1 現(xiàn)象一讀回來全FF像設(shè)備不存在板卡調(diào)試最經(jīng)典的場景CPU去讀BAR空間讀回來全是0xFFFFFFFF或者直接報UR。排查思路按下述順序來先確認設(shè)備有沒有被枚舉成功lspci -vvv能不能看到設(shè)備BDF對不對確認BAR有沒有被分配地址讀配置空間里的BAR寄存器是不是非零值BAR0的值是否和你期望的地址段一致確認訪問地址是否落在BAR范圍內(nèi)用setpci直接讀BAR然后手動構(gòu)造一個落在BAR地址范圍內(nèi)的訪問。確認RC側(cè)有沒有對應的出站映射對帶自定義RC的嵌入式平臺這一步特別容易漏。如果在Linux下看到“Unsupported Request”UOS錯誤日志里通常能抓到端倪。用debugfs的aer或lspci -xxx看配置空間能讀到AER狀態(tài)寄存器里的錯誤類型。UR的根源九成在地址映射路徑不對才是重點。6.2 現(xiàn)象二EP收到請求但一直沒完成如果EP側(cè)邏輯分析儀/ILA里已經(jīng)看到MRd進來了地址也對但就是沒有CplD發(fā)回去問題基本出在三個方面EP內(nèi)部AXI側(cè)卡住讀請求轉(zhuǎn)成AXI后讀數(shù)據(jù)通道永遠沒有響應。檢查AXI的ready/handshake、檢查被讀寄存器/內(nèi)存的復位狀態(tài)。流控信用耗盡EP的Completion信用沒有初始化或者被大量CplD占滿。查InitFC寄存器的值看RC有沒有分配信用。Tag/狀態(tài)機死鎖EP的狀態(tài)機設(shè)計不良比如要求所有CplD必須按特定順序返回但RC不配合。這里有個實用的建議用Xilinx/Intel PCIe IP核時強烈建議在IP核的事務層入口拉出AXI接口進行觀測。把MRd變成一筆AXI讀請求的過程完全在AXI側(cè)可見問題定位速度能快幾倍。6.3 現(xiàn)象三Switch拓撲下路由失效系統(tǒng)里掛了Switch后問題模式會變多。典型的一種是直接掛在RC端口上的設(shè)備讀寫正常但Switch下游的設(shè)備讀不到。排查重點換到路由表上。Switch上游口的Bridge Base/Limit寄存器決定哪個地址范圍轉(zhuǎn)給下游ID路由表決定Completion回家方向。如果Switch固件或枚舉邏輯沒配好這些窗口Crossing的包就會走錯路。另一個常見問題是MPS/MRRS不一致。如果一個設(shè)備的MPS只有128B而RC發(fā)來的MRd長度是256BMRRS256BEP會因為“請求長度超過自己能處理的范圍”直接回UR或直接丟棄。檢查兩端的MPS/MRRS協(xié)商結(jié)果確保它們是一致的這是Switch環(huán)境下最容易忽略的坑。6.4 內(nèi)存讀故障速查表現(xiàn)象可能原因優(yōu)先排查項讀回全FF地址未映射/設(shè)備未枚舉lspci、配置空間BAR值UR錯誤完成地址路由失敗/請求類型不支持RC出站窗口、EP BAR匹配CA錯誤完成EP內(nèi)部訪問失敗AXI側(cè)邏輯、EP內(nèi)部地址空間讀超時無完成信用不足/內(nèi)部忙/死鎖FC寄存器、ILA觀測AXI讀性能極低Tag池太少/MRRS太小在途請求深度、MRRS/MPSSwitch下讀失敗路由窗口未配對Bridge Base/Limit寄存器6.5 給FPGA開發(fā)者和驅(qū)動工程師的調(diào)試建議如果你是FPGA工程師兩條經(jīng)驗最值得記住第一條把PCIe事務層的調(diào)試關(guān)口前移到AXI側(cè)。不要在物理層或鏈路層面糾結(jié)太久事務層做完BAR匹配后基本都會轉(zhuǎn)成AXI/Avalon接口。AXI上看到什么地址、什么長度、什么響應幾乎等價于協(xié)議層看到的TLP但調(diào)試起來直觀得多。ILA抓AXI總線比抓PCIe總線簡單這是無數(shù)項目驗證過的路徑。第二條Completion的構(gòu)造順序不要想當然。有些IP核允許CplD亂序返回有些IP核內(nèi)部強制順序處理。如果是自己寫事務層狀態(tài)機強烈建議先用最簡單的“順序處理、一次返回全部數(shù)據(jù)”模式跑通再考慮拆分和亂序優(yōu)化。先把正確性做出來性能是后面的事。驅(qū)動工程師則建議從Linux的lspci -vvv、setpci、/sys/bus/pci/devices/*/config這些基礎(chǔ)工具入手配合AER日志。遇到錯誤不要先懷疑協(xié)議棧先把地址、BAR、路由、MPS/MRRS四個維度查一遍能解決九成問題。寫在最后我最初學PCIe事務層時犯的最大錯誤是把注意力全部放在TLP類型和字段上背得很熟但完全不知道一個包從發(fā)出到返回中間每一步是怎么串起來的。后來開始跟著一個讀請求走完整條路——從CPU地址到RC查表、到Switch路由、到EP讀寫、到CplD返回、到RC靠Tag匹配——整個協(xié)議才真正在腦子里立體起來。這篇寫得比較長就是把這條路走完整、走清楚。下一篇打算講Memory Write的完整旅程或者反過來從EP發(fā)起DMA讀主機內(nèi)存的角度把角色互換講一遍。這兩個方向都值得仔細展開。如果你在實際調(diào)板時也踩過什么玄學坑比如MPS不匹配導致的詭異UR、Tag池打滿后的性能雪崩歡迎來交流。調(diào)試經(jīng)驗這東西多聊一次就少一個半夜對著邏輯分析儀發(fā)愣的人。