關於DMA原理部分講解,及CubeMx配置部分,請參考該文章
【STM32】HAL庫 STM32CubeMX教程十一—DMA (串口DMA發送接收)
本篇文章我們僅針對例程進行詳解剖析
歷程詳解
詳解包括:
- 中斷原理講解
- 例程流程詳解
- 庫函數分析詳解
- 對應寄存器介紹
- 對應函數介紹
- 對應註釋詳解
本篇文章提供兩種方法:
一種是 :IDLE 接收空閒中斷+DMA
一種是: IDLE 接收空閒中斷+RXNE接收數據中斷
都可完成串口數據的收發
知識點介紹:
STM32 IDLE 接收空閒中斷
功能:
在使用串口接受字符串時,可以使用空閒中斷(IDLEIE置1,即可使能空閒中斷),這樣在接收完一個字符串,進入空閒狀態時(IDLE置1)便會激發一個空閒中斷。在中斷處理函數,我們可以解析這個字符串。
接受完一幀數據,觸發中斷
STM32的IDLE的中斷產生條件:
在串口無數據接收的情況下,不會產生,當清除IDLE標誌位後,必須有接收到第一個數據後,纔開始觸發,一但接收的數據斷流,沒有接收到數據,即產生IDLE中斷
STM32 RXNE接收數據中斷
功能:
當串口接收到一個bit的數據時,(讀取到一個停止位) 便會觸發 RXNE接收數據中斷
接受到一個字節的數據,觸發中斷
比如給上位機給單片機一次性發送了8個字節,就會產生8次RXNE中斷,1次IDLE中斷。
串口CR1寄存器
對bit4寫1開啓IDLE接受空閒中斷
,對bit5寫1開啓RXNE接收數據中斷。
串口ISR寄存器
此寄存器爲串口狀態查詢寄存器
當串口接收到數據時,bit5 RXNE就會自動變成1,當接收完一幀數據後,bit4就會變成1.
清除RXNE中斷標誌位的方法爲:
只要把接收到的一個字節讀出來,就會清除這個中斷
在STM32F1 /STM32F4 系列中 清除IDLE中斷標誌位的方法爲:
- 先讀SR寄存器,
- 再讀DR寄存器。
memset()函數
extern void *memset(void *buffer, int c, int count)
- buffer:爲指針或是數組
- c:是賦給buffer的值
- count:是buffer的長度.
USART採用DMA接收時,如何讀取當前接收字節數?
#define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->CNDTR);
DMA接收時該宏將返回當前接收空間剩餘字節
實際接受的字節= 預先定義的接收總字節 - __HAL_DMA_GET_COUNTER()
其本質就是讀取NTDR寄存器,DMA通道結構體中定義了NDTR寄存器,讀取該寄存器即可得到未傳輸的數據數呢,
NTDR寄存器
實現方法:
兩種利用串口IDLE空閒中斷的方式接收一幀數據,方法如下:
方法1:實現思路:採用STM32F103的串口1,並配置成空閒中斷IDLE模式且使能DMA接收,並同時設置接收緩衝區和初始化DMA。那麼初始化完成之後,當外部給單片機發送數據的時候,假設這次接受的數據長度是200個字節,那麼在單片機接收到一個字節的時候並不會產生串口中斷,而是DMA在後臺把數據默默地搬運到你指定的緩衝區數組裏面。當整幀數據發送完畢之後串口才會產生一次中斷,此時可以利用__HAL_DMA_GET_COUNTER(&hdma_usart1_rx);函數計算出當前DMA接收存儲空間剩餘字節
本次的數據接受長度=預先定義的接收總字節-接收存儲空間剩餘字節
- 比方說 本次串口接受200個字節,
HAL_UART_Receive_DMA(&huart1,rx_buffer,200);//打開DMA接收
-
然後我發送了 Zxiaoxuan 9個字節的數據長度
-
那麼此時 GET_COUNTER函數讀出來 接收存儲空間剩餘字節 就是191個字節
-
實際接受的字節(9) = 預先定義的接收總字節(200) - __HAL_DMA_GET_COUNTER()(191)
9 = 200-191
應用對象:適用於各種串口相關的通信協議,如:MODBUS,PPI ;還有類似於GPS數據接收解析,串口WIFI的數據接收等,都是很好的應用對象。
方法2:實現思路:直接利用stm32的RXNE和IDLE中斷進行接收不定字節數據。 每次接收到一個字節的數據,觸發RXNE中斷 將該字節數據存放到數組裏,傳輸完成之後,觸發一次IDLE中斷,對已經獲取到的數據進行處理
例程1
本例程功能:
使用DMA+串口接受空閒中斷 實現將接收的數據完整發送到上位機的功能
接收數據的流程:
首先在初始化的時候打開DMA接收,當MCU通過USART接收外部發來的數據時,在進行第①②③步的時候,DMA直接將接收到的數據寫入緩存rx_buffer[100] //接收數據緩存數組,程序此時也不會進入接收中斷,在軟件上無需做任何事情,要在初始化配置的時候設置好配置就可以了。
數據接收完成的流程:
當數據接收完成之後產生接收空閒中斷④
在中斷服務函數中做這幾件事:
- 判斷是否爲IDLE接受空閒中斷
- 在中斷服務函數中將接收完成標誌位置1
- 關閉DMA防止在處理數據時候接收數據,產生干擾。
- 計算出接收緩存中的數據長度,清除中斷位,
while循環 主程序流程:
- 主程序中檢測到接收完成標誌被置1
- 進入數據處理程序,現將接收完成標誌位置0,
- 將接收到的數據重新發送到上位機
- 重新設置DMA下次要接收的數據字節數,使能DMA進入接收數據狀態。
例程代碼:
uart.c
volatile uint8_t rx_len = 0; //接收一幀數據的長度
volatile uint8_t recv_end_flag = 0; //一幀數據接收完成標誌
uint8_t rx_buffer[100]={0}; //接收數據緩存數組
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
//下方爲自己添加的代碼
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //使能IDLE中斷
//DMA接收函數,此句一定要加,不加接收不到第一次傳進來的實數據,是空的,且此時接收到的數據長度爲緩存器的數據長度
HAL_UART_Receive_DMA(&huart1,rx_buffer,BUFFER_SIZE);
}
uart.h
extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_rx;
extern DMA_HandleTypeDef hdma_usart1_tx;
/* USER CODE BEGIN Private defines */
#define BUFFER_SIZE 100
extern volatile uint8_t rx_len ; //接收一幀數據的長度
extern volatile uint8_t recv_end_flag; //一幀數據接收完成標誌
extern uint8_t rx_buffer[100]; //接收數據緩存數組
main.c
/*
*********************************************************************************************************
* 函 數 名: DMA_Usart_Send
* 功能說明: 串口發送功能函數
* 形 參: buf,len
* 返 回 值: 無
*********************************************************************************************************
*/
void DMA_Usart_Send(uint8_t *buf,uint8_t len)//串口發送封裝
{
if(HAL_UART_Transmit_DMA(&huart1, buf,len)!= HAL_OK) //判斷是否發送正常,如果出現異常則進入異常中斷函數
{
Error_Handler();
}
}
/*
*********************************************************************************************************
* 函 數 名: DMA_Usart1_Read
* 功能說明: 串口接收功能函數
* 形 參: Data,len
* 返 回 值: 無
*********************************************************************************************************
*/
void DMA_Usart1_Read(uint8_t *Data,uint8_t len)//串口接收封裝
{
HAL_UART_Receive_DMA(&huart1,Data,len);//重新打開DMA接收
}
while循環
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(recv_end_flag == 1) //接收完成標誌
{
DMA_Usart_Send(rx_buffer, rx_len);
rx_len = 0;//清除計數
recv_end_flag = 0;//清除接收結束標誌位
// for(uint8_t i=0;i<rx_len;i++)
// {
// rx_buffer[i]=0;//清接收緩存
// }
memset(rx_buffer,0,rx_len);
}
HAL_UART_Receive_DMA(&huart1,rx_buffer,BUFFER_SIZE);//重新打開DMA接收
}
stm32f1xx_it.c中
#include "usart.h"
void USART1_IRQHandler(void)
{
uint32_t tmp_flag = 0;
uint32_t temp;
tmp_flag =__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE); //獲取IDLE標誌位
if((tmp_flag != RESET))//idle標誌被置位
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除標誌位
//temp = huart1.Instance->SR; //清除狀態寄存器SR,讀取SR寄存器可以實現清除SR寄存器的功能
//temp = huart1.Instance->DR; //讀取數據寄存器中的數據
//這兩句和上面那句等效
HAL_UART_DMAStop(&huart1); // 停止DMA傳輸,防止
temp = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);// 獲取DMA中未傳輸的數據個數
//temp = hdma_usart1_rx.Instance->NDTR;// 讀取NDTR寄存器,獲取DMA中未傳輸的數據個數,
rx_len = BUFFER_SIZE - temp; //總計數減去未傳輸的數據個數,得到已經接收的數據個數
recv_end_flag = 1; // 接受完成標誌位置1
}
HAL_UART_IRQHandler(&huart1);
}
註釋詳解:
註釋1:
temp = UartHandle.Instance->SR; //清除狀態寄存器SR,讀取SR寄存器可以實現清除SR寄存器的功能
temp = UartHandle.Instance->DR; //讀取數據寄存器中的數據
這兩句被屏蔽的原因是它們實現的功能和這下面串口IDLE狀態寄存器SR標誌位清零的宏定義實現的功能是一樣的:
__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除標誌位
我們可以點擊這個宏定義進去看看,它實現的功能和上面兩句是一樣的:
#define __HAL_UART_CLEAR_IDLEFLAG(HANDLE) __HAL_UART_CLEAR_PEFLAG(HANDLE)
註釋2:
temp = hdma_usart1_rx.Instance->NDTR;// 獲取DMA中未傳輸的數據個數,NDTR寄存器分析見下面
同理, 這句被屏蔽的原因是因爲他和上面的__HAL_DMA_GET_COUNTER的作用也是一樣的,都可以獲取DMA中未傳輸的數據個數
測試正常:
- 此程序可以進行接收不定長的數據幀,不需像RXNE每次接收到一個字節就進一次中斷。
- 同時開啓DMA傳輸速率也會加快
完整例程下載:ZXiaoxuanSTM32-DMA-IDLE
例程2
UART中斷使能函數
__HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__)
功能: 該函數的作用是使能對應的UART中斷
在stm32f1xx_hal_uart.h中被宏定義
可以使能的中斷一共有這些:
我們現在所用到的爲:
* @arg UART_IT_RXNE: Receive Data register not empty interrupt
* @arg UART_IT_IDLE: Idle line detection interrupt
RXNE接收數據中斷
IDLE 接收空閒中斷
本例程功能:
使用RXNE接收數據中斷+IDLE串口接受空閒中斷 實現將接收的數據完整發送到上位機的功能
接收數據的流程:
首先在初始化的時候打開DMA接收,當MCU通過USART接收外部發來的數據時,在進行第①②③步的時候,DMA直接將接收到的數據寫入緩存rx_buffer[100] //接收數據緩存數組,同樣初始化配置的時候設置好配置就可以了。
接收到一個字節數據:
接收到一個字節的數據之後,便會進入RXNE接收數據中斷,
接受完一幀數據之後,便會進入IDLE 接收空閒中斷
在uart.c中添加中斷函數
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); //使能IDLE中斷
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //使能IDLE中斷
//DMA接收函數,此句一定要加,不加接收不到第一次傳進來的實數據,是空的,且此時接收到的數據長度爲緩存器的數據長度
HAL_UART_Receive_DMA(&huart1,rx_buffer,BUFFER_SIZE);
}
其核心代碼就是下面的兩句
第一條語句用來判斷是否接收到1個字節,第二條語句用來判斷是否接收到1幀數據
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)!= RESET) //如果接收到了一個字節的數據
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5); //反轉LED
}
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)!= RESET)//如果接受到了一幀數據
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除標誌位
其餘部分於上方例程1基本相同
- 本次測試是檢測接收到一個字節就反轉LED
經測試,歷程正常: