一、DMA功能簡介
首先嘮叨一下DMA的基本概念,DMA的出現大大減輕了CPU的工作量。在硬件系統中,主要由CPU(內核)、外設、內存(SRAM)、總線等結構組成,數據經常要在內存和外設之間,外設和外設之間轉移。例如:CPU需要處理從外設采集回來的數據,CPU需要先將數據從ADC外設的寄存器讀取到內存中(變量)去,然后進行運算處理,這是一般的解決方法。CPU的資源是非常寶貴的,我們可以設法把轉移的工作交給其他部件來完成,CPU把更多的資源用於數據運算和中斷響應上,如此DMA便登場了。DMA正是為CPU分擔數據轉移工作,因為DMA的存在,CPU才被解放出來,它可以在數據轉移的同時進行數據運算,相應中斷,大大提高了效率。
二、DMA的主要特性

三、DMA中斷特性

四、DMA之串口通信
我們實現一個簡單的功能,在DMA中處理串口通信,把數據轉移的工作交給DMA,DMA把數據從內存(數組)到外設(串口)的轉移,在main函數中不斷進行閃燈操作,這樣我們可以看到DMA在工作的時候CPU也在工作。非常有必要復習一下DMA的對應關系,我們知道stm32總共有2個DMA控制器(DMA1有7個通道,DMA2有5個通道),每個通道專門用來管理來自一個或多個外設對存儲器訪問的請求,還有一個仲裁器來協調DMA請求的優先級(優先級分:很高、高、中等、低),這可不是隨便對應的。

1、LED初始化程序如下:
void LED_GPIO_Config(void)
{
/*定義一個GPIO_InitTypeDef類型的結構體*/
GPIO_InitTypeDef GPIO_InitStructure;
/*開啟LED的外設時鍾*/
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);
/*選擇要控制的GPIOB引腳*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
/*設置引腳模式為通用推挽輸出*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/*設置引腳速率為50MHz */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/*調用庫函數,初始化GPIOB0*/
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* 關閉所有led燈 */
GPIO_SetBits(GPIOB, GPIO_Pin_14);
}
這個地方地方沒什么要注意的,唯一要注意的就是輸入輸出模式,我們按需求這樣配就好了。
2、串口初始化
void USART3_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
/* config USART3 clock */
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB , ENABLE);
RCC_APB1PeriphClockCmd( RCC_APB1Periph_USART3, ENABLE);
/* USART1 GPIO config */
/* Configure USART1 Tx (PA.09) as alternate function push-pull */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* Configure USART1 Rx (PA.10) as input floating */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* USART1 mode config */
USART_InitStructure.USART_BaudRate = 38400;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No ;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART3, &USART_InitStructure);
USART_Cmd(USART3, ENABLE);
}
3、DMA初始化
void USART3_DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
/*開啟DMA時鍾*/
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
//NVIC_Config(); //配置DMA中斷
//NVIC_Configuration();
/*設置DMA源:串口數據寄存器地址*/
DMA_InitStructure.DMA_PeripheralBaseAddr = USART3_DR_Base;
/*內存地址(要傳輸的變量的指針)*/
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)SendBuff;
/*方向:從內存到外設*/
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
/*傳輸大小DMA_BufferSize=SENDBUFF_SIZE*/
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;
/*內存數據單位 8bit*/
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
/*DMA模式:不斷循環*/
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;
//DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
/*優先級:中*/
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
/*禁止內存到內存的傳輸 */
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
/*配置DMA1的2通道*/
DMA_Init(DMA1_Channel2, &DMA_InitStructure);
//DMA_ITConfig(DMA1_Channel2,DMA_IT_TC,ENABLE); //配置DMA發送完成后產生中斷
/*使能DMA*/
DMA_Cmd (DMA1_Channel2,ENABLE);
}
在這里我們要注意以下幾點:
(1)DMA_InitStructure.DMA_PeripheralBaseAddr = USART3_DR_Base;這里對應USART數據寄存器地址,這個地址我們是這樣定義的:#define USART3_DR_Base 0x40004804,這個值是怎么算出來的呢?我們可以查看stm32存儲器映射表:

USART3的起始地址是0x40004800,我們查看stm32串口數據寄存器偏移地址為0x04

因此我們可以計算到USART3數據寄存器地址為0x40004804
(2)我們數據傳輸方向內存(變量)到外設(串口),所以DMA方向為內存到外設
(3)DMA傳輸模式有兩種:DMA_Mode_Normal(普通模式),DMA只傳輸一次;DMA_Mode_Circular(循環模式),DMA循環傳輸,比如在AD采集時要配置成循環模式。
4、主函數
int main(void)
{
/* USART1 config 115200 8-N-1 */
USART3_Config();
USART3_DMA_Config();
LED_GPIO_Config();
printf("\r\n usart3 DMA TX 測試 \r\n");
{
uint16_t i;
/*填充將要發送的數據*/
for(i=0;i<SENDBUFF_SIZE;i++)
{
SendBuff[i] = 'A';
}
}
/* USART1 向 DMA發出TX請求 */
USART_DMACmd(USART3, USART_DMAReq_Tx, ENABLE);
/* 此時CPU是空閑的,可以干其他的事情 */
//例如同時控制LED
for(;;)
{
LED1(ON);
Delay(0xFFFFF);
LED1(OFF);
Delay(0xFFFFF);
}
}
這個函數很簡單,我們很容易就可以實現,達到效果,這里就不貼圖片了。
五、串口通信DMA傳輸完成中斷
我們知道DMA可以在傳輸過半,傳輸完成,傳輸錯誤時產生中斷。我們實現的功能是,DMA工作在普通模式下即只傳輸一次,LED燈初始化是關閉的,DMA傳輸完成后產生一個中斷,在中斷中我們做點燈操作。這個程序調了一天才調了出來,並不是因為它很難,而是有一些要注意的地方沒有注意到,從而到時耽誤了好長時間才調出來。不過有錯誤就會有進步嘛。
我先貼出正確的代碼,然后在討論我犯的錯誤,由於和上一個程序好多都是一樣的,這里我們只貼出不同的地方。
(1)DMA初始化程序:
void USART3_DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
/*開啟DMA時鍾*/
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
//NVIC_Config(); //配置DMA中斷 NVIC_Configuration();
/*設置DMA源:串口數據寄存器地址*/
DMA_InitStructure.DMA_PeripheralBaseAddr = USART3_DR_Base;
/*內存地址(要傳輸的變量的指針)*/
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)SendBuff;
/*方向:從內存到外設*/
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
/*傳輸大小DMA_BufferSize=SENDBUFF_SIZE*/
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;
/*內存數據單位 8bit*/
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
/*DMA模式:不斷循環*/
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;
//DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
/*優先級:中*/
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
/*禁止內存到內存的傳輸 */
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
/*配置DMA1的2通道*/
DMA_Init(DMA1_Channel2, &DMA_InitStructure);
DMA_ITConfig(DMA1_Channel2,DMA_IT_TC,ENABLE); //配置DMA發送完成后產生中斷
/*使能DMA*/
DMA_Cmd (DMA1_Channel2,ENABLE);
}
注意我們在這里打開了DMA傳輸完成中斷。
(2)NVIC初始化
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure; /* Configure one bit for preemption priority */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
(3)中斷處理程序
我們在stm32f10x_it.c中編寫我們的中斷處理程序:
void DMA1_Channel2_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC2))
{
LED1(ON);
DMA_ClearITPendingBit(DMA1_IT_GL2); //清除全部中斷標志
}
}
我們也可以這樣寫中斷處理程序:
void DMA1_Channel2_IRQHandler(void)
{
if(DMA_GetFlagStatus(DMA1_FLAG_TC2))
{
LED1(ON);
DMA_ClearFLAG(DMA1_FLAG_TC2); //清除全部中斷標志
}
}
這兩種寫法都行,我們在庫開發文檔可以查看。都代表DMA的通道2傳輸完成中斷。
(4)主函數
int main(void)
{
/* USART1 config 115200 8-N-1 */
USART3_Config();
USART3_DMA_Config();
LED_GPIO_Config();
printf("\r\n usart3 DMA TX 測試 \r\n");
{
uint16_t i;
/*填充將要發送的數據*/
for(i=0;i<SENDBUFF_SIZE;i++)
{
SendBuff[i] = 'A';
}
}
/* USART1 向 DMA發出TX請求 */
USART_DMACmd(USART3, USART_DMAReq_Tx, ENABLE);
/* 此時CPU是空閑的,可以干其他的事情 */
//例如同時控制LED
for(;;)
{
}
}
這樣我們實驗便可以看到,LED燈初始化是關閉的,當串口發送完40000字節的‘A’后,LED等亮。
(5)補充
原意是測試DMA發送完成中斷指的是每次指定字節發送完成后便產生一個中斷還是最終都傳輸完成觸發一次中斷,剛開始中斷處理函數寫的程序如下:
void DMA1_Channel2_IRQHandler(void)
{
uint16_t n = 0;
if(DMA_GetFlagStatus(DMA1_FLAG_TC2))
{
n = ~n;
if(n) LED1(ON);
else LED1(OFF);
DMA_ClearFlag(DMA1_FLAG_TC2); //清除全部中斷標志
}
}
通過測試,我發現LED燈並沒有像試想的那樣每次發送完成后便觸發一次中斷,然后燈會間隔閃爍,而實際是第一次傳輸完成后燈點亮,之后就一直保持亮的狀態。剛開始我還以為DMA只會觸發第一次中斷,后來仔細分析后才發現了問題。正確的代碼應該如下。
void DMA1_Channel2_IRQHandler(void) { static uint16_t n = 0; if(DMA_GetFlagStatus(DMA1_FLAG_TC2)) { n = ~n; if(n) LED1(ON); else LED1(OFF); DMA_ClearFlag(DMA1_FLAG_TC2); //清除全部中斷標志 } }
在這里n是一個局部變量,如果不定義成靜態變量,每次出中斷時后n所占的內存(棧)便會釋放,這樣再次進入后n還是會初始化為0.與我們要達到的效果不符。因此,在這里我們把它指定為靜態變量,那么內存就不會釋放,它會保持上一次的的值,修改之后達到了效果,每次傳輸完成3000個字節后燈的狀態就會改變一次。
在這里我們整理一下變量:
全局動態變量:作用范圍為整個工程,不釋放內存,會保持上一次的值。
全局靜態變量:作用范圍為當前文件,不釋放內存,會保持上一次的值。
局部動態變量:作用范圍為當前函數,每次函數執行結束釋放內存,不會保持上一次的值。
局部靜態變量:作用范圍為當前函數,不釋放內存,會保持上一次的值。
