「正點原子NANO STM32F103開發板資料連載」第二十二章 DMA 實驗

1)實驗平臺:【正點原子】 NANO STM32F103 開發板

2)摘自《正點原子STM32 F1 開發指南(NANO 板-HAL 庫版)》關注官方微信號公眾號,獲取更多資料:正點原子

「正點原子NANO STM32F103開發板資料連載」第二十二章 DMA 實驗

第二十二章 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 各通道一覽表:

「正點原子NANO STM32F103開發板資料連載」第二十二章 DMA 實驗

這裡解釋一下上面說的邏輯或,例如通道 1 的幾個 DMA1 請求(ADC1、TIM2_CH3、TIM4_CH1),

這幾個是通過邏輯或到通道 1 的,這樣我們在同一時間,就只能使用其中的一個。其他通道也

是類似的。

這裡我們要使用的是串口 1 的 DMA 傳送,也就是要用到通道 4。接下來,我們介紹一下 DMA

設置相關的幾個寄存器。

第一個是 DMA 中斷狀態寄存器(DMA_ISR)。該寄存器的各位描述如圖 22.1.1 所示:

「正點原子NANO STM32F103開發板資料連載」第二十二章 DMA 實驗

圖 22.1.1 DMA_ISR 寄存器各位描述

我們如果開啟了 DMA_ISR 中這些中斷,在達到條件後就會跳到中斷服務函數里面去,即使

沒開啟,我們也可以通過查詢這些位來獲得當前 DMA 傳輸的狀態。這裡我們常用的是 TCIFx,

即通道 DMA 傳輸完成與否的標誌。注意此寄存器為只讀寄存器,所以在這些位被置位之後,只

能通過其他的操作來清除。

第二個是 DMA 中斷標誌清除寄存器(DMA_IFCR)。該寄存器的各位描述如圖 27.1.2 所示:

「正點原子NANO STM32F103開發板資料連載」第二十二章 DMA 實驗

圖 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 所示:

「正點原子NANO STM32F103開發板資料連載」第二十二章 DMA 實驗

圖 22.4.1 DMA 串口實驗實物測試圖

伴隨 DS0 的不停閃爍,提示程序在運行。然後按 KEY0,DMA 數據開始傳輸,DS2 快閃

以表示數據正在傳輸,正常可以看到串口顯示如圖 22.4.2 所示的內容:

「正點原子NANO STM32F103開發板資料連載」第二十二章 DMA 實驗

圖 22.4.2 串口收到的數據內容

可以看到串口收到了 NANO STM32F103 發送過來的數據。

至此,我們整個 DMA 實驗就結束了,希望大家通過本章的學習,掌握 STM32F1 的 DMA

使用。DMA 是個非常好的功能,它不但能減輕 CPU 負擔,還能提高數據傳輸速度,合理的應

用 DMA,往往能讓你的程序設計變得簡單。


分享到:


相關文章: