到調(diào)試)
簡介Zynq平臺中PL通過AXI總線讀寫PS端DDR是不可回避的實戰(zhàn)技能。項目完整覆蓋AXI4-Lite協(xié)議交互、DDR控制器搭建、地址映射、跨時鐘域同步以及非DMA模式下的讀寫驗證適合需要掌握PS-PL高效數(shù)據(jù)交互的FPGA工程師和嵌入式學(xué)習(xí)者。資源以RAR格式打包容量約77.85MB內(nèi)含完整工程源碼、Vivado工程配置與仿真測試文件并附有作者調(diào)試過程中的關(guān)鍵思路注釋便于直接導(dǎo)入工具進行綜合與仿真。目前已有4369人瀏覽學(xué)習(xí)印證了該主題在Zynq開發(fā)中的關(guān)注度與實用價值。通過這套工程讀者可以系統(tǒng)理解AXI握手信號如AWVALID、RVALID、ARREADY等的時序配合掌握DDR控制器設(shè)計中的讀寫請求生成與等待狀態(tài)控制同時獲得一套可直接運行和二次修改的參考設(shè)計有效縮短自主開發(fā)中的排錯周期。 最近有個做圖像采集的朋友來找我說他的Zynq工程卡在PL側(cè)把數(shù)據(jù)送給PS端DDR這一步明明讀回來的數(shù)據(jù)偶爾對、偶爾錯搞了兩天也沒定位到是時序問題還是總線配置問題。聊了幾句我就發(fā)現(xiàn)問題不出在代碼邏輯而是他對AXI總線的幾個關(guān)鍵原則理解得不夠透。正好我之前做過一個完整的“PL通過AXI總線讀寫PS端DDR”工程里面從IP封裝、Block Design搭建到SDK側(cè)Cache操作全都走了一遍踩過的坑也夠多。這篇就把整個工程從架構(gòu)思路到實際驗證完整拆開講適合正在做Zynq開發(fā)、卡在PL與PS數(shù)據(jù)交互環(huán)節(jié)的工程師也適合剛接觸AXI協(xié)議、想搞清楚DDR讀寫鏈路的新手。1. Zynq的PS和PL怎么“通話”先把架構(gòu)底子鋪平1.1 為什么非得繞AXI這一圈很多人第一次接觸Zynq會覺得奇怪PS端明明有DDR控制器PL端想訪問DDR為什么不直接拉個管腳連過去這里有個關(guān)鍵概念必須想明白——Zynq的PS端DDR控制器只歸PS管PL里沒有任何一份合法的“物理地址空間”可以直接去摸DDR的管腳。PL想讀寫DDR唯一的方式是通過PS端暴露出來的AXI Slave接口由PS內(nèi)部的互聯(lián)邏輯轉(zhuǎn)接到DDR控制器再從DDR控制器回到存儲陣列。也就是說PL訪問DDR的路徑是“PL自定義邏輯 → AXI總線 → PS端互聯(lián) → DDR控制器 → DDR顆?!敝虚g任何一環(huán)配置不對數(shù)據(jù)都會翻車。理解了這條鏈路你就知道為什么AXI協(xié)議在這個場景里這么重要。AXIAdvanced eXtensible Interface是AMBA協(xié)議家族的一員在Xilinx的SoC和FPGA上幾乎是無處不在的總線標準。它把讀寫操作拆成五個獨立通道——讀地址、讀數(shù)據(jù)、寫地址、寫數(shù)據(jù)、寫響應(yīng)每個通道有自己的握手信號VALID和READY。這種解耦設(shè)計的最大好處是讀和寫可以完全并行數(shù)據(jù)流不需要等待地址握手完成就能提前準備高吞吐場景下非常有用。壞處也明顯對初次接觸的人來說五個通道的握手關(guān)系容易繞暈尤其是調(diào)試時看到某個READY信號一直拉不高根本不知道是卡在哪個環(huán)節(jié)。1.2 GP、HP、ACP三種橋該怎么選PL訪問PS端DDR其實不止一條路。Zynq-7000的PS端向外提供了三組AXI接口每組性質(zhì)完全不同選錯了后面會吃大虧。AXI_GPGeneral Purpose通用目的接口32位數(shù)據(jù)寬度不帶FIFO緩沖吞吐量最低。適合用來讀寫寄存器、配置少量控制字傳大數(shù)據(jù)流不建議走這條。AXI_HPHigh Performance高性能接口64位數(shù)據(jù)寬度帶可配置的FIFO理論上可以跑到比較高的帶寬是PL大規(guī)模訪問DDR的首選。AXI_ACPAccelerator Coherency Port加速器一致性端口64位直接連到PS端的SCUSnoop Control Unit可以跟CPU的L2緩存保持一致性。比HP接口更聰明但用起來約束也多。我自己的項目里PL高速寫DDR用的是HP接口小批量寄存器配置走GP接口。不要試圖用一個接口干所有事帶寬不匹配會拖慢整體性能。下表是我實測下來的接口特點對比接口位寬內(nèi)部FIFO主要用途典型帶寬估算M_AXI_GP32位無寄存器配置、小數(shù)據(jù)量控制較低S_AXI_HP64位有高速數(shù)據(jù)采集、圖像寫入高接近DDR帶寬上限S_AXI_ACP64位無通過SCU訪問緩存與CPU共享數(shù)據(jù)的加速場景視緩存命中率而定ACPI接口在數(shù)據(jù)量小、重復(fù)訪問同一塊緩存行時優(yōu)勢明顯但做大規(guī)模流式寫入時不一定比HP好因為SCU要維護緩存一致性反而可能引入額外開銷。所以如果只是做圖像傳感器數(shù)據(jù)搬運老老實實用HP接口別為了“聽起來高級”去選ACP。2. Vivado工程搭建Block Design、自定義IP和地址映射2.1 從零建Block Design的完整操作鏈這個工程我建議直接用Vivado的Block Design來搭不用純RTL手寫互聯(lián)邏輯。原因很實在手寫AXI Interconnect要處理仲裁、跨時鐘域、協(xié)議轉(zhuǎn)換容易出錯且不好調(diào)試Block Design里PS端的Zynq核已經(jīng)把互聯(lián)邏輯固化好了操作起來就是拖拽連線的事。具體步驟大概是新建RTL工程器件選你板子對應(yīng)的Zynq型號。創(chuàng)建Block Design添加“Zynq7 Processing System”IP核。雙擊Zynq IP在配置向?qū)Ю锕瓷螪DR內(nèi)存型號根據(jù)板子實際顆粒選、UART1用于打印串口信息、以及你要用的HP接口比如S_AXI_HP0。如果是首次配置建議用“Preset”載入板級預(yù)設(shè)再手動修改省得漏掉DDR參數(shù)。添加自定義AXI IP下一節(jié)詳說和AXI Interconnect把CPU側(cè)接口M_AXI_GP0或M_AXI_HP0連到AXI Interconnect再連到自定義IP的從機口。點擊“Run Connection Automation”讓工具自動連時鐘和復(fù)位。分配地址后Validate Design確認沒有Address Editor未分配的錯誤。Generate Output Products → Create HDL Wrapper → Synthesis → Implementation → Generate Bitstream。這個流程里最容易讓新手栽跟頭的就是第7步。Validate Design之前必須打開Address Editor給每個從機接口分配地址空間。為什么AXI總線是個地址映射系統(tǒng)沒有門牌號的從機主機發(fā)起的讀寫請求根本找不到目標Validate會直接報錯。2.2 自定義AXI-Lite從機讓PL擁有“寫DDR”的能力理論上我們可以直接用Xilinx自帶的AXI GPIO或AXI Bram來測試DDR讀寫但那樣比較局限。我更推薦在Tools → Create and Package New IP里創(chuàng)建一個自定義的AXI-Lite從機IP這樣可以精確控制PL側(cè)寄存器和讀寫行為后面調(diào)試和擴展都方便。創(chuàng)建IP時選擇“AXI4-Lite”接口模板。模板會生成一個標準的從機邏輯框架包含寄存器讀寫和握手控制我們需要修改的是用戶邏輯區(qū)。舉個例子做一個雙向互通的小設(shè)計// 自定義IP的用戶邏輯 // 假設(shè)有4個寄存器 // slv_reg0: 控制寄存器 bit0 開始寫入使能 // slv_reg1: 寫入DDR的目標地址低32位 // slv_reg2: 寫入的數(shù)據(jù) // slv_reg3: 狀態(tài)寄存器 bit0 寫入完成標志 always (posedge S_AXI_ACLK) begin if (S_AXI_ARESETN 1b0) begin write_done 1b0; write_trigger 1b0; end else if (slv_reg0[0] 1b1) begin write_trigger 1b1; end else if (your_ddr_write_logic_busy) begin write_trigger 1b0; // 這里模擬PL發(fā)起寫DDR操作 // 實際中你會把自己的數(shù)據(jù)通道接到這里的握手信號上 write_done 1b1; end end真正的項目里不會只有一個寄存器你會發(fā)現(xiàn)AXI-Lite的寄存器訪問流程是主機先把地址放到AWADDR地址通道數(shù)據(jù)放到WDATA數(shù)據(jù)通道從機同時采到AWVALID和WVALID有效后才把數(shù)據(jù)鎖存到對應(yīng)寄存器再回一個BVALID響應(yīng)。看起來邏輯并不復(fù)雜但握手的時序優(yōu)先級必須處理好否則會出現(xiàn)“寫一次兩次行寫三次就丟”的怪毛病。2.3 地址映射給PL分配一塊PS端DDR的“門牌號”在Address Editor里給自定義IP分配地址時默認會分配到0x43C00000附近這是AXI_GP默認的寄存器區(qū)間。如果你想測試PL訪問DDR還需要在Zynq IP配置里打開HP接口然后在Address Editor里給HP0 Slave接口分配一段地址范圍。比如你可以分配0x1E000000到0x1EFFFFFF這16MB的物理地址就和PS端DDR的某段物理地址對應(yīng)上了。這里要注意一個很容易混淆的點PS端DDR的物理地址到底從哪里開始Zynq-7000的DDR地址通常從0x00100000開始具體要看DDR配置。如果顆DDR是1GB實際可用地址范圍是0x00100000到0x3FFFFFFF。Vivado里給HP接口分配的地址必須落在這個范圍內(nèi)才有效。如果你分配了0xE0000000這種地址PS端的DDR控制器根本解析不到這段地址總線會回一個DECERR錯誤SDK側(cè)讀出來要么是0要么是垃圾數(shù)據(jù)。我在這個環(huán)節(jié)通常分兩步驗證第一步查Address Editor里分配的基地址是否在DDR范圍內(nèi)并記錄下來第二步在SDK里用同一個物理地址去讀寫DDR確認PS端本身能正常訪問再讓PL介入。這個順序能幫你快速判斷問題是在PL側(cè)還是PS側(cè)。3. SDK側(cè)讀寫邏輯地址換算、Cache一致性和數(shù)據(jù)校驗3.1 SDK里的地址到底怎么算Block Design導(dǎo)出硬件后Vivado會生成一個hdf文件SDK或Vitis基于這個文件生成BSP。你在SDK里看到的xparameters.h會自動定義外設(shè)的基地址比如自定義IP的地址是XPAR_XXX_BASEADDR這個宏在代碼里直接引用就行。但如果你想通過PL的路徑去訪問DDRSDK里并沒有專門給你定義一個宏。你需要手動使用Block Design里分配的地址比如0x1E000000。可以用Xil_In32和Xil_Out32直接讀寫物理地址#include xil_printf.h #include xil_io.h #include xil_cache.h #define DDR_BASE 0x1E000000UL #define TEST_LEN 4096 int main() { u32 val, errors 0; volatile u32 *ddr_ptr (volatile u32 *)DDR_BASE; // 第一步CPU寫一段已知數(shù)據(jù)到DDR for (int i 0; i TEST_LEN; i) { ddr_ptr[i] i; } // 寫完后必須Flush DCache保證數(shù)據(jù)真的落到DDR而不是停在緩存里 Xil_DCacheFlushRange(DDR_BASE, TEST_LEN * 4); // 第二步可以從PL側(cè)觸發(fā)讀和寫這里省略PL操作 // 第三步CPU讀回校驗數(shù)據(jù) Xil_DCacheInvalidateRange(DDR_BASE, TEST_LEN * 4); for (int i 0; i TEST_LEN; i) { val ddr_ptr[i]; if (val ! i) { xil_printf(Mismatch at %d: got 0x%08x\r\n, i, val); errors; } } xil_printf(Errors: %d\r\n, errors); return 0; }關(guān)于Flush和Invalidate的區(qū)別很多視頻教程一帶而過但這句話值得反復(fù)強調(diào)Flush是把CPU緩存里的臟數(shù)據(jù)寫回DDRInvalidate是讓CPU下次讀取時強制從DDR重新讀。這兩個操作弄反了就會出現(xiàn)數(shù)據(jù)校驗時好時壞的詭異現(xiàn)象。3.2 Cache一致性是最大的隱形坑如果你用HP接口讓PL直接寫DDR那么數(shù)據(jù)根本沒經(jīng)過CPU緩存DDR里已經(jīng)是新的值了。但CPU在讀這段地址時它不會直接去DDR讀而是先去自己的L1/L2緩存里找如果緩存里恰好有這條緩存行就直接返回舊數(shù)據(jù)。這就是為什么數(shù)據(jù)明明被PL更新了CPU卻讀不到的原因。解決辦法就一句話PL寫DDR之后CPU讀之前做一次Xil_DCacheInvalidateRangeCPU寫DDR之后PL讀之前做一次Xil_DCacheFlushRange。順序不對照樣翻車。我見過一個項目工程師只加了Flush沒加Invalidate結(jié)果圖像數(shù)據(jù)第一幀對第二幀全是殘影查了一周才發(fā)現(xiàn)是緩存老化在搗鬼。如果數(shù)據(jù)量大、實時性要求高還可以考慮兩個進階方案一是用ACP接口讓PL直接和L2緩存保持一致CPU無需手動管理緩存二是把關(guān)鍵緩存行配置為Non-cacheable這樣CPU訪問DDR永遠不經(jīng)過緩存。后者犧牲一點性能但邏輯會簡單很多在某些視頻流應(yīng)用中反而更穩(wěn)。4. 數(shù)據(jù)傳輸?shù)恼_姿勢Burst、對齊和DMA搬運4.1 Burst長度和地址對齊為什么不能亂來AXI協(xié)議里的Burst突發(fā)傳輸是提高DDR讀寫效率的關(guān)鍵。一次突發(fā)傳輸可以連續(xù)讀寫多個數(shù)據(jù)節(jié)拍不必每次單發(fā)一個地址。DDR控制器本身就擅長行激活后連續(xù)讀寫如果PL側(cè)每次只做單次32位傳輸效率會低到離譜。但Burst長度不是想設(shè)多大就設(shè)多大。AXI協(xié)議規(guī)定寫突發(fā)最多可以到256拍讀突發(fā)也有限制加上DDR控制器對Burst長度有內(nèi)部約束常常是16拍或32拍更合適所以實際設(shè)計時建議保守一點。另一個刺頭是地址對齊。比如你在64位總線上發(fā)起一個寫突發(fā)起始地址必須是8字節(jié)對齊否則協(xié)議直接報錯。如果從0x1E000001開始寫很多Interconnect會生成ALIGNMENT錯誤數(shù)據(jù)錯位還不好排查。實際操作里我習(xí)慣做一步強制對齊在PL側(cè)自定義IP里對地址做掩碼處理低3位清零保證64位總線的對齊要求。寧可犧牲少量地址空間也不讓總線在角落里報錯。DDR的Burst長度同理用固定16拍最省心性能也夠用。4.2 大數(shù)據(jù)量搬運為什么要用AXI DMA或Datamover如果你只是想驗證PL能寫DDR自定義IP里寫寄存器控制讀寫也能跑但這種方式在真正傳圖像數(shù)據(jù)時不實用。原因很簡單寄存器方式每次只能寫一個或幾個數(shù)據(jù)帶寬遠不夠。圖像傳感器一幀動輒幾百萬像素每個像素32位如果都靠CPU在SDK里寫寄存器驅(qū)動幀率會掉到?jīng)]法看。實際項目里PL大規(guī)模寫DDR的正確姿勢是使用AXI DMA或AXI Datamover。以AXI DMA為例PS端CPU先配置DMA的源地址、目的地址、傳輸長度和起始命令然后DMA自己按AXI突發(fā)協(xié)議從源地址把數(shù)據(jù)搬到目的地址搬運過程中不需要CPU持續(xù)介入。這樣做的好處肉眼可見DMA可以用滿AXI HP接口的帶寬而且和自定義IP解耦CPU只做配置和收中斷。我的建議是如果你的目標偏數(shù)據(jù)采集類應(yīng)用開發(fā)順序應(yīng)該是先用自定義AXI-Lite IP驗證通路再換成AXI DMA/Datamover吃滿帶寬。直接上DMA雖然也可以但一旦出問題調(diào)試時影響變量太多反而不容易定位。5. 踩坑實錄用ILA抓AXI波形定位讀全FFFF的完整排查鏈路5.1 為什么讀回的數(shù)據(jù)都是全F或者全0這里講一個真實案例。有一次我把PL自定義IP掛在HP接口上PS端通過DMA去讀結(jié)果DMA搬回來的數(shù)據(jù)全是0xFFFFFFFF。我第一反應(yīng)是DDR沒初始化但PS端自己寫讀DDR是正常的。排除硬件問題后我懷疑PL側(cè)的地址沒對齊檢查下來也不是。最后用ILA集成邏輯分析儀掛到自定義IP的AXI接口上抓寫通道和讀通道才發(fā)現(xiàn)問題出在WSTRB信號上。WSTRB是寫數(shù)據(jù)字節(jié)使能相當(dāng)于告訴總線“這次寫哪些字節(jié)有效”。我自定義IP里寫數(shù)據(jù)時直接把WSTRB固定成了4‘b0001結(jié)果每次只寫入1個字節(jié)其他三個字節(jié)被當(dāng)成無效位讀出來自然全是垃圾。把WSTRB改成4’b1111數(shù)據(jù)立刻正常。這個坑很典型協(xié)議文檔里有寫但不實際操作根本不會引起重視。所以遇到讀回數(shù)據(jù)異常第一步不要改PS側(cè)代碼先在PL側(cè)掛ILA觀察AXI握手和數(shù)據(jù)信號。很多時候問題就藏在你看不見的通道細節(jié)里。5.2 握手信號狀態(tài)機和ILA觸發(fā)條件AXI協(xié)議里的VALID和READY是蹺蹺板關(guān)系一個信號拉高的同時另一個必須在同一個時鐘沿有效傳輸才算完成。調(diào)試時最常見的問題是地址通道已經(jīng)握手成功數(shù)據(jù)通道卻一直卡在WVALID拉高、WREADY不拉高或者反過來WREADY等不到WVALID。用ILA抓這個場景時設(shè)置觸發(fā)條件可以這樣寫先設(shè)一個邏輯表達式比如AWVALID AWREADY作為第一級觸發(fā)再設(shè)WVALID WREADY作為第二級觸發(fā)。這樣當(dāng)一次寫事務(wù)完成時ILA會捕獲前后多拍的數(shù)據(jù)你就能看到是哪個信號沒有配合到位。針對Zynq開發(fā)我還遇到過Vivado報“[BD 41-968] AXI interface port is not associated to any clock”這類錯誤。這個報錯的意思是某個AXI接口沒有綁定時鐘。解決方法是回到Block Design在Connection Automation或手動配置里給這個接口指定S_AXI_ACLK時鐘域。不要忽略它直接生成比特流生成出來的硬件大概率不穩(wěn)定。6. 工程交付和后續(xù)擴展建議6.1 一個“完整程序壓縮包”應(yīng)該包含什么既然標題說的是“完整程序壓縮包”這里多說一句交付規(guī)范。我交付給同事或客戶的Zynq工程一般至少包含這些內(nèi)容完整的Vivado工程目錄或者至少包含Block Design的.tcl導(dǎo)出腳本這樣對方可以用腳本重建BDRTL源文件和XDC引腳約束自定義IP的IP打包目錄或者對應(yīng)的.zipSDK/Vitis工程源碼包括BSP相關(guān)配置說明README.md寫明Vivado/SDK版本、板卡型號、DDR顆粒型號、操作步驟和注意事項已經(jīng)編譯好的BIT文件和hdf方便對方快速燒寫驗證不要只丟一個壓縮包就完事。Zynq開發(fā)環(huán)境版本敏感Vivado不同版本打開舊工程經(jīng)常報IP核升級提示有時升完IP的行為還會變。README里寫清楚環(huán)境版本能省掉大量溝通成本。6.2 從這塊板子換到另一塊板子時要檢查什么最后分享一個實際操作經(jīng)驗。當(dāng)你把同一個工程從A板卡移植到B板卡時最容易出問題的三個地方是DDR配置、UART引腳約束和AXI地址分配。DDR顆粒型號不同Zynq IP里的DDR參數(shù)必須重新選否則PS端自檢都可能不過UART引腳變了XDC約束要改B板卡DDR大小不同Address Editor里給HP接口分配的地址范圍也要重新評估是否越界。我自己的習(xí)慣是換板子后先不跑PL邏輯先燒一個PS端最小系統(tǒng)把DDR讀寫和串口打印驗證通過再加載PL部分。這個步驟雖然多花十幾分鐘但能避免PL和PS兩端同時出問題時的雙重折磨。整個工程做下來最大的感受是PL讀寫PS端DDR這件事難點不在寫代碼而在理解AXI的字對齊、緩存一致性、地址映射這些“看不見的規(guī)則”。把這幾個環(huán)節(jié)把握住了數(shù)據(jù)通路就是一條直線。本文還有配套的精品資源點擊獲取