1)實驗平臺:【正點原子】 NANO STM32F103 開發板
2)摘自《正點原子STM32 F1 開發指南(NANO 板-HAL 庫版)》關注官方微信號公眾號,獲取更多資料:正點原子
第二十二章 DMA 實驗
本章我們將向大家介紹 STM32F1 的 DMA。在本章中,我們將利用 STM32F1 的 DMA
來實現串口數據傳送,並在串口助手打印顯示。本章分為如下幾個部分:
22.1 STM32F1 DMA 簡介
22.2 硬件設計
22.3 軟件設計
22.4 下載驗證
22.1 STM32 DMA 簡介
DMA,全稱為:Direct Memory Access,即直接存儲器訪問,DMA 傳輸將數據從一個
地址空間複製到另外一個地址空間。當 CPU 初始化這個傳輸動作,傳輸動作本身是由
DMA 控制器 來實行和完成。典型的例子就是移動一個外部內存的區塊到芯片內部更快的
內存區。像是這樣的操作並沒有讓處理器工作拖延,反而可以被重新排程去處理其他的工
作。DMA 傳輸對於高效能嵌入式系統算法和網絡是很重要的。DMA 傳輸方式無需 CPU 直接
控制傳輸,也沒有中斷處理方式那樣保留現場和恢復現場的過程,通過硬件為 RAM 與 I/O 設備
開闢一條直接傳送數據的通路,能使 CPU 的效率大為提高。
STM32 最多有 2 個 DMA 控制器(DMA2 僅存在大容量產品中,中容量只有 DMA1),DMA1 有 7
個通道。DMA2 有 5 個通道。每個通道專門用來管理來自於一個或多個外設對存儲器訪問的請求。
還有一個仲裁起來協調各個 DMA 請求的優先權。
STM32 的 DMA 有以下一些特性:
●每個通道都直接連接專用的硬件 DMA 請求,每個通道都同樣支持軟件觸發。這些功能
通過軟件來配置。
●在七個請求間的優先權可以通過軟件編程設置(共有四級:很高、高、中等和低),假如
在相等優先權時由硬件決定(請求 0 優先於請求 1,依此類推) 。
●獨立的源和目標數據區的傳輸寬度(字節、半字、全字),模擬打包和拆包的過程。源和
目標地址必須按數據傳輸寬度對齊。
●支持循環的緩衝器管理
●每個通道都有 3 個事件標誌(DMA 半傳輸,DMA 傳輸完成和 DMA 傳輸出錯),這 3 個
事件標誌邏輯或成為一個單獨的中斷請求。
●存儲器和存儲器間的傳輸
●外設和存儲器,存儲器和外設的傳輸
●閃存、SRAM、外設的 SRAM、APB1 APB2 和 AHB 外設均可作為訪問的源和目標。
●可編程的數據傳輸數目:最大為 65536
STM32F103RBT6 有一個 DMA 控制器,DMA1,本章,我們僅針對 DMA1 進行介紹。
從外設(TIMx、ADC、SPIx、I2Cx 和 USARTx)產生的 DMA 請求,通過邏輯或輸入到
DMA 控制器,這就意味著同時只能有一個請求有效。外設的 DMA 請求,可以通過設置相應的
外設寄存器中的控制位,被獨立地開啟或關閉。
表 22.1.1 是 DMA1 各通道一覽表:
這裡解釋一下上面說的邏輯或,例如通道 1 的幾個 DMA1 請求(ADC1、TIM2_CH3、TIM4_CH1),
這幾個是通過邏輯或到通道 1 的,這樣我們在同一時間,就只能使用其中的一個。其他通道也
是類似的。
這裡我們要使用的是串口 1 的 DMA 傳送,也就是要用到通道 4。接下來,我們介紹一下 DMA
設置相關的幾個寄存器。
第一個是 DMA 中斷狀態寄存器(DMA_ISR)。該寄存器的各位描述如圖 22.1.1 所示:
圖 22.1.1 DMA_ISR 寄存器各位描述
我們如果開啟了 DMA_ISR 中這些中斷,在達到條件後就會跳到中斷服務函數里面去,即使
沒開啟,我們也可以通過查詢這些位來獲得當前 DMA 傳輸的狀態。這裡我們常用的是 TCIFx,
即通道 DMA 傳輸完成與否的標誌。注意此寄存器為只讀寄存器,所以在這些位被置位之後,只
能通過其他的操作來清除。
第二個是 DMA 中斷標誌清除寄存器(DMA_IFCR)。該寄存器的各位描述如圖 27.1.2 所示:
圖 22.1.2 DMA_IFCR 寄存器各位描述
DMA_IFCR 的各位就是用來清除 DMA_ISR 的對應位的,通過寫 0 清除。在 DMA_ISR 被置位後,
我們必須通過向該位寄存器對應的位寫入 0 來清除。
第三個是 DMA 通道 x 配置寄存器(DMA_CCRx)(x=1~7,下同)。該寄存器的我們在這裡就
不貼出來了,見《STM32 參考手冊》第 150 頁 10.4.3 一節。該寄存器控制著 DMA 的很多相關信
息,包括數據寬度、外設及存儲器的寬度、通道優先級、增量模式、傳輸方向、中斷允許、使
能等都是通過該寄存器來設置的。所以 DMA_CCRx 是 DMA 傳輸的核心控制寄存器。
第四個是 DMA 通道 x 傳輸數據量寄存器(DMA_CNDTRx)。這個寄存器控制 DMA 通道 x 的每
次傳輸所要傳輸的數據量。其設置範圍為 0~65535。並且該寄存器的值會隨著傳輸的進行而減
少,當該寄存器的值為 0 的時候就代表此次數據傳輸已經全部發送完成了。所以可以通過這個
寄存器的值來知道當前 DMA 傳輸的進度。
第五個是 DMA 通道 x 的外設地址寄存器(DMA_CPARx)。該寄存器用來存儲 STM32 外設的地
址,比如我們使用串口 1,那麼該寄存器必須寫入 0x40013804(其實就是&USART1_DR)。如果
使用其他外設,就修改成相應外設的地址就行了。
最後一個是 DMA 通道 x 的存儲器地址寄存器(DMA_CMARx),該寄存器和 DMA_CPARx 差不多,
但是是用來放存儲器的地址的。比如我們使用 SendBuf[5200]數組來做存儲器,那麼我們在
DMA_CMARx 中寫入&SendBuff 就可以了。
DMA 相關寄存器就為大家介紹到這裡,此節我們要用到串口 1 的發送,屬於 DMA1 的通道 4
(表 27.1.1),接下來我們就介紹 HAL 庫配置步驟和方法。首先這裡我們需要指出的是,DMA
相關的庫函數文件在文件 stm32f1xx_hal_dma.c/stm32f1xx_hal_dma_ex.c 以及對應的頭文件
中,同時因為我們是用串口的 DMA 功能,所以還要加入串口相關的文件 stm32f1xx_hal_uart.c。
具體步驟如下:
1)使能 DMA1 時鐘
__HAL_RCC_DMA1_CLK_ENABLE(); //DMA1 時鐘使能
2)初始化 DMA 通道 4,包括配置通道,外設地址,存儲器地址,傳輸數據量等參數
DMA 的某個數據流各種配置參數初始化是通過 HAL_DMA_Init 函數實現的,該函數聲明為:
HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);
該函數只有一個 DMA_HandleTypeDef 結構體指針類型入口參數,結構體定義為:
typedef struct __DMA_HandleTypeDef
{
DMA_Channel_TypeDef *Instance;
DMA_InitTypeDef Init;
HAL_LockTypeDef Lock;
HAL_DMA_StateTypeDef State;
void *Parent;
void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma);
void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);
void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma);
__IO uint32_t ErrorCode;
DMA_TypeDef *DmaBaseAddress;
uint32_t ChannelIndex;
}DMA_HandleTypeDef;
成員變量 Instance 是用來設置寄存器基地址,例如要設置為 DMA1 的通道 4,那麼取值為
DMA1_Channel4。
成員變量 Parent 是 HAL 庫處理中間變量,用來指向 DMA 通道外設句柄。
成員變量 XferCpltCallback(傳輸完成回調函數),XferHalfCpltCallback(半傳輸完成回調
函數),XferErrorCallback(傳輸錯誤回調函數),XferAbortCallback(傳輸中止回調函數)是
四個函數指針,用來指向回調函數入口地址。
成員變量 DmaBaseAddress 和 ChannelIndex 是通道基地址和索引好,這個是 HAL 庫處理的
時候會自動計算,用戶無需設置。
其他成員變量 HAL 庫處理過程狀態標識變量,這裡就不做過多講解。接下來我們著重看
看成員變量 Init,它是 DMA_InitTypeDef 結構體類型,該結構體定義為:
typedef struct
{
uint32_t Direction; //傳輸方向,例如存儲器到外設 DMA_MEMORY_TO_PERIPH
uint32_t PeriphInc; //外設(非)增量模式,非增量模式 DMA_PINC_DISABLE
uint32_t MemInc; //存儲器(非)增量模式,增量模式 DMA_MINC_ENABLE
uint32_t PeriphDataAlignment; //外設數據大小:8/16/32 位。
uint32_t MemDataAlignment; //存儲器數據大小:8/16/32 位。
uint32_t Mode; //模式:循環模式/普通模式
uint32_t Priority; //DMA 優先級:低/中/高/非常高
} DMA_InitTypeDef;
該結構體成員非常多,但是每個成員變量配置的基本都是 DMA_SxCR 寄存器和
DMA_IFCR 寄存器的響應為。我們把結構體各個成員通過註釋的方式列出來了。例如本實驗我
們要用到 DMA1_Channel4。把內存中數組的值發送到串口外設發送寄存器 DR,所以方向為寄
存器到外設 DMA_MEMORY_TO_PERIPH,一個一個字節發送,需要數字索引自動增加,所以
是存儲器增量模式 DMA_MINC_ENABLE,存儲器和外設的字寬都是字節 8 位。具體配置如下:
DMA_HandleTypeDef UART1TxDMA_Handler; //DMA 句柄
UART1TxDMA_Handler.Instance=DMA1_Channel4; //通道選擇
UART1TxDMA_Handler.Init.Direction=DMA_MEMORY_TO_PERIPH; //存儲器到外設
UART1TxDMA_Handler.Init.PeriphInc=DMA_PINC_DISABLE; //外設非增量模式
UART1TxDMA_Handler.Init.MemInc=DMA_MINC_ENABLE; //存儲器增量模式
UART1TxDMA_Handler.Init.PeriphDataAlignment=DMA_PDATAALIGN_BYTE;
//外設數據長度:8 位
UART1TxDMA_Handler.Init.MemDataAlignment=DMA_MDATAALIGN_BYTE;
//存儲器數據長度:8 位
UART1TxDMA_Handler.Init.Mode=DMA_NORMAL; //外設普通模式
UART1TxDMA_Handler.Init.Priority=DMA_PRIORITY_MEDIUM; //中等優先級
這裡大家要注意,HAL 庫為了處理各類外設的 DMA 請求,在調用相關函數之前,需要調
用一個宏定義標識符,來連接 DMA 和外設句柄。例如要使用串口 DMA 發送,所以方式為:
__HAL_LINKDMA(&UART1_Handler,hdmatx,UART1TxDMA_Handler);
其 中 UART1_Handler 是 串 口 初 始 化 句 柄 , 我 們 在 usart.c 中 定 義 過 了 。
UART1TxDMA_Handler 是 DMA 初始化句柄。hdmatx 是外設句柄結構體的成員變量,在這裡
實際就是 UART1_Handler 的成員變量。在 HAL 庫中,任何一個可以使用 DMA 的外設,它的
初始化結構體句柄都會有有個 DMA_HandleTypeDef 指針類型的成員變量,是 HAL 庫用來做相
關指向的。Hdmatx 就是 DMA_HandleTypeDef 結構體指針類型。
這 句 話 的 含 義 就 是 把 UART1_Handler 句 柄 的 成 員 變 量 hdmatx 和 DMA 句 柄
UART1TxDMA_Handler 連接起來,是純軟件處理,沒有任何硬件操作。
這裡我們就點到為止,如果大家要詳細瞭解 HAL 庫指向關係,請查看本實驗宏定義標識
符__HAL_LINKDMA 的定義和調用方法,就會很清楚了。
3)使能串口 DMA 發送
串口 1 的 DMA 發送實際是串口控制寄存器 CR3 的位 7 來控制的,在 HAL 庫中,多次操作該
寄存器來使能串口 DMA 發送,但是它並沒有提供一個獨立的使能函數,所以這裡我們可以通過
直接操作寄存器方式來實現:
USART1->CR3 | =USART_CR3_DMAT;//使能串口 1 的 DMA 發送
HAL 庫還提供了對串口的 DMA 發送的停止,暫停,繼續等操作函數:
HAL_StatusTypeDef HAL_USART_DMAStop(USART_HandleTypeDef *husart);//停止
HAL_StatusTypeDef HAL_USART_DMAPause(USART_HandleTypeDef *husart);//暫停
HAL_StatusTypeDef HAL_USART_DMAResume(USART_HandleTypeDef *husart);//恢復
這些函數使用方法這裡我們就不累贅了。
4) 使能 DMA1 通道 4,啟動傳輸。
使能串口 DMA 發送之後,我們接著就要使能 DMA 傳輸通道:
HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress,
uint32_t DstAddress, uint32_t DataLength);
這個函數比較好理解,第一個參數是 DMA 句柄,第二個是傳輸源地址,第三個是傳輸目
標地址,第四個是傳輸的數據長度。
通過以上 4 步設置,我們就可以啟動一次 USART1 的 DMA 傳輸了。
5)查詢 DMA 傳輸狀態
在 DMA 傳輸過程中,我們要查詢 DMA 傳輸通道的狀態,使用的函數是:
__HAL_DMA_GET_FLAG(&UART1TxDMA_Handler,DMA_FLAG_TC4);
獲取當前傳輸剩餘數據量:
__HAL_DMA_GET_COUNTER(&UART1TxDMA_Handler);
6)DMA 中斷使用方法
DMA 中斷對於每個通道都有一箇中斷服務函數,比如 DMA1_Channel4 的中斷服務函
數為 DMA1_Channel4_IRQHandler。同樣,HAL 庫也提供了一個通用的 DMA 中斷處理函
數 HAL_DMA_IRQHandler,在該函數內部,會對 DMA 傳輸狀態進行分析,然後調用響應
的中斷處理回調函數:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);//發送完成回調函數
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);//發送一半回調函數
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//接收完成回調函數
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);//接收一半回調函數
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);//傳輸出錯回調函數
對於串口 DMA 開啟,使能數據流,啟動傳輸,這些步驟,如果使用了中斷,可以直接調
用 HAL 庫函數 HAL_USART_Transmit_DMA,該函數聲明如下:
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart,
uint8_t *pData, uint16_t Size);
22.2 硬件設計
所以本章用到的硬件資源有:
1) 指示燈 DS0、DS2
2) KEY0 按鍵
3) 串口
4) DMA
本章我們將利用外部按鍵 KEY0 來控制 DMA 的傳送,每按一次 KEY0,DMA 就傳送一次數據到
USART1,同時 DS2 燈作為傳輸進度燈。DS0 還是用來做為程序運行的指示燈。
本章實驗需要注意 P5 口的 RXD 和 TXD 是否和 PA9 和 PA10 連接上,如果沒有,請先連接。
22.3 軟件設計
打開我們的 DMA 傳輸實驗,可以發現,我們的實驗中多了 dma.c 文件和其頭文件 dma.h,
同時我們要引入 dma 相關的庫函數文件 stm32f1xx_hal_dma.c 和 stm32f1xx_hal_dma.h。
打開 dma.c 文件,代碼如下:
DMA_HandleTypeDef UART1TxDMA_Handler; //DMA 句柄
//DMA1 的各通道配置
//這裡的傳輸形式是固定的,這點要根據不同的情況來修改
//從存儲器->外設模式/8 位數據寬度/存儲器增量模式
//chx:DMA 通道選擇,DMA1_Channel1~DMA1_Channel7
void MYDMA_Config(DMA_Channel_TypeDef *chx)
{
__HAL_RCC_DMA1_CLK_ENABLE();//DMA1 時鐘使能
__HAL_LINKDMA(&UART1_Handler,hdmatx,UART1TxDMA_Handler);
//將 DMA 與 USART1 聯繫起來(發送 DMA)
//Tx DMA 配置
UART1TxDMA_Handler.Instance=chx; //通道選擇
UART1TxDMA_Handler.Init.Direction=DMA_MEMORY_TO_PERIPH; //存儲器到外設
UART1TxDMA_Handler.Init.PeriphInc=DMA_PINC_DISABLE; //外設非增量模式
UART1TxDMA_Handler.Init.MemInc=DMA_MINC_ENABLE; //存儲器增量模式
UART1TxDMA_Handler.Init.PeriphDataAlignment=DMA_PDATAALIGN_BYTE;
//外設數據長度:8 位
UART1TxDMA_Handler.Init.MemDataAlignment=DMA_MDATAALIGN_BYTE;
//存儲器數據長度:8 位
UART1TxDMA_Handler.Init.Mode=DMA_NORMAL; //外設普通模式
UART1TxDMA_Handler.Init.Priority=DMA_PRIORITY_MEDIUM; //中等優先級
HAL_DMA_DeInit(&UART1TxDMA_Handler);
HAL_DMA_Init(&UART1TxDMA_Handler);
}
//開啟一次 DMA 傳輸
//huart:串口句柄
//pData:傳輸的數據指針
//Size:傳輸的數據量
void MYDMA_USART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData,
uint16_t Size)
{
HAL_DMA_Start(huart->hdmatx, (u32)pData, (uint32_t)&huart->Instance->DR, Size);
//開啟 DMA 傳輸
huart->Instance->CR3 |= USART_CR3_DMAT;//使能串口 DMA 發送
}
該部分代碼僅僅 2 個函數,MYDMA_Config 函數,基本上就是按照我們上面介紹的步驟來使
能 DMA 時鐘和初始化 DMA 的,該函數是一個通用的 DMA 配置函數,DMA1 的所有通道,都可以利
用該函數配置,不過有些固定參數可能要適當修改(比如位寬,傳輸方向等)。該函數在外部
只能修改 DMA 通道號,更多的其他設置只能在該函數內部修改。MYDMA_USART_Transmit 函數就
是按照 22.1 小節講解的步驟 3 和步驟 4 來啟動串口 DMA 傳輸的。對照前面的配置步驟的詳細講
解來分析這部分代碼即可。
dma.h 頭文件內容比較簡單,主要是函數聲明,這裡我們不細說。
接下來我們看看 main 函數如下:
const u8 TEXT_TO_SEND[]={"ALIENTEK NANO STM32 DMA 串口實驗"};
#define TEXT_LENTH sizeof(TEXT_TO_SEND)-1
//TEXT_TO_SEND 字符串長度(不包含結束符)
u8 SendBuff[(TEXT_LENTH+2)*100];
int main(void)
{
u16 i;
u8 t=0;
HAL_Init(); //初始化 HAL 庫
Stm32_Clock_Init(RCC_PLL_MUL9); //設置時鐘,72M
delay_init(72); //初始化延時函數
uart_init(115200);
//串口初始化為 115200
LED_Init();
//初始化與 LED 連接的硬件接口
KEY_Init();
//按鍵初始化
MYDMA_Config(DMA1_Channel4); //初始化 DMA1 通道 4
printf("NANO STM32\\r\\n");
printf("DMA TEST\\r\\n");
printf("KEY0:Start\\r\\n");
//顯示提示信息
for(i=0;i
{
if(t>=TEXT_LENTH)//加入換行符
{
SendBuff[i++]=0x0d;
SendBuff[i]=0x0a;
t=0;
}else SendBuff[i]=TEXT_TO_SEND[t++];//複製 TEXT_TO_SEND 語句
}
i=0;
while(1)
{
t=KEY_Scan(0);
if(t==KEY0_PRES)//KEY0 按下
{
printf("\\r\\nDMA DATA:\\r\\n");
HAL_UART_Transmit_DMA(&UART1_Handler,SendBuff,
(TEXT_LENTH+2)*100);//啟動傳輸
//等待 DMA 傳輸完成,此時我們來做另外一些事,點燈
//實際應用中,傳輸數據期間,可以執行另外的任務
while(1)
{
if(__HAL_DMA_GET_FLAG(&UART1TxDMA_Handler,
DMA_FLAG_TC4))//等待 DMA1 通道 4 傳輸完成
{
HAL_DMA_CLEAR_FLAG(&UART1TxDMA_Handler,
DMA_FLAG_TC4);//清除 DMA1 通道 4 傳輸完成標誌
HAL_UART_DMAStop(&UART1_Handler);
//傳輸完成以後關閉串口 DMA
break;
}
LED2=!LED2;
delay_ms(50);
}
LED2=1;
printf("Transimit Finished!\\r\\n");//提示傳送完成
}
i++;
delay_ms(10);
if(i==20)
{
LED0=!LED0;//提示系統正在運行
i=0;
}
}
}
main 函數的流程大致是:先初始化內存 SendBuff 的值,然後通過 KEY0 開啟串口 DMA 發
送,在發送過程中,通過__HAL_DMA_GET_FLAG(&UART1TxDMA_Handler),獲取當前是否傳
輸結束,並且 DS2 閃爍。最後在傳輸結束之後清除相應標誌位,提示已經傳輸完成。
至此,DMA 串口傳輸的軟件設計就完成了。
22.4 下載驗證
在代碼編譯成功之後,我們下載代碼到 ALIENTEK NANO STM32F103 上,我們打開串口
調試助手,可以看到串口顯示如圖 22.4.1 所示:
圖 22.4.1 DMA 串口實驗實物測試圖
伴隨 DS0 的不停閃爍,提示程序在運行。然後按 KEY0,DMA 數據開始傳輸,DS2 快閃
以表示數據正在傳輸,正常可以看到串口顯示如圖 22.4.2 所示的內容:
圖 22.4.2 串口收到的數據內容
可以看到串口收到了 NANO STM32F103 發送過來的數據。
至此,我們整個 DMA 實驗就結束了,希望大家通過本章的學習,掌握 STM32F1 的 DMA
使用。DMA 是個非常好的功能,它不但能減輕 CPU 負擔,還能提高數據傳輸速度,合理的應
用 DMA,往往能讓你的程序設計變得簡單。