1.MDA簡介
DMA全稱Direct Memory Access,直接存儲區訪問。
DMA傳輸將數據從一個地址空間復制到另一個地址空間。當CPU初始化這個傳輸動作,傳輸動作本身是由DMA控制器來實現和完成的。DMA傳輸方式無需CPU直接控制傳輸,也沒有中斷處理方式那樣保留現場和恢復現場的過程,通過硬件為RAM和IO設備開辟一個直接傳輸數據的通道,使得CPU的效率大大提高。
STM32F4xx系列的DMA支持外設到存儲器傳輸、存儲器到外設傳輸和存儲器到存儲器傳輸三種傳輸模式。 這里的外設一般指外設的數據寄存器,比如ADC、SPI、I2C、DCMI等等外設的數據寄存器,存儲器一般是指片內SRAM、外部存儲器、片內Flash等等。
外設到存儲器傳輸就是把外設數據寄存器內容轉移到指定的內存空間。比如進行ADC采集時我們可以利用DMA傳輸把AD轉換數據轉移到我們定義的存儲區中, 這樣對于多通道采集、采樣頻率高、連續輸出數據的AD采集是非常高效的處理方法。
存儲區到外設傳輸就是把特定存儲區內容轉移至外設的數據寄存器中,這種多用于外設的發送通信。
存儲器到存儲器傳輸就是把一個指定的存儲區內容拷貝到另一個存儲區空間。功能類似于C語言內存拷貝函數memcpy, 利用DMA傳輸可以達到更高的傳輸效率,特別是DMA傳輸是不占用CPU的,可以節省很多CPU資源。
1.1外設通道選擇
STM32F4xx系列資源豐富,具有兩個DMA控制器,同時外設繁多,為實現正常傳輸,DMA需要通道選擇控制。每個DMA控制器具有8個數據流, 每個數據流對應8個外設請求。在實現DMA傳輸之前,DMA控制器會通過DMA數據流x配置寄存器DMA_SxCR(x為0~7,對應8個DMA數據流)的CHSEL[2:0]位選擇對應的通道作為該數據流的目標外設。
外設通道選擇要解決的主要問題是決定哪一個外設作為該數據流的源地址或者目標地址。
DMA1各個通道的請求映像
DMA2各個通道的請求映像每個外設請求都占用一個數據流通道,相同外設請求可以占用不同數據流通道。
1.2仲裁器
一個DMA控制器對應8個數據流,數據流包含要傳輸數據的源地址、目標地址、數據等等信息。如果我們需要同時使用同一個DMA控制器(DMA1或DMA2)多個外設請求時, 那必然需要同時使用多個數據流,那究竟哪一個數據流具有優先傳輸的權利呢?這就需要仲裁器來管理判斷了。
仲裁器管理數據流方法分為兩個階段。第一階段屬于軟件階段,我們在配置數據流時可以通過寄存器設定它的優先級別, 具體配置DMA_SxCR寄存器PL[1:0]位,可以設置為非常高、高、中和低四個級別。第二階段屬于硬件階段,如果兩個或以上數據流軟件設置優先級一樣, 則他們優先級取決于數據流編號,編號越低越具有優先權,比如數據流2優先級高于數據流3。
1.3FIFO
每個數據流都獨立擁有四級32位FIFO(先進先出存儲器緩沖區)。DMA傳輸具有FIFO模式和直接模式。
直接模式在每個外設請求都立即啟動對存儲器傳輸。在直接模式下,如果DMA配置為存儲器到外設傳輸那DMA會見一個數據存放在FIFO內, 如果外設啟動DMA傳輸請求就可以馬上將數據傳輸過去。
FIFO用于在源數據傳輸到目標地址之前臨時存放這些數據。可以通過DMA數據流xFIFO控制寄存器DMA_SxFCR的FTH[1:0]位來控制FIFO的閾值, 分別為1/4、1/2、3/4和滿。如果數據存儲量達到閾值級別時,FIFO內容將傳輸到目標中。
FIFO對于要求源地址和目標地址數據寬度不同時非常有用,比如源數據是源源不斷的字節數據,而目標地址要求輸出字寬度的數據, 即在實現數據傳輸時同時把原來4個8位字節的數據拼湊成一個32位字數據。此時使用FIFO功能先把數據緩存起來,分別根據需要輸出數據。
FIFO另外一個作用使用于突發(burst)傳輸。
2.DMA數據配置
2.1DMA傳輸模式
DMA2支持全部三種傳輸模式,而DMA1只有外設到存儲器和存儲器到外設兩種模式。模式選擇可以通過DMA_SxCR寄存器的DIR[1:0]位控制, 進而將DMA_SxCR寄存器的EN位置1就可以使能DMA傳輸。
在DMA_SxCR寄存器的PSIZE[1:0]和MSIZE[1:0]位分別指定外設和存儲器數據寬度大小,可以指定為字節(8位)、半字(16位)和字(32位), 我們可以根據實際情況設置。直接模式要求外設和存儲器數據寬度大小一樣,實際上在這種模式下DMA數據流直接使用PSIZE,MSIZE不被使用。
2.2源地址和目標地址
DMA數據流x外設地址DMA_SxPAR(x為0~7)寄存器用來指定外設地址,它是一個32位數據有效寄存器。 DMA數據流x存儲器0地址DMA_SxM0AR(x為0~7) 寄存器和DMA數據流x存儲器1地址DMA_SxM1AR(x為0~7)寄存器用來存放存儲器地址, 其中DMA_SxM1AR只用于雙緩沖模式,DMA_SxM0AR和DMA_SxM1AR都是32位數據有效的。
當選擇外設到存儲器模式時,即設置DMA_SxCR寄存器的DIR[1:0] 位為“00”,DMA_SxPAR寄存器為外設地址,也是傳輸的源地址, DMA_SxM0AR寄存器為存儲器地址,也是傳輸的目標地址。對于存儲器到存儲器傳輸模式,即設置DIR[1:0]位為“10”時, 采用與外設到存儲器模式相同配置。而對于存儲器到外設,即設置DIR[1:0]位為“01”時,DMA_SxM0AR寄存器作為為源地址,DMA_SxPAR寄存器作為目標地址。
2.3流控制器
流控制器主要涉及到一個控制DMA傳輸停止問題。DMA傳輸在DMA_SxCR寄存器的EN位被置1后就進入準備傳輸狀態,如果有外設請求DMA傳輸就可以進行數據傳輸。 很多情況下,我們明確知道傳輸數據的數目,比如要傳1000個或者2000個數據,這樣我們就可以在傳輸之前設置DMA_SxNDTR寄存器為要傳輸數目值, DMA控制器在傳輸完這么多數目數據后就可以控制DMA停止傳輸。
DMA數據流x數據項數DMA_SxNDTR(x為0~7)寄存器用來記錄當前仍需要傳輸數目,它是一個16位數據有效寄存器,即最大值為65535, 這個值在程序設計是非常有用也是需要注意的地方。我們在編程時一般都會明確指定一個傳輸數量,在完成一次數目傳輸后DMA_SxNDTR計數值就會自減,當達到零時就說 明傳輸完成。
如果某些情況下在傳輸之前我們無法確定數據的數目,那DMA就無法自動控制傳輸停止了,此時需要外設通過硬件通信向DMA控制器發送停止傳輸信號。 這里有一個大前提就是外設必須是可以發出這個停止傳輸信號,只有SDIO才有這個功能,其他外設不具備此功能。
2.4循環模式
循環模式相對應于一次模式。一次模式就是傳輸一次就停止傳輸,下一次傳輸需要手動控制,而循環模式在傳輸一次后會自動按照相同配置重新傳輸,周而復始直至被控制停止或傳輸發生錯誤。
通過DMA_SxCR寄存器的CIRC位可以使能循環模式。
2.5傳輸類型
DMA傳輸類型有單次(Single)傳輸和突發(Burst)傳輸。突發傳輸就是用非常短時間結合非常高數據信號率傳輸數據,相對正常傳輸速度, 突發傳輸就是在傳輸階段把速度瞬間提高,實現高速傳輸,在數據傳輸完成后恢復正常速度,有點類似達到數據塊“秒傳”效果。為達到這個效果突發傳輸過程要占用AHB總線, 保證要求每個數據項在傳輸過程不被分割,這樣一次性把數據全部傳輸完才釋放AHB總線;而單次傳輸時必須通過AHB的總線仲裁多次控制才傳輸完成。
2.6直接模式
默認情況下,DMA工作在直接模式,不使能FIFO閾值級別。
直接模式在每個外設請求都立即啟動對存儲器傳輸的單次傳輸。直接模式要求源地址和目標地址的數據寬度必須一致, 所以只有PSIZE控制,而MSIZE值被忽略。突發傳輸是基于FIFO的所以直接模式不被支持。另外直接模式不能用于存儲器到存儲器傳輸。
在直接模式下,如果DMA配置為存儲器到外設傳輸那DMA會見一個數據存放在FIFO內,如果外設啟動DMA傳輸請求就可以馬上將數據傳輸過去。
2.7雙緩沖模式
設置DMA_SxCR寄存器的DBM位為1可啟動雙緩沖傳輸模式,并自動激活循環模式。雙緩沖不應用與存儲器到存儲器的傳輸。雙緩沖模式下, 兩個存儲器地址指針都有效,即DMA_SxM1AR寄存器將被激活使用。開始傳輸使用DMA_SxM0AR寄存器的地址指針所對應的存儲區, 當這個存儲區數據傳輸完DMA控制器會自動切換至DMA_SxM1AR寄存器的地址指針所對應的另一塊存儲區, 如果這一塊也傳輸完成就再切換至DMA_SxM0AR寄存器的地址指針所對應的存儲區,這樣循環調用。
當其中一個存儲區傳輸完成時都會把傳輸完成中斷標志TCIF位置1,如果我們使能了DMA_SxCR寄存器的傳輸完成中斷,則可以產生中斷信號, 這個對我們編程非常有用。另外一個非常有用的信息是DMA_SxCR寄存器的CT位,當DMA控制器是在訪問使用DMA_SxM0AR時CT=0, 此時CPU不能訪問DMA_SxM0AR,但可以向DMA_SxM1AR填充或者讀取數據;當DMA控制器是在訪問使用DMA_SxM1AR時CT=1,此時CPU不能訪問DMA_SxM1AR, 但可以向DMA_SxM0AR填充或者讀取數據。另外在未使能DMA數據流傳輸時,可以直接寫CT位,改變開始傳輸的目標存儲區。
雙緩沖模式應用在需要解碼程序的地方是非常有效的。比如MP3格式音頻解碼播放,MP3是被壓縮的文件格式, 我們需要特定的解碼庫程序來解碼文件才能得到可以播放的PCM信號,解碼需要一定的實際,按照常規方法是讀取一段原始數據到緩沖區, 然后對緩沖區內容進行解碼,解碼后才輸出到音頻播放電路,這種流程對CPU運算速度要求高,很容易出現播放不流暢現象。 如果我們使用DMA雙緩沖模式傳輸數據就可以非常好的解決這個問題,達到解碼和輸出音頻數據到音頻電路同步進行的效果。
2.8DMA中斷
每個DMA數據流可以在發送以下事件時產生中斷:
-
達到半傳輸:DMA數據傳輸達到一半時HTIF標志位被置1, 如果使能HTIE中斷控制位將產生達到半傳輸中斷;
-
傳輸完成:DMA數據傳輸完成時TCIF標志位被置1, 如果使能TCIE中斷控制位將產生傳輸完成中斷;
-
傳輸錯誤:DMA訪問總線發生錯誤或者在雙緩沖模式下試圖訪問“受限”存儲器地址寄存器時TEIF標志位被置1, 如果使能TEIE中斷控制位將產生傳輸錯誤中斷;
-
FIFO錯誤:發生FIFO下溢或者上溢時FEIF標志位被置1, 如果使能FEIE中斷控制位將產生FIFO錯誤中斷;
-
直接模式錯誤:在外設到存儲器的直接模式下,因為存儲器總線沒得到授權,使得先前數據沒有完成被傳輸到存儲器空間上, 此時DMEIF標志位被置1,如果使能DMEIE中斷控制位將產生直接模式錯誤中斷。
3.DMA_InitTypeDef初始化結構體
typedef struct {uint32_t DMA_Channel; //通道選擇uint32_t DMA_PeripheralBaseAddr; //外設地址uint32_t DMA_Memory0BaseAddr; //存儲器0地址uint32_t DMA_DIR; //傳輸方向uint32_t DMA_BufferSize; //數據數目uint32_t DMA_PeripheralInc; //外設遞增uint32_t DMA_MemoryInc; //存儲器遞增uint32_t DMA_PeripheralDataSize; //外設數據寬度uint32_t DMA_MemoryDataSize; //存儲器數據寬度uint32_t DMA_Mode; //模式選擇uint32_t DMA_Priority; //優先級uint32_t DMA_FIFOMode; //FIFO模式uint32_t DMA_FIFOThreshold; //FIFO閾值uint32_t DMA_MemoryBurst; //存儲器突發傳輸uint32_t DMA_PeripheralBurst; //外設突發傳輸
} DMA_InitTypeDef;
-
DMA_Channel:DMA請求通道選擇,可選通道0至通道7,每個外設對應固定的通道, 具體設置值需要查表 DMA1各個通道的請求映像 和表 DMA2各個通道的請求映像 ; 它設定DMA_SxCR寄存器的CHSEL[2:0]位的值。例如,我們使用模擬數字轉換器ADC3規則采集4個輸入通道的電壓數據,查表 DMA2各個通道的請求映像 可知使用通道2。
-
DMA_PeripheralBaseAddr:外設地址,設定DMA_SxPAR寄存器的值;一般設置為外設的數據寄存器地址, 如果是存儲器到存儲器模式則設置為其中一個存儲區地址。ADC3的數據寄存器ADC_DR地址為((uint32_t)ADC3+0x4C)。
-
DMA_Memory0BaseAddr:存儲器0地址,設定DMA_SxM0AR寄存器值;一般設置為我們自定義存儲區的首地址。 我們程序先自定義一個16位無符號整形數組ADC_ConvertedValue[4]用來存放每個通道的ADC值, 所以把數組首地址(直接使用數組名即可)賦值給DMA_Memory0BaseAddr。
-
DMA_DIR:傳輸方向選擇,可選外設到存儲器、存儲器到外設以及存儲器到存儲器。 它設定DMA_SxCR寄存器的DIR[1:0]位的值。ADC采集顯然使用外設到存儲器模式。
-
DMA_BufferSize:設定待傳輸數據數目,初始化設定DMA_SxNDTR寄存器的值。 這里ADC是采集4個通道數據,所以待傳輸數目也就是4。
-
DMA_PeripheralInc:如果配置為DMA_PeripheralInc_Enable,使能外設地址自動遞增功能,它設定DMA_SxCR寄存器的PINC位的值; 一般外設都是只有一個數據寄存器,所以一般不會使能該位。ADC3的數據寄存器地址是固定并且只有一個所以不使能外設地址遞增。
-
DMA_MemoryInc:如果配置為DMA_MemoryInc_Enable,使能存儲器地址自動遞增功能,它設定DMA_SxCR寄存器的MINC位的值; 我們自定義的存儲區一般都是存放多個數據的,所以使能存儲器地址自動遞增功能。我們之前已經定義了一個包含4個元素的數字用來存放數據, 使能存儲區地址遞增功能,自動把每個通道數據存放到對應數組元素內。
-
DMA_PeripheralDataSize:外設數據寬度,可選字節(8位)、半字(16位)和字(32位),它設定DMA_SxCR寄存器的PSIZE[1:0]位的值。 ADC數據寄存器只有低16位數據有效,使用半字數據寬度。
-
DMA_MemoryDataSize:存儲器數據寬度,可選字節(8位)、半字(16位)和字(32位),它設定DMA_SxCR寄存器的MSIZE[1:0]位的值。 保存ADC轉換數據也要使用半字數據寬度,這跟我們定義的數組是相對應的。
-
DMA_Mode:DMA傳輸模式選擇,可選一次傳輸或者循環傳輸,它設定DMA_SxCR寄存器的CIRC位的值。 我們希望ADC采集是持續循環進行的,所以使用循環傳輸模式。
-
DMA_Priority:軟件設置數據流的優先級,有4個可選優先級分別為非常高、高、中和低,它設定DMA_SxCR寄存器的PL[1:0]位的值。 DMA優先級只有在多個DMA數據流同時使用時才有意義,這里我們設置為非常高優先級就可以了。
-
DMA_FIFOMode:FIFO模式使能,如果設置為DMA_FIFOMode_Enable表示使能FIFO模式功能; 它設定DMA_SxFCR寄存器的DMDIS位。ADC采集傳輸使用直接傳輸模式即可,不需要使用FIFO模式。
-
DMA_FIFOThreshold:FIFO閾值選擇,可選4種狀態分別為FIFO容量的1/4、1/2、3/4和滿;它設定DMA_SxFCR寄存器的FTH[1:0]位; DMA_FIFOMode設置為DMA_FIFOMode_Disable,那DMA_FIFOThreshold值無效。ADC采集傳輸不使用FIFO模式,設置改值無效。
-
DMA_MemoryBurst:存儲器突發模式選擇,可選單次模式、4節拍的增量突發模式、8節拍的增量突發模式或16節拍的增量突發模式, 它設定DMA_SxCR寄存器的MBURST[1:0]位的值。ADC采集傳輸是直接模式,要求使用單次模式。
-
DMA_PeripheralBurst:外設突發模式選擇,可選單次模式、4節拍的增量突發模式、8節拍的增量突發模式或16節拍的增量突發模式, 它設定DMA_SxCR寄存器的PBURST[1:0]位的值。ADC采集傳輸是直接模式,要求使用單次模式。
4.DMA存儲器到存儲器模式實驗
存儲器到存儲器模式可以實現數據在兩個內存的快速拷貝。我們先定義一個靜態的源數據,然后使用DMA傳輸把源數據拷貝到目標地址上,最后對比源數據和目標地址的數據,看看是否傳輸準確。
#ifndef __BSP_DMA_H
#define __BSP_DMA_H#ifdef __cplusplus
extern "C"{#endif#include "stm32f4xx.h"/* 相關宏定義,使用存儲器到存儲器傳輸必須使用DMA2 */
#define DMA_STREAM DMA2_Stream0
#define DMA_CHANNEL DMA_Channel_0
#define DMA_STREAM_CLOCK RCC_AHB1Periph_DMA2
#define DMA_IT_TCIF DMA_IT_TCIF0
#define DMA_IT_HTIF DMA_IT_HTIF0
#define DMA_FLAG_TCIF DMA_FLAG_TCIF0
#define DMA_FLAG_HTIF DMA_FLAG_HTIF0
#define DMA_STREAM_IRQn DMA2_Stream0_IRQn
#define DMA_STREAM_IRQHandler DMA2_Stream0_IRQHandler#define BUFFER_SIZE 32void Init_M2M_DMA(void);
#ifdef __cplusplus
}
#endif#endif
#include "bsp_dma.h"
#include "stdio.h"
#include "string.h"/* 定義aSRC_Const_Buffer數組作為DMA傳輸數據源
const關鍵字將aSRC_Const_Buffer數組變量定義為常量類型 */
const uint32_t aSRC_Const_Buffer[BUFFER_SIZE]= {0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80
};
/* 定義DMA傳輸目標存儲器 */
uint32_t aDST_Buffer[BUFFER_SIZE];void Init_M2M_DMA(void)
{/* 使能DMA時鐘 */RCC_AHB1PeriphClockCmd(DMA_STREAM_CLOCK, ENABLE);/* 復位初始化DMA數據流 */DMA_DeInit(DMA_STREAM);/* 確保DMA數據流復位完成 */while (DMA_GetCmdStatus(DMA_STREAM) != DISABLE) {}DMA_InitTypeDef DMA_InitStructure;DMA_InitStructure.DMA_BufferSize=BUFFER_SIZE;//一次DMA事務傳輸的數據個數DMA_InitStructure.DMA_Channel=DMA_CHANNEL;DMA_InitStructure.DMA_DIR=DMA_DIR_MemoryToMemory;DMA_InitStructure.DMA_FIFOMode=DMA_FIFOMode_Disable;DMA_InitStructure.DMA_FIFOThreshold=DMA_FIFOThreshold_Full;DMA_InitStructure.DMA_Memory0BaseAddr= (uint32_t)aDST_Buffer;DMA_InitStructure.DMA_MemoryBurst=DMA_MemoryBurst_Single;DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_Word;DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)aSRC_Const_Buffer;DMA_InitStructure.DMA_PeripheralBurst=DMA_PeripheralBurst_Single;DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Word;DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Enable;DMA_InitStructure.DMA_Priority=DMA_Priority_Low;DMA_Init(DMA_STREAM,&DMA_InitStructure);//配置中斷控制器并使能中斷NVIC_InitTypeDef NVIC_InitStruct;NVIC_InitStruct.NVIC_IRQChannel=DMA_STREAM_IRQn;NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStruct.NVIC_IRQChannelSubPriority=0;NVIC_Init(&NVIC_InitStruct);DMA_ITConfig(DMA_STREAM,DMA_IT_TCIF|DMA_IT_HTIF,ENABLE);DMA_ClearITPendingBit(DMA_STREAM,DMA_IT_TCIF|DMA_IT_HTIF);DMA_Cmd(DMA_STREAM,ENABLE);
}void DMA_STREAM_IRQHandler(void)
{if(SET== DMA_GetFlagStatus(DMA_STREAM,DMA_FLAG_HTIF)){printf("half transfer\r\n");DMA_ClearFlag(DMA_STREAM,DMA_FLAG_HTIF);}else if(SET== DMA_GetFlagStatus(DMA_STREAM,DMA_FLAG_TCIF)){printf("transfer complete\r\n");DMA_ClearFlag(DMA_STREAM,DMA_FLAG_TCIF);}// if(SET==DMA_GetITStatus(DMA_STREAM,DMA_IT_HTIF))
// {
// //half transfer complete
// printf("half transfer\r\n");
// DMA_ClearITPendingBit(DMA_STREAM,DMA_IT_HTIF);
//
// }
// else if(SET==DMA_GetITStatus(DMA_STREAM,DMA_IT_TCIF))
// {
// //transfer complete
//
// if(0== memcmp(aSRC_Const_Buffer,aDST_Buffer,BUFFER_SIZE))
// {
// printf("transfer complete\r\n");
// }
// DMA_ClearITPendingBit(DMA_STREAM,DMA_IT_TCIF);
// }
}
編程注意事項:
- 對于存儲器到存儲器傳輸模式,源地址和目標地址的設置, 采用與外設到存儲器模式相同配置。也就是源存儲器地址當作外設地址。
- 我們在中斷服務函數中檢查ITStatus時發現,沒有檢測到half transfer的IT標志位,但是可以檢測到transfer complete的IT標志位。但是FlagStatus都可以獲取到,也不知道是啥問題。
- 如果我換為其他的DMA2_STREAM,比如DMA2_Stream1。甚至transfer complete的IT標志位都沒有檢測到。也不知道是啥原因。
- 對于存儲器到存儲器傳輸模式,只能選擇DMA2。