1、引言
我們在FPGA上進行數據處理或者信號處理時,通常會遇到從片外存儲器(DDR)讀取數據至片內,或者將片內的結果直接暫存至片外(DDR)。其中以Xilinx家的DMA控制器(英文全稱:AXI Direct Memory Access)的讀取功能(Read Channel)為例,能夠通過AXI總線讀取某個地址區間的數據,同時再將這些數據轉換以數據流的形式傳輸至處理單元。典型的AXI Direct Memory Access(IP核)配置界面如下圖所示。
從圖中可以看出,普通模式的DMA具備以下特性:
①AXI Memory Map總線地址位寬可選32bit或64bit;
②Buffer Length最大位寬為26,對應的單次傳輸大小最大為64MByte;
③同時支持讀通道和寫通道,並且每個通道的AXI Memory Map數據位寬可配置,Stream數據流接口的位寬可配置,用於AXIMemory Map的突出傳輸長度可配置,同樣也允許未對齊數據的傳輸;
那么根據上述的總結可以看出,普通模式的DMA對於解決簡單的數據傳輸,完全能夠應付。但是當面向大規模數據(單次傳輸>64MByte),或需要操作的地址不連續時,普通模式的DMA不能夠滿足要求,即便能夠將大規模數據分割為多個<64MByte的片段,但是這個過程需要額外的處理器(例如ARM/NIOS/MicroBlaze/RISC-V)進行查詢監測,或者啟用中斷函數,這樣額外的消耗了處理器性能(我們期望的是一次配置,永久使用)。對於這樣的問題,通常啟用該IP核的高級功能——Scatter Gather (SG) Engine模式,雖然單個傳輸片段依然有64MByte的限制(取決於Buffer Length Width),但是我們可以把一片>64MByte的數據划分為多個<64MByte的區域來解決。並且在此基礎上,我們需要傳輸的數據有多個地址不連續的片段,同樣也能夠完美解決。其中開啟Scatter Gather Engine后,IP核的接口如下圖所示。與普通模式的DMA相比,多了一根M_AXI_SG總線(這個總線稍后我會單獨介紹)。
(那么可能有的讀者開始杠了:對於這種特殊的DMA,我完全可以用Vivado HLS自己寫一個特殊的數據傳輸過程,沒必要用SG DMA。我一開始也是這么想的,功能上倒沒什么問題,但是基於筆者菜雞的VivadoHLS功底,發現傳輸效率實在是太差了,尤其是地址切換的時候,需要花費大量的時鍾周期用於發起AXI傳輸請求,這樣導致我后面的數據處理單元性能受限於數據傳輸的性能。再后來我發現官方對於這類需求已經在給定的DMA IP核中做好了,數據傳輸效率極高,尤其是單次傳輸多個數據,中間的那點延遲幾乎可以忽略不計,真香~)
但是,官方的軟件IP核功能是如此的牛逼,但是給定的軟件代碼配置過程卻又像是一坨Shit一樣,我看了下官方提供裸機(Baremetal)下的SG DMA驅動例程,洋洋灑灑好幾千行代碼,愣是把我給看懵了。沒辦法,為了帶寬不拖數據處理單元的后腿,硬着頭皮看一下官方給的Product Guide(就在IP核配置界面的左上角Documentation地方)。不得不說,官方給的數據手冊還是挺詳細的,對於IP核怎么使用,以及需要注意的事項,寫的清清楚楚。雖然是英文的,但是鑒於筆者還是有過好幾篇SCI論文寫作的經歷,對於看這類文檔還是沒有什么問題,基本上都能看得懂。
因此,為了能夠方便的使用SG DMA,本文從SG DMA的運行行為分析入手,介紹SG DMA的驅動軟件操作流程,並在此基礎上對其性能進行優化。本文的主要內容如下:
①根據Product Guide介紹SG DMA的運行行為及其注意事項;
②根據運行行為編寫裸機(Baremetal)環境下應用程序;
③根據初步實驗結果分析多個DMA傳輸間隔周期較長的原因,提出改進策略並對比結果。
由於筆者能力有限,介紹過程中不可避免存在一部分設計不足的問題。本文主要是為SG DMA的使用方法提供一種設計思路,希望能夠以本人的一得之見,起到拋磚引玉的作用。
2、SG DMA運行過程分析
在這里先介紹一下SG DMA的一個大概的工作過程。上一節的介紹中提到SG DMA與普通DMA相比多了一根M_AXI_SG總線,這根總線其實是SG DMA的關鍵點。運行過程如下:
①DMA控制器通過M_AXI_SG總線讀取Scatter Gather Descriptor(以下簡稱為描述符),描述符沒有集成在IP核中,需要在內存中進行定義;
②接着對該Descriptor(描述符)進行解析,根據解析到的內容開啟單次傳輸;
③讀取的描述符中包含了下一個描述符的地址,在當前傳輸完成后讀取下一個描述符;
④往復循環上述過程,直到讀取到最后一個描述符,停止數據傳輸。
其中描述符內容的定義如下(pg021_axi_dma v7.1, page38):
其中我們比較關心的地方有:00h和04h為下一個描述符在內存中的地址;08h和0Ch為待傳輸數據在內存中的地址;18h為控制寄存器,其中Bit0~Bit25為傳輸長度,Bit26表示當前描述符是否為最后一幀(高有效),Bit27表示當前描述福是否為第一幀(高有效)。需要特別注意的是第一條注意事項:描述符必須為16個字,即64字節對齊。
介紹完描述符以后,接下來介紹SG DMA內部自帶的寄存器,其中SG DMA內部寄存器如下圖所示(pg021_axi_dma v7.1, page12):
以Read Channel(即MM2S)為例,我們需要關注的寄存器包括:00h控制寄存器,用於控制SG DMA的啟動、停止、復位、循環模式、中斷開啟等;04h狀態寄存器,查詢停止、空閑、中斷錯誤類型等;08h和0Ch當前描述符的地址;10h和14h尾描述符的地址,也就是結束描述符的地址。同理,Write Channel(S2MM)寄存器與之相同,剩下的其他寄存器暫時用不到。
因此,根據上述寄存器的介紹,依據描述的定義在內存中完成描述符編寫,然后再將描述符存儲的地址寫入至SG DMA(IP核)的寄存器中,最后開啟數據傳輸即可。中間過程支持對SG DMA運行狀態的查看。
3、SG DMA軟件操作流程
依據Product Guide的描述,完成軟件操作流程。該步驟可以直接使用官方給的驅動程序示例,但是筆者覺得太繁瑣了,而且代碼量實在是太大了,於是乎筆者決定自己寫一個軟件驅動。
在寫軟件驅動之前,強烈建議查閱下官方給出的操作流程,並嚴格按照流程的規范來,要不然程序運行步起來。(一開始筆者根據自己的經驗寫了一版驅動,但是發現完全運行不起來,后面發現手冊給出了操作流程說明,更改后才能運行)其中操作流程如下所示(pg021_axi_dma v7.1, page73):
假如我們已經在內存中完成了描述符的定義,根據上述流程,翻譯成中文就是下面的步驟:
①將起始描述符的地址寫入當前描述符寄存器(08h和0Ch);
②通過控制寄存器(00h)啟動DMA傳輸,必要的時候可以檢查下狀態寄存器(04h)的Halted標志位,用來查看DMA是否開始運行;(如果需要運行在Keyhole或Cyclic模式,需要在②之前進行設置)
③如果需要的話,通過控制寄存器(00h)對中斷進行設置;
④寫入一個有效的地址到尾描述符(10h和14h)寄存器,一般是將尾描述符的地址寫到這個里面;
⑤寫入尾描述寄存器將會觸發DMA從內存中讀取第一個描述;對於多通道模式下,多個描述符的讀取將會在S2MM通道接收到數據包以后開始;
⑥讀取到的描述符開始被解析,要傳輸的數據從內存中讀取,然后輸出到MM2S的數據流通道上。
看了上述過程,是不是覺得很奇葩,先寫起始描述符寄存器,然后要先開啟DMA傳輸,最后由寫入尾描述符寄存器觸發傳輸。這和我們傳統的寫完所有的描述符地址,然后通過控制寄存器觸發DMA傳輸完全不一樣,一開始筆者就是因為這種固定思維,導致遲遲無法啟動DMA傳輸。對於官方給出的這種操作流程,真的是讓筆者大開眼界。
那么基於上述操作,我們可以編寫出如下的代碼:
1 // @Time : 2021.07.22 2 // @Author : wuruidong 3 // @Email : wuruidong@hotmail.com 4 // @FileName: sdk_main.c 5 // @Software: Xilinx SDK 2018.3 6 // @Cnblogs : https://www.cnblogs.com/ruidongwu 7 #include "xaxidma.h" 8 #include "xaxidma_hw.h" 9 10 #define AXI_DMA_ADDR XPAR_AXI_DMA_0_BASEADDR 11 12 typedef struct 13 { 14 u32 next_desc; //00h 15 u32 next_desc_msb; //04h 16 u32 buffer_addr; //08h 17 u32 buffer_addr_msb; //0ch 18 u32 reserved[2]; //10h & 14h 19 u32 ctrl; //18h 20 u32 status; //1ch 21 u32 app[5]; //20h 24h 28h 2ch 30h 22 u32 aligned[3]; 23 }SG_Desc; //aligned to 64 24 25 SG_Desc DMA_Desc[3] __attribute__ ((aligned (64))); 26 27 void DMA_Desc_Init(void) 28 { 29 memset(DMA_Desc, 0, sizeof(DMA_Desc)); 30 31 //start 32 DMA_Desc[0].next_desc = (INTPTR)(&(DMA_Desc[1])); 33 DMA_Desc[0].buffer_addr = (INTPTR)(data0); 34 DMA_Desc[0].ctrl = sizeof(data0)|XAXIDMA_BD_CTRL_TXSOF_MASK; 35 36 DMA_Desc[1].next_desc = (INTPTR)(&(DMA_Desc[2])); 37 DMA_Desc[1].buffer_addr = (INTPTR)(data1); 38 DMA_Desc[1].ctrl = sizeof(data1); 39 40 //end 41 DMA_Desc[2].next_desc = (INTPTR)(&(DMA_Desc[0]));//Any address 42 DMA_Desc[2].buffer_addr = (INTPTR)(data2); 43 DMA_Desc[2].ctrl = sizeof(data2)|XAXIDMA_BD_CTRL_TXEOF_MASK; 44 45 Xil_DCacheFlushRange((INTPTR)DMA_Desc, sizeof(DMA_Desc)); 46 } 47 48 void DMA_Init(void) 49 { 50 // reset 51 Xil_Out32(AXI_DMA_ADDR+XAXIDMA_CR_OFFSET, XAXIDMA_CR_RESET_MASK); 52 usleep(1000); 53 54 //disable all interrupt 55 Xil_Out32(AXI_DMA_ADDR+XAXIDMA_CR_OFFSET, Xil_In32(AXI_DMA_ADDR+XAXIDMA_CR_OFFSET)&(~XAXIDMA_IRQ_ALL_MASK)); 56 //It can be ignored due to system reset; 57 58 //set DMA_Desc address 59 Xil_Out32(AXI_DMA_ADDR+XAXIDMA_CDESC_OFFSET, (UINTPTR)(&(DMA_Desc[0]))); 60 61 //start 62 Xil_Out32(AXI_DMA_ADDR+XAXIDMA_CR_OFFSET, Xil_In32(AXI_DMA_ADDR+XAXIDMA_CR_OFFSET)|XAXIDMA_CR_RUNSTOP_MASK|XAXIDMA_CR_CYCLIC_MASK); 63 64 //It can be ignore. 65 while(1) 66 { 67 if((Xil_In32(AXI_DMA_ADDR+XAXIDMA_SR_OFFSET)&XAXIDMA_HALTED_MASK)==0) 68 break; 69 } 70 71 //trigger 72 Xil_Out32(AXI_DMA_ADDR+XAXIDMA_TDESC_OFFSET, (UINTPTR)(&(DMA_Desc[2]))); 73 } 74 75 u32 DMA_Status(void) 76 { 77 return Xil_In32(AXI_DMA_ADDR+XAXIDMA_SR_OFFSET)&XAXIDMA_HALTED_MASK; 78 }
上述代碼中包含了描述符的定義、描述符的初始化、DMA的啟動過程以及查詢DMA的運行狀態,其中完整的代碼下載鏈接在文章末尾。
4、工程搭建與初步測試結果
根據上述步驟我們在Vivado中構建Block Design,然后在Diagram中按照下圖的方式進行連接,其中本文把SG DMA設置為讀取通道(即MM2S)。DMA輸出接口的M_AXIS_MM2S的m_axis_mm2s_tready強制設置為1(表明輸出一直有效),然后再添加內嵌邏輯分析儀System ILA抓取輸出信號,查看傳輸過程中的數據是否正確及傳輸狀態變化。
在這里聲明筆者開發過程中遇到的一個坑,AXI Direct Memory Access在配置的過程中,千萬不要勾選使用Micro DMA模式 ,除非你傳輸的數據量特別特別小,主要原因見下圖(pg021_axi_dma v7.1, page41)。
雖然我們可以在配置界面上看到勾選了Micro DMA並不會對接口有任何的影響,但是能夠支持的最大傳輸長度有了限制。筆者猜測Micro DMA模式的意義是:傳輸的長度剛好滿足MM2S接口的單次突發傳輸。所以有了上述限制。
經過上述的一系列操作以后,我們其實可以生成比特流文件,然后導出硬件和比特流到SDK中,編寫裸機下的驅動程序。實驗測試過程,本文分別定義了Data0、Data1和Data2三組數據,數據類型為32位有符號整形,數據長度分別為16、32和64。另外為了方便測試,本文對SG DMA采用Cyclic循環模式(如何啟用Cyclic模式將會在文末的附錄中描述)。通過查看傳輸過程中單次傳輸是否存在不連續的情況,同時分別查看兩組數據之間的空閑周期,即為切換傳輸所需要的周期。如果切換的周期越短,那么傳輸的效率越高效,反之效率越差。
邏輯分析儀抓取結果中,由於數據傳輸比較短,單組數據傳輸過程中沒有發生不連續的情況,而多組數據間所消耗的額外時鍾周期如下表所示:
- | Data2~Data0 | Data0~Data1 | Data1~Data2 |
Round 1 | - | 36 | 22 |
Round 2 | 0 | 24 | 20 |
Round 3 | 0 | 24 | 20 |
Round 4 | 0 | 24 | 20 |
Round N | … | … | … |
通過對表格的結果可以看出,開始階段中描述符所對應的數據段切換所消耗的周期比較多,隨着數據段切換次數的增加,中間所消耗的周期降低並保持穩定。其中切換周期為0的原因可能是Data2與Data0在內存上地址處於連續狀態,可以不用考慮。則數據段的切換所消耗的最小周期為20個時鍾周期。
我們可以發現SD DMA的工作流程先通過M_AXI_SG總線從指定地址中讀取描述符,讀取后對描述符進行解析,然后再啟動數據讀取。那么在切換的周期中,所消耗的是否包括:M_AXI_SG讀取時間、解析時間、M_AXI_MM2S讀取時間。筆者分析后發現,M_AXI_SG是從片外的DDR中進行描述符的讀取,那么該過程可能需要消耗大量的時鍾周期,因此本文接下來針對M_AXI_SG做專門的優化設計。
5、M_AXI_SG優化與實驗結果
根據對AXI Memory Map總線協議的分析,發起單次數據傳輸需要先發送地址信息和突發傳輸長度,然后等待總線響應,握手成功后返回讀取到的數據。正常運行環境下,存在有多個主設備請求訪問DDR,通過總線仲裁后再進行數據傳輸,上述實驗還是只有一個主機工作條件下的延遲,實際應用中如果存在有其他設備,數據段切換所引起的額外時鍾周期消耗還會增加。因此,如果將M_AXI_SG獨立分配總線,甚至可以將描述符直接存儲在片上的BRAM中,那么將會極大的減少M_AXI_SG總線所引起的時鍾周期延遲。因此本文針對上述系統進行優化,優化后的Diagram如下圖所示。
其中對應的地址分配如下圖所示,我們給BRAM分配的地址空間大小為4K,起始地址為0x8000_0000,需要注意的是在Data_SG分配的地址區間內,只保留BRAM對應的S_AXI mem0一個地址區域,去除其他區域,否則在Validate Design中將會報錯。
經過上述改進后,重新生成比特流,並通過邏輯分析儀抓取M_AXIS_MM2S總線數據(SDK程序源碼在文末給出)。其中數據段切換之間的周期數如下表所示:
- | Data2~Data0 | Data0~Data1 | Data1~Data2 |
Round 1 | - | 18 | 7 |
Round 2 | 0 | 0 | 5 |
Round 3 | 0 | 0 | 5 |
Round 4 | 0 | 0 | 5 |
Round N | … | … | … |
通過對比可以看出,在數據傳輸穩定后,數據段切換的時鍾周期間隔穩定在5個時鍾周期,與SG DMA默認的從片外DDR中讀取描述符相比,周期間隔出現大大的減少,即傳輸的效率得到了提升,這也對后續的數據處理單元的高效性進行了保障,使得數據處理單元的性能不再受帶寬傳輸效率的影響,尤其是在需要多個數據段之間多次切換的場景,保證了傳輸的高效性。
6、總結
本文針對於AXI Direct Memory Access的Scatter Gather模式中的讀取通道進行了行為分析,並完成了邏輯狀態下的操作流程的軟件代碼編寫。同時本文采用了基於片內BRAM存儲的獨立M_AXI_SG傳輸總線方法,解決了多個數據段之間切換存在的時鍾周期較長的問題,從而提升了數據傳輸效率。本文所采用的軟件流程同樣適用於其他具備Scatter Gather DMA傳輸需求的IP核軟件編寫,所提優化方法同樣可適配至其他需要提升總線傳輸效率的場合。
附錄(Scatter Gather DMA的Cyclic模式):
Cyclic模式主要是用在數據處理模塊對數據有循環需求的場合。例如在卷積神經網絡加速應用中,當網絡對多張連續的輸入圖像進行卷積,需要循環從片外DDR中加載權重數據數據至片內,然后與輸入圖像或特征圖進行卷積操作。根據Xilinx官方提供的手冊可以看出Cyclic模式的介紹如下(pg021_axi_dma v7.1, page74):
翻譯成中文,進入Cyclic模式的操作步驟如下(假如已經在內存中完成對描述符的賦值操作):
①使能MM2S_DMACR控制寄存器的Bit4(Cyclic BD Enable);
②將起始描述符的地址寫入當前描述符寄存器;(該步驟與①的順序可以調換)
③通過控制寄存器(00h)啟動DMA傳輸;
④隨便寫一個地址到尾描述符寄存器,只要不與上面的描述符地址重合就行;(對,你沒看錯,隨便寫啥都可以)
⑤DMA開始運行和傳輸數據,在這個過程除非主動通過控制寄存器停止DMA或者復位,其他條件都不會讓DMA停下來。
最后是代碼的下載鏈接,點擊我下載。