基於Xilinx FPGA的AXI Direct Memory Access (Scatter Gather Engine模式) 行為分析及軟件操作流程


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停下來。

最后是代碼的下載鏈接,點擊我下載

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM