:FPGA加速卡PCIe通信框架解析與快速跑通)
簡介Riffa PCIe 2.2驅(qū)動與源碼分析包是一套面向FPGA開發(fā)者和系統(tǒng)工程師的開源PCIe通信解決方案用于在FPGA與主機(jī)之間構(gòu)建高速PCI-E鏈路支持PCIe 3.0規(guī)范下的4X通道全雙工傳輸場景。壓縮包共535個文件約44.55MB涵蓋Riffa庫核心源碼Verilog/VHDL/C/C、驅(qū)動工程配置xci/xdc/xpr、編譯腳本與構(gòu)建工具以及官方文檔和示例程序目錄結(jié)構(gòu)清晰便于按模塊查閱。目前已有1870人學(xué)習(xí)使用。資源提供可直接部署的驅(qū)動程序、可定制的源碼、配套文檔與示例應(yīng)用并附有VC709等FPGA板卡的bit文件和配置腳本能幫助讀者深入理解PCIe協(xié)議實現(xiàn)、完成驅(qū)動移植與性能優(yōu)化是搭建高效并行處理系統(tǒng)的高價值參考。1. RIFFA 2.2是什么這個zip包憑什么值得收藏最近整理FPGA加速卡的項目資料翻到了riffa_pcie_2.2.zip這個壓縮包順手又把它解壓跑了一遍。RIFFAReusable Integration Framework for FPGA Accelerators是一套開源的主機(jī)與FPGA之間PCIe通信框架2.2版是它在Xilinx 7系列和部分UltraScale平臺上很穩(wěn)的版本。這個zip里并不只是源碼而是把FPGA側(cè)IP核、主機(jī)側(cè)驅(qū)動、用戶態(tài)庫和示例工程全部打包好了。你解壓之后按文檔把FPGA側(cè)的工程綜合出比特流主機(jī)側(cè)編譯驅(qū)動并加載就能看到一個“主機(jī)發(fā)數(shù)據(jù)、FPGA收FPGA回數(shù)據(jù)、主機(jī)讀”的最小閉環(huán)。我為什么會特別推薦這個zip做過PCIe通信的人都知道這玩意兒自己從零搞起來有多疼。先是驅(qū)動層要處理BAR映射、DMA描述符、中斷線程然后是FPGA側(cè)要寫一套和AXI總線對接的狀態(tài)機(jī)忙活一個月能跑通一個最簡單的loopback都算快的。RIFFA的做法是把這個流程抽象成類似socket的讀寫接口讓你先專注于業(yè)務(wù)邏輯而不是一上來就啃PCIe協(xié)議。1.1 它解決的痛點CPU和FPGA之間那堵墻FPGA加速卡的傳輸鏈路本質(zhì)上是把數(shù)據(jù)從CPU內(nèi)存搬到FPGA邏輯里再把結(jié)果搬回去。PCIe點到點帶寬高、延遲低但難點在于“搬運”的整個過程需要硬件和軟件緊密配合主機(jī)側(cè)要有驅(qū)動完成設(shè)備枚舉、內(nèi)存分配、地址映射FPGA側(cè)要有DMA引擎和中斷處理兩端還要約定命令格式。這些事RIFFA都替你做了。它把每條數(shù)據(jù)通道抽象成類似socket的接口主機(jī)端調(diào)用riffa_open、riffa_send、riffa_recv就能完成整包收發(fā)FPGA側(cè)的RTL代碼里則直接使用CHNL_TX和CHNL_RX接口不需要自己寫PCIe配置空間邏輯。對于剛接觸PCIe的開發(fā)者來說這套抽象能直接砍掉好幾周的底層聯(lián)調(diào)時間。1.2 為什么不建議裸寫PCIe驅(qū)動可能有人覺得驅(qū)動就是調(diào)幾個內(nèi)核API沒那么難。但實際操作下來把設(shè)備枚舉、中斷申請、DMA緩沖池管理都串起來時各種邊界情況非常考驗功底。尤其是DMA方向如果把物理地址傳錯、長度沒對齊表現(xiàn)往往不是立即報錯而是內(nèi)存被踩、系統(tǒng)隨機(jī)卡死。RIFFA這類框架的價值在于它把“已經(jīng)被驗證過”的驅(qū)動和硬件邏輯一并給你省去的是最耗時的聯(lián)調(diào)階段。相比之下Xillybus和XDMA也做類似的事但商業(yè)授權(quán)和代碼可讀性往往不如這個開源項目。如果你想把主要精力放在自己的加速算法上而不是花一個月去調(diào)傳輸通道RIFFA是個很合適的起點。2. 拿到zip之后解壓、校驗、目錄結(jié)構(gòu)2.1 下載后先別急著解壓先做兩件事第一件事是校驗完整性。從GitHub Release頁面下載的riffa_pcie_2.2.zip我建議先核對SHA256或者至少看壓縮包大小是不是和頁面一致。之前見過有人從第三方網(wǎng)盤下載結(jié)果文件已經(jīng)損壞解壓時報invalid zip archive: could not find EOCD這種基本是下載不完整不是開發(fā)工具問題。第二件事是確認(rèn)版本匹配。RIFFA 2.2對應(yīng)的FPGA例程、驅(qū)動和用戶態(tài)庫是整體發(fā)布的盡量不要混用其他分支的驅(qū)動或庫否則容易遇到結(jié)構(gòu)體長度不匹配導(dǎo)致的詭異問題。寧可多花五分鐘做這兩個檢查也不要等到編譯階段才發(fā)現(xiàn)代碼和庫對不上。2.2 解壓后的目錄到底該怎么看不同tag下的目錄會有細(xì)微差別但核心內(nèi)容基本是這幾塊fpga目錄放的是Xilinx工程源碼和IP封裝software目錄下分Linux驅(qū)動、Windows驅(qū)動和用戶態(tài)C庫examples目錄放著主機(jī)端示例docs目錄有使用說明和API文檔。打開fpga后你會發(fā)現(xiàn)工程里已經(jīng)例化好了PCIe硬核和一個RIFFA wrapper用戶邏輯掛在wrapper的通道接口上。這個結(jié)構(gòu)很關(guān)鍵你改的是channel接口后面的模塊PCIe配置空間和DMA引擎不用動。換句話說這個zip給你的不是一個黑盒而是一個帶完整RTL源碼、可讀性很好的框架方便你日后做深度定制。2.3 從GitHub下載zip后想轉(zhuǎn)成git倉庫怎么辦不少人習(xí)慣下載zip但項目改到一半想用git版本管理于是直接把解壓目錄拿來做倉庫。這時候如果遠(yuǎn)程倉庫是官方repo直接git pull大概率報refusing to merge unrelated histories因為zip快照和git倉庫的歷史完全不同。正確做法有兩種一是直接git clone --depth 1 https://github.com/.../riffa.git拿完整倉庫二是如果已經(jīng)改了自己的代碼就把官方repo加為remote再git pull --allow-unrelated-histories但要做好沖突處理的心理準(zhǔn)備。我個人建議想長期改就直接clonezip只適合快速試用用git管理之后改動記錄會清楚很多。3. 快速跑通一個最小系統(tǒng)3.1 需要的軟硬件清單FPGA板卡建議用有PCIe硬核的Xilinx 7系列比如KC705、VC707或者比較常見的Artix-7 PCIe開發(fā)板。主機(jī)是普通的x86_64 Linux機(jī)器即可Ubuntu 18.04和20.04我都跑過。Vivado版本不需要太新2018.3到2020.1都沒問題太新的版本在升級IP時需要多花些功夫。PCIe插槽優(yōu)先選x4或以上供電最好用獨立供電的板卡別用那種需要從PCIe金手指取大電流的轉(zhuǎn)接板。鏈路不穩(wěn)定會直接導(dǎo)致后面調(diào)試無從下手所以供電這塊最好一開始就認(rèn)真對待。3.2 FPGA側(cè)在Vivado里把例程變成比特流RIFFA 2.2的FPGA例程通常自帶一個Vivado工程直接打開后第一件事不是綜合而是確認(rèn)器件型號和你的板子一致。之后重點檢查PCIe硬核配置lane數(shù)、Gen速率、參考時鐘源。比如板卡上PCIe參考時鐘是100MHz差分對配置里寫成了125MHz后面鏈路訓(xùn)練怎么都上不去。確認(rèn)無誤后跑綜合實現(xiàn)生成比特流通過JTAG下載到FPGA。下載完先別急打開lspci如果看到Xilinx設(shè)備被枚舉出來了說明物理鏈路已經(jīng)通了可以進(jìn)入主機(jī)側(cè)。這里最忌諱的是“看著配置好像沒錯就直接燒”每次改完板卡都要把PCIe核的配置頁截圖存檔方便后面排查。3.3 主機(jī)側(cè)編譯驅(qū)動并跑通example解壓目錄后進(jìn)入software/linux執(zhí)行make生成riffa.ko。加載驅(qū)動前最好先確認(rèn)PCIe設(shè)備枚舉結(jié)果Vendor ID一般是10eeDevice ID根據(jù)你在Vivado里的配置不同可能有區(qū)別。加載后dmesg看到注冊信息再用examples里的測試程序做一次數(shù)據(jù)回環(huán)。RIFFA例程里通常有一個函數(shù)會先發(fā)送一段特定模式的數(shù)據(jù)再接收回來比對。第一次跑通這個loopback基本上整條鏈路就是好的后面可以開始替換用戶邏輯。如果你的Linux內(nèi)核版本比較新驅(qū)動編譯時可能會報API不兼容這時候優(yōu)先查官方issue區(qū)有沒有補(bǔ)丁而不是自己硬改驅(qū)動。4. 核心知識點鏈路、枚舉、DMA和地址映射4.1 PCIe鏈路訓(xùn)練與link lane插上FPGA板卡之后PCIe鏈路能不能起來屬于物理層LTSSM狀態(tài)機(jī)負(fù)責(zé)的領(lǐng)域。RC和EP兩端會先協(xié)商lane數(shù)和速率。比如RC支持x16EP配置成x4實際鏈路會退到x4。在Linux下用lspci -vvv可以看到LnkSta字段如果顯示LnkSta Down說明鏈路沒有訓(xùn)練成功。一個很常見的原因是板卡的參考時鐘沒接或者頻率不對另一個是PERST#復(fù)位信號時序不符合PCIe規(guī)范。link lane數(shù)直接決定了理論帶寬x1 Gen2只有500MB/s左右x4 Gen2大概2GB/sx8 Gen3能到8GB/s。如果你的RIFFA工程默認(rèn)是x1而測試發(fā)現(xiàn)帶寬上不去不要奇怪先去查配置和實際協(xié)商結(jié)果是否一致。很多開發(fā)板為了兼容性默認(rèn)x1傳輸大量數(shù)據(jù)時就會成為瓶頸。4.2 PCIe枚舉過程與RIFFA的“替身”工作BIOS/UEFI在上電時會掃描PCIe總線上的設(shè)備為每個設(shè)備分配Bus號、Device號、Function號和BAR地址空間。RIFFA的驅(qū)動程序加載后會通過這些BAR地址把FPGA端的寄存器映射到CPU虛擬地址空間后續(xù)所有控制操作都是對一段內(nèi)存地址的讀寫。對于不懂底層的人來說這就是一個“替身”在背后處理好了設(shè)備注冊、地址分配和中斷申請你不用自己寫pci_probe之類的東西。但這不意味著你可以完全忽略枚舉過程。當(dāng)插多張卡或者經(jīng)過PCIe Switch時設(shè)備順序和物理槽位對應(yīng)關(guān)系需要你自己去判斷否則很容易把數(shù)據(jù)發(fā)到錯誤的加速卡上。4.3 inbound/outbound與DMA緩沖區(qū)PCIe地址轉(zhuǎn)換有兩個常見術(shù)語inbound是FPGA發(fā)起的、訪問主機(jī)內(nèi)存的操作outbound是CPU通過BAR訪問FPGA寄存器的操作。RIFFA的DMA發(fā)送和接收本質(zhì)上依賴inbound方向主機(jī)驅(qū)動分配一段物理連續(xù)的內(nèi)存把物理地址通過BAR寫到FPGA寄存器FPGA側(cè)DMA引擎拿到地址后直接讀寫主機(jī)內(nèi)存。這個過程要求buffer地址至少按頁對齊否則DMA可能越界。我在代碼里習(xí)慣用posix_memalign分配4KB對齊的內(nèi)存而不是普通malloc。很多“偶發(fā)性傳輸出錯”的問題根因都出在這里內(nèi)存地址不對齊時驅(qū)動偶爾能跑通一旦系統(tǒng)內(nèi)存碎片化就立刻暴露問題。5. 實戰(zhàn)中的坑我從RIFFA 2.2踩過的問題5.1 鏈路起不來先看LTSSM而不是先看代碼如果你的FPGA板卡插上后Linux下lspci根本看不到設(shè)備說明硬件鏈路就沒通。此時不要急著查RIFFA代碼先用排除法換一個PCIe槽位、檢查參考時鐘和復(fù)位最好用開發(fā)板的ILA抓一下PCIe核內(nèi)部的LTSSM狀態(tài)。很多開發(fā)板默認(rèn)的PCIe參考時鐘差分對沒有接導(dǎo)致Link Training一直卡在Detect狀態(tài)。有一次我就是因為轉(zhuǎn)接板5V供電不足鏈路始終無法穩(wěn)定換獨立供電后一次通過。這類問題有個特點現(xiàn)象看起來像驅(qū)動或軟件問題但實際純硬件。所以我的習(xí)慣是任何PCIe問題排查的第一步永遠(yuǎn)是lspci -vvv看鏈路協(xié)商結(jié)果鏈路沒通就別往下走。5.2 DMA傳輸失敗或數(shù)據(jù)錯位最容易被忽略的細(xì)節(jié)數(shù)據(jù)錯位、DMA超時這類問題排到最后的根因往往是這幾個緩沖區(qū)沒有對齊、傳輸長度超過FPGA側(cè)配置的最大長度、驅(qū)動和固件版本混用。RIFFA在fpga端對單次傳輸?shù)淖畲笞止?jié)數(shù)有參數(shù)限制超過后驅(qū)動接口會直接返回錯誤或者FPGA側(cè)丟棄包。開發(fā)時最好把測試數(shù)據(jù)打上序號比如前4字節(jié)是包序號后面才是payload這樣一旦傳輸錯位立刻能定位是驅(qū)動層丟包還是用戶邏輯拼包問題。另外如果FPGA側(cè)用戶邏輯沒有正確處理frame起始和結(jié)束信號主機(jī)端收到數(shù)據(jù)的邊界就會亂這種錯誤在回環(huán)測試?yán)镒钊菀妆┞丁?.3 多FPGA和PCIe Switch的組合在服務(wù)器里插多張F(tuán)PGA卡或者經(jīng)過PCIe Switch連接時RIFFA仍然能識別多個設(shè)備用riffa_open(index)按設(shè)備索引打開。但索引順序并不代表物理槽位順序最好在應(yīng)用層通過lspci的Bus號建立映射避免程序跑錯卡。經(jīng)過PCIe Switch會多一些跳數(shù)延遲但吞吐一般不會明顯下降前提是Switch端口帶寬足夠。如果你的系統(tǒng)里有多個不同型號的PCIe設(shè)備驅(qū)動加載時還會涉及驅(qū)動綁定順序的問題建議在加載riffa.ko之前先用lspci -k確認(rèn)設(shè)備沒有被其他驅(qū)動占用。5.4 zip解壓和文件權(quán)限的小事從GitHub上下載的riffa_pcie_2.2.zip是不需要密碼的。如果你在解壓時遇到failed to open或者文件權(quán)限異常先檢查是不是解壓到了NTFS掛載分區(qū)導(dǎo)致腳本沒有可執(zhí)行權(quán)限。在Linux下解壓后最好確認(rèn)一下software/linux下的Makefile和工具腳本權(quán)限可讀可執(zhí)行否則后面make會莫名報一些shell錯誤。另外如果壓縮包是在Windows下解壓再傳到Linux的很容易把換行符和權(quán)限一起搞壞不如直接在Linux環(huán)境里重新解壓一份干凈的文件。這個細(xì)節(jié)雖然小但能省掉很多看起來不可理喻的編譯問題。6. 從例程到自己的加速邏輯6.1 替換用戶邏輯的套路RIFFA的FPGA側(cè)給用戶提供的是channel接口通常包含數(shù)據(jù)、有效信號、幀起始、幀結(jié)束和背壓信號。把自己的模塊掛上去時我建議先做一個簡單的FIFO隔離讓跨時鐘域的問題只出現(xiàn)在你和FIFO之間而不擴(kuò)散到RIFFA整個鏈路。偽代碼如下assign chnl0_tx_data user_fifo_rdata; assign chnl0_tx_data_valid user_fifo_valid; assign user_fifo_ren chnl0_tx_ready user_fifo_valid;這段代碼的意思是當(dāng)RIFFA側(cè)準(zhǔn)備好接收且FIFO非空時才允許讀FIFO。注意frame信號一定要和data一起打拍不能只把數(shù)據(jù)流送過去而丟掉邊界標(biāo)記。我自己第一次接用戶邏輯時就是忘了把frame和data對齊導(dǎo)致主機(jī)側(cè)每次收到的包長度都不固定排查了很久才發(fā)現(xiàn)是RTL里少打了一拍。6.2 性能調(diào)優(yōu)方向如果loopback已經(jīng)通了接下來最關(guān)心的就是帶寬。我的經(jīng)驗是優(yōu)先做三件事使用hugepage減少DMA緩沖區(qū)的TLB miss在Vivado的PCIe配置里把Max Payload Size統(tǒng)一在應(yīng)用層用雙緩沖讓DMA傳輸和計算重疊。RIFFA本身支持多通道如果單通道到不了帶寬上限可以拆成多通道并行傳輸?shù)獸PGA側(cè)需要額外邏輯做數(shù)據(jù)分發(fā)。調(diào)優(yōu)時建議先用一個固定長度的環(huán)形buffer反復(fù)做壓力測試記錄吞吐曲線再逐步優(yōu)化瓶頸。不要一上來就追求極限先把功能跑穩(wěn)定更重要。6.3 RIFFA的局限和替代方案RIFFA 2.2畢竟是幾年前的版本對較新的Xilinx器件和Linux內(nèi)核兼容性需要自己測試。如果項目要商用或者需要更底層的控制可以考慮XDMA驅(qū)動或Xillybus。前者在Xilinx官方工具鏈里集成度高后者商業(yè)支持完善。我的看法是評估期先用RIFFA把數(shù)據(jù)通路打通遇到瓶頸再切換到商業(yè)方案比一開始就陷在驅(qū)動開發(fā)里劃算得多。實際上很多正式項目的前期原型驗證都是用RIFFA這類框架做出來的等驗證完業(yè)務(wù)邏輯后再針對性替換底層風(fēng)險會小很多。最后分享一點個人實操體會。每次拿到一個新的FPGA加速板卡我都會先跑一遍RIFFA的loopback例程確認(rèn)PCIe鏈路、DMA和主機(jī)環(huán)境都沒問題再往上疊加業(yè)務(wù)邏輯。這個zip的價值不在代碼多高級而在于它把PCIe通信里最浪費時間的那層“連接檢查”變成了一個半小時內(nèi)能完成的事情。遇到問題也記得先從鏈路、枚舉、緩沖區(qū)對齊這些基礎(chǔ)項排查大多數(shù)坑都不是協(xié)議本身而是環(huán)境和配置細(xì)節(jié)。本文還有配套的精品資源點擊獲取