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