)
判斷一個Zynq工程師是不是真把數(shù)據(jù)通路吃透了最簡單的問題就是你的PL邏輯打算怎么往DDR里寫數(shù)據(jù)很多人習(xí)慣直接在AXI總線上掛一個寄存器堆或者BRAM轉(zhuǎn)FIFO讓PS用CPU去搬結(jié)果帶寬和實時性雙雙拉胯。真正干這活的其實是AXI Datamover這個IP。這篇就把PS、PL、DDR三者之間最核心的數(shù)據(jù)搬運(yùn)邏輯講透重點說清楚Datamover到底能干什么、怎么干、以及它和AXI DMA那類看起來差不多的IP到底差在哪。本文適合手里有Zynq板子、準(zhǔn)備讓PL側(cè)邏輯直接讀寫DDR做數(shù)據(jù)采集或算法加速的人。你要是從來沒碰過AXI協(xié)議光聽過valid/ready握手那也問題不大我會把鏈接關(guān)系、命令字格式、狀態(tài)返回邏輯一條條拆開講爭取讓剛?cè)腴T的同學(xué)能對著這篇把最小系統(tǒng)搭出來讓已經(jīng)被Datamover折磨過的人也能對號入座找到坑。1. 為什么PL要讀寫DDR以及為什么得靠Datamover1.1 Zynq里PS、PL、DDR之間的真實拓?fù)湎壤砬逡粋€容易被初始概念帶偏的問題Zynq里的DDR物理上是掛在PS側(cè)的。DDR控制器的硬核位于PS內(nèi)部DDR顆粒通過專用的物理接口連到PSPL這邊并沒有獨立的內(nèi)存控制器。也就是說PL想要訪問DDR唯一的路徑就是通過AXI互聯(lián)矩陣把請求送到PS側(cè)的高性能端口比如S_AXI_HP0/1/2/3再由HP端口接到DDR控制器后端。這個拓?fù)錄Q定了三件事第一PL訪問DDR必須發(fā)起AXI內(nèi)存映射Memory Map類型的事務(wù)第二訪問路徑上有一個跨時鐘域的橋接過程吞吐量和延遲都會受AXI互聯(lián)配置影響第三PL側(cè)如果只是做數(shù)據(jù)采集那么數(shù)據(jù)最終必須落到DDR里否則后面的算法和軟件處理無從談起。這么說吧整個Zynq的數(shù)據(jù)通路就像一個倉庫DDR搬運(yùn)通道AXI互聯(lián)被安排在倉庫旁邊而Datamover就是一個專業(yè)的叉車司機(jī)。你寫RTL時如果想親自實現(xiàn)AXI讀突發(fā)、寫突發(fā)、突發(fā)長度控制、地址遞增那復(fù)雜度會迅速爆炸而且很容易在握手時序上出錯。Datamover存在的意義就是把這些底層的AXI事務(wù)細(xì)節(jié)全部封裝掉對外只提供一套簡潔的AXI4-Stream接口。1.2 用CPU搬運(yùn)數(shù)據(jù)的三個痛點也有人問那我直接在PL里掛一個AXI接口讓PS通過CPU循環(huán)讀寫不行嗎行但會有三個明顯的坑。第一個坑是帶寬。PS的CPU通過M_AXI_GP口訪問PL時數(shù)據(jù)位寬一般只有32位而且每次訪問都需要穿越整個AXI互聯(lián)CPU還要執(zhí)行l(wèi)oad/store指令。做大數(shù)據(jù)量搬運(yùn)時這種方式的帶寬根本跑不滿DDR控制器只能眼睜睜看著CPU占用率拉滿、DDR帶寬閑著。第二個坑是實時性。CPU搬運(yùn)本質(zhì)是請求-響應(yīng)模型兩次讀之間CPU還要做循環(huán)控制、地址更新、結(jié)束判斷這就引入了不可控延遲抖動。在高速數(shù)據(jù)采集場景下ADC往FIFO里灌數(shù)據(jù)的速度可不管你的CPU忙不忙只要上游FIFO滿一次采樣就丟點。第三個坑是CPU被完全占死。如果搬數(shù)據(jù)的活全讓PS側(cè)核心干了那PS就無法同時跑Linux、做協(xié)議棧處理、響應(yīng)中斷。對很多應(yīng)用來說CPU應(yīng)該去干更復(fù)雜的事情機(jī)械的搬數(shù)據(jù)工作應(yīng)該交給專用硬件。所以PL直接讀寫DDR的標(biāo)準(zhǔn)方案就是在PL里例化一個Datamover讓這個IP替你去執(zhí)行AXI內(nèi)存映射端的讀寫事務(wù)數(shù)據(jù)的生產(chǎn)和消費則通過AXI4-Stream接口對接。這樣CPU只需要下發(fā)一條命令剩下的搬運(yùn)全部由硬件完成。2. 先弄清Datamover的三個通道和控制模型2.1 指令通道把傳輸需求寫成一條命令字Datamover最核心的思想是它自己不產(chǎn)生數(shù)據(jù)只是一個執(zhí)行引擎。你要它干什么得通過一條命令告訴它。這條命令走的是S_AXIS_MM2S_CMD讀側(cè)或者S_AXIS_S2MM_CMD寫側(cè)本質(zhì)上就是一個AXI4-Stream接口。命令字的位寬默認(rèn)是72位跟具體IP版本和配置有關(guān)核心字段就兩個一個是目標(biāo)地址就是你想從DDR哪里讀、或者往DDR哪里寫另一個是傳輸字節(jié)數(shù)BTTBytes to Transfer告訴它要搬多少數(shù)據(jù)。有些配置里還有tag字段方便你在狀態(tài)返回時區(qū)分是哪一條命令完成了。正經(jīng)的命令流格式不同Vivado版本略有差異但大邏輯是一樣的。以讀傳輸為例你往S_AXIS_MM2S_CMD通道扔一條命令字其中包含起始地址、長度Datamover就會自動把這次傳輸拆成一筆或者多筆AXI讀突發(fā)從DDR把數(shù)據(jù)讀出來然后從M_AXIS_MM2S端口流式輸出。傳輸完成后它會從M_AXIS_MM2S_STS端口回一條狀態(tài)。這里有個細(xì)節(jié)特別重要命令通道的握手是標(biāo)準(zhǔn)的AXI4-Stream valid/ready機(jī)制??刂七壿嫴荒芟氘?dāng)然地認(rèn)為我發(fā)出命令就完成了必須等到valid和ready同時拉高的那個時鐘上升沿才算命令真正被接收。很多初學(xué)者卡死在這里后面專門講。2.2 狀態(tài)通道和數(shù)據(jù)通道怎么配合Datamover本質(zhì)上幫用戶省掉了三類復(fù)雜工作AXI突發(fā)拆分、地址遞增、DDR控制器時序管理的適配。所有這些對用戶來說都是透明的黑盒用戶只需要盯著一組Stream接口。整個系統(tǒng)有兩種工作方向?qū)?yīng)兩個完全對稱的通道。MM2S方向是Memory Map to StreamS2MM方向是Stream to Memory Map。MM2S把DDR數(shù)據(jù)讀出來送到PL內(nèi)部S2MM把PL內(nèi)部的數(shù)據(jù)寫入DDR。每個方向都有獨立的命令通道、數(shù)據(jù)通道和狀態(tài)通道。狀態(tài)通道返回的狀態(tài)字通常是8位或者32位。狀態(tài)字里除了OK標(biāo)志還有錯誤標(biāo)志位。一旦你在狀態(tài)字里看到錯誤位置1基本就是下面這幾種情況之一地址越界訪問到了DDR地址空間之外、命令格式非法比如BTT配置超過支持范圍、S2MM通道數(shù)據(jù)沒跟上上游數(shù)據(jù)斷流導(dǎo)致內(nèi)部FIFO上溢/下溢。調(diào)試時把狀態(tài)通道拉進(jìn)ILA比瞎猜快得多。數(shù)據(jù)通道則是對外提供的數(shù)據(jù)接口。MM2S的數(shù)據(jù)通道是輸出方向S2MM的數(shù)據(jù)通道是輸入方向。這兩個通道都有valid/ready握手并且在數(shù)據(jù)流之前還有一個tlast信號表示一筆傳輸?shù)淖詈笠淮瓮话l(fā)結(jié)束。如果數(shù)據(jù)流里有tlast你要把它和有效的最后一個數(shù)據(jù)對齊好不然接收端可能在Done判斷上出錯。2.3 它和AXI DMA、AXI CDMA差在哪這三個IP經(jīng)常被放在一起比較功能也確實高度重疊但定位有明顯不同。AXI DMA是一個自帶寄存器管理功能的IP。它內(nèi)部有一組用戶可配置的寄存器PS側(cè)的軟件可以通過AXI4-Lite接口讀寫這些寄存器從而發(fā)起DMA傳輸。這種方式對軟件工程師非常友好因為在Linux里直接操作寄存器就能做DMA搬運(yùn)。但代價是它的控制模型相對固定你很難把它嵌入到很復(fù)雜的PL自定義狀態(tài)機(jī)里。AXI CDMA是Central DMA它最大的特點是同一個引擎既能讀也能寫也就是先從一個地址讀、再直接寫到另一個地址不需要數(shù)據(jù)通過外部邏輯過一次手。這在做DDR內(nèi)部數(shù)據(jù)拷貝時特別高效。Datamover跟它倆不一樣。Datamover沒有用戶寄存器你全靠往命令通道塞命令字來驅(qū)動它。這種設(shè)計在一開始確實讓人不適應(yīng)但它的優(yōu)勢在于兩點第一它是純Stream接口的引擎很容易被PL里的自定義邏輯直接控制根本不需要PS參與第二它能同時支持MM2S和S2MM兩個方向并發(fā)工作流式吞吐能力很強(qiáng)。所以當(dāng)你需要在PL側(cè)實現(xiàn)一個完全由硬件自主控制的搬運(yùn)鏈時Datamover往往比AXI DMA順手。3. IP配置里真正值得摳的選項3.1 模式選擇與數(shù)據(jù)寬度、突發(fā)長度Vivado里例化AXI Datamover時首先會問你Enable MM2S還是Enable S2MM或者兩個都要。這個選擇不是越多越好。如果只需要讀數(shù)就只開MM2S可以節(jié)省不少BRAM和LUT資源。如果要做環(huán)回測試或轉(zhuǎn)發(fā)系統(tǒng)那當(dāng)然兩個都開。接下來是Stream Data Width和Memory Map Data Width。這兩個值得單獨說。Memory Map側(cè)的位寬應(yīng)該跟實際DDR控制器端口位寬匹配常見的有64位和128位。Stream側(cè)的位寬則可以相對獨立Datamover內(nèi)部會做位寬轉(zhuǎn)換。但位寬轉(zhuǎn)換是有代價的每跨一次位寬就需要額外的FIFO做緩沖資源占用和延遲都會上去。如果你的上游邏輯本身就是64位建議兩邊都設(shè)成64位減少無謂轉(zhuǎn)換。Max Burst Size決定了Datamover單次AXI突發(fā)能跨多少拍。AXI4協(xié)議規(guī)定INCR突發(fā)最大不能超過256筆。這個值設(shè)大了對DDR帶寬利用有好處因為突發(fā)越長AXI總線上地址階段占用的開銷比例越小。但如果你的下游邏輯比如接一個窄FIFO跟不上數(shù)據(jù)速率突發(fā)太大反而容易導(dǎo)致Stall。我實測下來64位總線上設(shè)16或者32是比較保守穩(wěn)定的選擇128位總線可以試試64或者128。另外有一個Address Width設(shè)置。這個值決定了命令字里地址字段占據(jù)多少位一般跟處理器的地址空間匹配32位或40位。千萬不要想當(dāng)然地認(rèn)為地址位寬越大越好因為命令字的位寬是固定的地址字段占多了留給其他字段的位置就少了。3.2 這些配置對應(yīng)到哪些端口配置完之后你會在IP例化界面看到一堆端口。很多人一看到S_AXIS_MM2S_CMD、M_AXIS_MM2S_STS、M_AXIS_MM2S、S_AXIS_S2MM_CMD、M_AXIS_S2MM_STS、S_AXIS_S2MM這些名字就開始頭大其實它們一一對應(yīng)著指定的功能。MM2S側(cè)的三個口分別是命令入口、數(shù)據(jù)出口或者叫狀態(tài)出口、以及數(shù)據(jù)出口。如果方向記混了后面寫RTL時大概率會接錯線。有一個端口需要特別注意主接口側(cè)的M_AXI_MM2S和M_AXI_S2MM。這兩個才是真正連接內(nèi)存映射世界的通道必須接到AXI互聯(lián)上最終指向HP端口或者ACP端口。如果這倆沒接對Datamover再強(qiáng)大也使不上勁。同時建議把復(fù)位和時鐘端口都檢查一遍。Datamover有獨立的aresetn它要求復(fù)位信號至少保持幾個時鐘周期的低電平才能安全重新開始。別把復(fù)位和系統(tǒng)復(fù)位直接群聊式地接在一起最好是復(fù)位釋放后至少等100個時鐘周期再下發(fā)命令讓IP內(nèi)部邏輯充分初始化。4. 搭一個最小讀寫鏈路從地址映射到控制狀態(tài)機(jī)4.1 地址映射和HP端口選型假設(shè)我們要在PL里例化一個Datamover把DDR里地址0x10000000開始的4KB數(shù)據(jù)讀出來經(jīng)過MM2S口發(fā)到一個FIFO中同時用S2MM口把另一個數(shù)據(jù)源寫入DDR另一段地址。第一步是搞清楚DDR的基地址。Zynq-7000系列DDR默認(rèn)被映射在0x00100000起始的地址空間具體看板卡的memory map一般在地址0x0010_0000到0x3FFFFFFF范圍內(nèi)MPSOC平臺又有不同映射。一定去查硬件手冊別拿網(wǎng)上的默認(rèn)值直接套。了解基地址后把DDR的起始位置1GB或2GB空間分配好PL側(cè)的M_AXI_MM2S/S2MM接到S_AXI_HP口然后做地址翻譯。地址翻譯這一步也很關(guān)鍵。Zynq里HP端口的地址空間和物理DDR地址是有映射關(guān)系的有時你從PL側(cè)發(fā)地址0x10000000對應(yīng)的DDR物理地址實際上是0x00100000 0x10000000。具體換算要看Vivado里的地址編輯器別在地址上栽跟頭。我的習(xí)慣做法是在Vivado Block Design里把Datamover的M_AXI接到一個AXI SmartConnect或者AXI Interconnect然后再接到S_AXI_HP0同時在Address Editor里給這條路徑分配地址范圍保證PS側(cè)和PL側(cè)看到的是同一個DDR物理區(qū)間。4.2 一次MM2S讀傳輸?shù)耐暾麜r序我把讀傳輸?shù)牧鞒滩鸪晌鍌€狀態(tài)做成一個極簡狀態(tài)機(jī)各位可以直接參考。第一步IDLE狀態(tài)下等待外部觸發(fā)。觸發(fā)信號可以是PS通過AXI GPIO拉高的也可以是PL內(nèi)部的事件計數(shù)器產(chǎn)生的。第二步SEND_CMD。在這個狀態(tài)下把要讀的起始地址、傳輸字節(jié)數(shù)拼成一個命令字放到S_AXIS_MM2S_CMD的TDATA上然后拉高TVALID。這時要等TREADY信號為高并且TVALID為高兩者同時滿足的那個時鐘上升沿命令才算發(fā)出。命令發(fā)完后TVALID拉低狀態(tài)機(jī)跳到WAIT_DATA。第三步WAIT_DATA。在這一步DataMover內(nèi)部會發(fā)起一系列AXI讀事務(wù)然后數(shù)據(jù)流從M_AXIS_MM2S送出來。你的接收邏輯只需要用valid/ready握手接收數(shù)據(jù)每收一個數(shù)據(jù)計數(shù)器加一直到收到TLAST拉高的那一拍說明這一次命令對應(yīng)的數(shù)據(jù)全部到期。第四步WAIT_STS。數(shù)據(jù)傳輸完后Datamover會從M_AXIS_MM2S_STS回狀態(tài)。要繼續(xù)讀取狀態(tài)通道握手方式跟命令通道一致直到狀態(tài)通道上出現(xiàn)valid。可以檢查狀態(tài)字里的錯誤位。第五步回到IDLE等待觸發(fā)下一次搬運(yùn)。整個過程中CPU完全不需要介入??雌饋砗芎唵螌Π傻珜嶋H操作里很多人會在WAIT_DATA和WAIT_STS之間漏掉一個關(guān)鍵點Datamover內(nèi)部是有命令FIFO的如果你連續(xù)下發(fā)多條命令不用等上一條完全結(jié)束就可以在數(shù)據(jù)通道還忙著的時候發(fā)下一條。這對提高吞吐量很關(guān)鍵但也要求你的狀態(tài)機(jī)不能死板地一讀一停否則吞吐量會被狀態(tài)機(jī)拖垮。4.3 AXI4-Stream握手與背壓怎么處理AXI4-Stream的握手是所有Stream邏輯的基礎(chǔ)規(guī)則其實就三條第一源端在數(shù)據(jù)穩(wěn)定后拉高TVALID第二目的端在可以接收時拉高TREADY第三只有在同一個時鐘上升沿TVALID和TREADY同時為高這一筆數(shù)據(jù)傳輸才完成。這里有個比較容易寫反的依賴關(guān)系TVALID不能等TREADY拉高后才拉高否則就形成了循環(huán)等待死鎖。正確寫法是只要源端有數(shù)據(jù)TVALID就可以拉高目的端如果沒有準(zhǔn)備好接收通過TREADY拉低來背壓也就是告訴源端先別發(fā)至少這一拍不發(fā)。源端收到TREADY為低時必須保持當(dāng)前數(shù)據(jù)和TVALID不變直到握手成功。當(dāng)我第一次把Datamover接進(jìn)一個實時數(shù)據(jù)采集系統(tǒng)時上游FIFO偶爾讀空下游FIFO偶爾寫滿如果背壓邏輯設(shè)計不當(dāng)整個數(shù)據(jù)鏈路就會卡死。所以我建議所有接收來自Datamover數(shù)據(jù)的FIFO都用First-Word-Fall-ThroughFWFT模式這樣tvalid可以提前有效更容易滿足Datamover數(shù)據(jù)通道的時序要求也不容易在握手初始化階段出現(xiàn)莫名的氣泡。S2MM方向同理當(dāng)你的數(shù)據(jù)源頻繁刷新時在Datamover接受數(shù)據(jù)之前數(shù)據(jù)源要一直保持?jǐn)?shù)據(jù)和TVALID不變。這時候如果上游是連續(xù)產(chǎn)生的有效信號你就要為它配一個足夠深的FIFO讓背壓能被緩沖掉不至于把前一級數(shù)據(jù)源直接逼停。5. 調(diào)試實錄最容易卡住的三類問題5.1 死鎖valid/ready的依賴關(guān)系寫反了我調(diào)試時踩過最經(jīng)典的坑就是命令通道的握手死鎖。最初寫控制狀態(tài)機(jī)時我很自然地寫成先判斷TREADY是否拉高如果拉高我再把TVALID拉高。表面上看邏輯沒有錯但實際上在仿真里這兩個信號互相等永遠(yuǎn)等不到握手完成那一刻。原因很簡單目的端的TREADY可能也在等你的TVALID。這個問題的根源是違反了AXI協(xié)議的基本法則valid不能依賴readyready可以等待valid。正確寫法是TVALID必須由數(shù)據(jù)是否準(zhǔn)備好決定而TREADY才是由我能否接收決定。你只需要保證自信地拉高TVALID然后等待兩者同時為高即可。如果你用的是Vivado的仿真環(huán)境建議直接在波形窗口里看s_axis_mm2s_cmd_tvalid和s_axis_mm2s_cmd_tready。如果看到tvalid和tready有一個一直為低另一個一直為高互相僵持那八九不離十就是依賴關(guān)系寫反了。再檢查一下狀態(tài)機(jī)的默認(rèn)分支確認(rèn)每個狀態(tài)在無效條件下都有明確的跳轉(zhuǎn)別因為某個信號缺省賦值為0導(dǎo)致狀態(tài)機(jī)徹底歇菜。5.2 未對齊地址導(dǎo)致的數(shù)據(jù)錯位另一個讓我折騰了整整一個晚上的問題是數(shù)據(jù)錯位。當(dāng)時系統(tǒng)是從DDR讀取數(shù)據(jù)結(jié)果收到的數(shù)據(jù)整體平移了幾個字節(jié)數(shù)據(jù)本身的數(shù)值都對但位置全亂了。排查到最后才發(fā)現(xiàn)數(shù)據(jù)位寬是64位時DDR地址必須是8字節(jié)對齊的。我的命令里給了一個末位不是0的地址比如0x10000004。Datamover實際AXI訪問時會把這個地址處理成8字節(jié)對齊的邊界然后把多讀出來的數(shù)據(jù)調(diào)整到正確的Stream輸出位置上。本身這個調(diào)整機(jī)制是IP內(nèi)部完成的理應(yīng)正確但問題出在我后續(xù)處理邏輯沒有把首地址未對齊導(dǎo)致的數(shù)據(jù)偏移考慮進(jìn)去。對策其實有兩種一種是在應(yīng)用層強(qiáng)制所有地址對齊到數(shù)據(jù)寬度對應(yīng)的字節(jié)數(shù)另一種是在配置Datamover時開啟未對齊支持并仔細(xì)閱讀它的輸出在未對齊情況下的行為說明。我個人建議除非你的數(shù)據(jù)流協(xié)議本質(zhì)上就必須支持任意偏移否則一律在軟件和邏輯層約定對齊訪問。不光是地址要對齊傳輸長度也盡量湊成突發(fā)長度的整數(shù)倍這樣能在調(diào)試階段省下大量時間。5.3 突發(fā)長度與BTT不一致還有一個很容易被忽略的問題命令字里的BTT值和實際配置的Max Burst Size之間如果不匹配會讓Datamover拆成多個突發(fā)去傳輸。本身這不會出錯但如果你的接收端邏輯里用了一個固定的突發(fā)計數(shù)認(rèn)為一條命令就是一次突發(fā)、一次突發(fā)就是固定的若干拍那麻煩就來了。比如你發(fā)了一條BTT1024字節(jié)的命令數(shù)據(jù)寬度64位總共需要128拍。如果Max Burst Size配置為16那么Datamover會拆成8次AXI突發(fā)每次突發(fā)16拍。在M_AXIS_MM2S側(cè)這些突發(fā)會以TLAST作為每次突發(fā)結(jié)束的標(biāo)記。那么你接收數(shù)據(jù)時就必須以TLAST為邊界而不是以固定的拍數(shù)來計數(shù)。更隱蔽的是如果你的上游數(shù)據(jù)源和Datamover的S2MM數(shù)據(jù)處理不好TLAST那么在最后一個突發(fā)結(jié)束后可能會少一個TLAST最終導(dǎo)致寫入DDR的長度比預(yù)期少一筆數(shù)據(jù)。S2MM方向有一個內(nèi)部字節(jié)計數(shù)邏輯它要求收到的數(shù)據(jù)字節(jié)數(shù)必須和BTT完全一致。數(shù)據(jù)多了、少了或者TLAST時機(jī)不對都會在狀態(tài)通道里上報錯誤。調(diào)試這類問題時我強(qiáng)烈建議在ILA里同時抓三個信號命令通道、數(shù)據(jù)通道、以及狀態(tài)通道。這樣你才能把我發(fā)了多少命令、我給了多少數(shù)據(jù)、Datamover最終認(rèn)為完成了多少三者對上。凡是看到數(shù)據(jù)計數(shù)和狀態(tài)字完成的字節(jié)計數(shù)對不上優(yōu)先懷疑TLAST的生成邏輯。Datamover這個IP說到底就是一個把AXI內(nèi)存映射事務(wù)翻譯成AXI4-Stream數(shù)據(jù)流的專職搬運(yùn)工。它不負(fù)責(zé)產(chǎn)生數(shù)據(jù)也不負(fù)責(zé)理解數(shù)據(jù)只負(fù)責(zé)準(zhǔn)確無誤地把數(shù)據(jù)從一個世界搬到另一個世界。你要做的就是學(xué)會用命令字驅(qū)動它、用握手協(xié)議伺候它、用狀態(tài)通道監(jiān)控它。把這幾個點想透了PL讀寫DDR這件事基本就打開了思路剩下的就是按照你的實際數(shù)據(jù)流需求把它接到對應(yīng)的邏輯上去。