從字面意思上看,DMA即為“直接內存讀取”的意思,換句話說DMA就是用來傳輸數據的,它也屬於一個外設。只是在傳輸數據時,無需占用CPU。
DMA請求
某個外設在通過DMA傳輸數據前,必須先給DMA控制器發送請求,控制器會返回一個應答信號給外設,外設應答后並且DMA控制器收到外設應答信號后,便會啟動DMA傳輸。這個過程類似於TCP的“三次握手”。
DMA有DMA1和DMA2兩個控制器,每個控制器都有不同的通道,每個通道對應不同的外設請求。如圖12-1為DMA1的通道請求、圖12-2為DMA2的通道請求。

圖12-1

圖12-2
如以上兩圖所示,DMA1有7個通道,DMA2有5個通道,每個通道都對應着不同的外設請求。既然這樣,就很有可能出現多個外設同時請求同一通道的情況。這響應先后順序該如何處理是好?那么,這就涉及到仲裁器管理了,用仲裁器來處理請求響應先后的問題。需要分兩個階段,第一階段是在DMA_CCRx寄存器中設置通道優先級,第二階段則需要判斷其通道編號,編號越低優先級越高。還有一點,DMA1優先級要高於DMA2。
DMA傳輸方向
DMA傳輸方向有三個:外設到內存,內存到外設,內存到內存。
外設到內存。即從外設讀取數據到內存。例如ADC采集數據到內存,ADC寄存器地址為源地址,內存地址為目標地址。
內存到外設。即從內存讀取數據到外設。例如串口向電腦發送數據,內存地址為源地址,串口數據寄存器地址為目標地址。此時內存存儲了需要發送的變量數據。
內存到內存。以內部flash向內部sram傳輸數據為例,此時內部flash地址即為源地址,內部sram地址即為目標地址。同時,需要將DMA_CCRx寄存器的MEM2MEM置位。
傳輸配置
我們需要確定數據每次傳輸的量,這個參數由DMA_CNDTRx寄存器配置。
再者,還有一個源地址和目標地址數據寬度的參數配置。由DMA_CCRx的PSIZE位和MSIZE位配置。可配置為8位、16位、32位。源地址和目標地址的數據寬度需要一致才可傳輸。
此外,數據想有序地傳輸,還需要配置源和目標數據指針的增量模式。由DMA_CCRx寄存器的PINC位和MINC位配置。例如串口向電腦發送數據,內存中的地址指針應該遞增的發送數據,而串口外設只有一個,所以外設的地址指針不變,無遞增。
傳輸狀態標識
可以通過查詢DMA_ISR寄存器的相應位的值來判斷傳輸狀態。如果在DMA_CCRx寄存器的相應位使能了相應中斷,則會產生中斷。
另外,傳輸完成還分成一次傳輸完成和循環傳輸完成。DMA在傳輸完成后,需要失能DMA后重新配置才能繼續傳輸。具體配置由DMA_CCRx寄存器的CIRC位完成。
DMA_InitTypeDef /** * @brief DMA Init structure definition */ typedef struct { uint32_t DMA_PeripheralBaseAddr; // 外設地址 uint32_t DMA_MemoryBaseAddr; // 內存地址 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_M2M; // 內存到內存模式 } DMA_InitTypeDef;
以上結構體代碼來自庫函數,我大概在每個成員后做了簡單注釋,在進行DMA編程時,需要對結構體成員進行配置。
內存到外設的DMA數據傳輸實驗
既然之前寫過串口通訊編程的相關文章,那干脆用串口外設來和內存進行數據傳輸吧。這里簡單講解一個從內存讀取數據到外設的DMA傳輸實驗,並且在實驗里用led燈驗證DMA傳輸時不占用CPU。
這里關於USART的配置就不繼續贅述,之前的文章有詳細的介紹,可移步閱讀。直接開始DMA配置。
#define SENDBUFF_SIZE 5000 //傳輸的數據量 uint8_t SendBuff[SENDBUFF_SIZE]; //內存里等待傳輸數據的數組 void USART_DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 串口外設為目標地址 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART1_BASE + 0x04; // 內存為源地址 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)SendBuff; // 傳輸方向,即從內存讀取數據到串口外設 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 數據傳輸量,初始化DMA_CNDTRx寄存器 DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE; // 外設地址不遞增 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 內存地址遞增 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 外設數據寬度 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 內存數據寬度 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 一次循環模式 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 通道優先級 DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 不使用內存到內存模式 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // DMA通道配置 DMA_Init(DMA1_Channel4, &DMA_InitStructure); // 清除傳輸完成標志位,避免產生不必要干擾 DMA_ClearFlag(DMA1_FLAG_TC4); DMA_Cmd(DMA1_Channel4, ENABLE); }
在main函數里調用USART、DMA、LED等的配置函數,用循環的方式往內存的數組里填充SENDBUFF_SIZE(5000)數量的字符作為等待傳輸的數據。然后調用庫函數USART_DMACmd()向DMA發出USART_DMAReq_Tx請求。同時,可以設置led頻閃狀態作為在DMA傳輸時占用CPU的進程,以驗證DMA傳輸不占用CPU。
程序編譯完成燒寫到開發板后,能看到在DMA傳輸過程中led同時也在頻閃,說明DMA傳輸過程確實不占用CPU資源,可以邊傳輸邊運行其他任務。
PS:有關led閃爍的延時控制,可用普通的軟件延時也可用SysTick定時器來完成,也很簡單。有關SysTick定時器的應用在之前的文章有過介紹,可移步閱讀。
分享一些關於DMA在數據傳輸方面的資料作為參考
stm32 如何用DMA搬運數據
http://www.makeru.com.cn/live/detail/1484.html?s=45051