上一次我們通過HAL庫的串口中斷回調函數,基本能夠實現簡單的不定長度讀寫收發的功能。這一次用DMA來實現,先了解一下DMA。
DMA 直接存儲器存取用來提供在外設和存儲器之間或者存儲器和存儲器之間的高速數據傳輸。無須CPU的干預,通過DMA數據可以快速地移動。這就節省了CPU的資源來做其他操作。
DMA詳解傳送門:https://blog.csdn.net/Cheatscat/article/details/79476806
CUBEMX中的設置大概如下,其他默認值(記得要開啟串口中斷)
關於DMA串口讀寫收發功能的實現,有兩種可用的方法。
第一種,
如果只是為了實現用DMA的功能,在cubemx中設置好之后,在main函數中添加
uint8_t aTxBuffer[] = "SENDING DATA USING USART1 with DMA\r\n";//(最好寫到全局中) HAL_UART_Transmit_DMA(&huart1,aTxBuffer,sizeof(aTxBuffer));// DMA發生數據
串口調試助手能夠在復位之后直接收到送出的數據。
亦或者使用(一)中普通的串口中斷回調函數的方法使用DMA,使用HAL_UART_Receive_DMA(&huart1,aRxBuffer,sizeof(aRxBuffer)) 放在main函數中進行DMA的接收(等待接收)。在回調函數中使用receive和trainsmit函數互相調用如(一)。但是該方法有個很重要的缺點,就是在DMA的應用中,無法做到跟串口中斷回調那樣子的效果,因為
HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)和HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)這兩個函數有區別。
HAL_UART_Receive_IT這個函數在接收數據的時候可以是一個字一個字的讀取,每讀完一個自動關閉串口接收。
HAL_UART_Receive_DMA則是要讀完一串一幀數據之后才能夠執行相關函數功能,所以DMA只有在接收到期待長度的數據時才觸發中斷。
如果DMA的期待長度為1時,一次性發送不定長數據時會導致接收一個丟失下一個的情況,例如發送123456會接收到135,可能是DMA發送時無法及時接收接下來的數據。
這種解決方法有上位機發送數據時最好是補全至定長后發送,或采用空閑中斷的方式 即使數據長度沒有達到期望,只要一段時間未接收到數據即進入中斷。
第二種,
就是采用DMA空閑中斷方式,對應事件標志為IDLE,檢測到空閑線路時,該位由硬件置 1,生成中斷。
還能夠在中斷函數中讀取DMA中的功能,如獲取已讀和未讀數據的長度。整體流程如下:
- 1、開啟串口DMA接收
- 2、串口收到數據,DMA不斷傳輸數據到存儲buf
- 3、一幀數據發送完畢,串口暫時空閑,觸發串口空閑中斷
- 4、在中斷服務函數中,可以計算剛才收到了多少個字節的數據
- 5、解碼存儲buf,清除標志位,開始下一幀接收
先上手動改過的源碼
uint8_t aTxBuffer[] = "*********SENDING DATA USING USART1 with DMA***********\r\n"; volatile uint8_t rx_len=0; //接收數據長度 volatile uint8_t recv_end_flag=0; //接收完成標記位 uint8_t aRxBuffer1[100]; //接收緩存 char BUFFER_SIZE=100; //不定長數據的最大長度,設置為100
main函數里面:
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); HAL_UART_Receive_DMA(&huart1,aRxBuffer1,BUFFER_SIZE);// 啟動DMA接收
while (1) { if(recv_end_flag ==1) { // printf("rx_len=%d\r\n",rx_len);//打印接收長度 HAL_UART_Transmit(&huart1,aRxBuffer1, rx_len,200);//接收數據打印出來 for(uint8_t i=0;i<rx_len;i++) { aRxBuffer1[i]=0;//清接收緩存 } rx_len=0;//清除計數 recv_end_flag=0;//清除接收結束標志位 } HAL_UART_Receive_DMA(&huart1,aRxBuffer1,BUFFER_SIZE);// 啟動DMA接收 /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ }
stm32f1xx_it.c文件中:
void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ 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); // temp = hdma_usart1_rx.Instance->CNDTR;// 獲取DMA中未傳輸的數據個數,NDTR寄存器分析見下面 rx_len = BUFFER_SIZE - temp; //總計數減去未傳輸的數據個數,得到已經接收的數據個數 recv_end_flag = 1; // 接受完成標志位置1 } /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(&huart1); /* USER CODE BEGIN USART1_IRQn 1 */ /* USER CODE END USART1_IRQn 1 */ }
即可實現功能。
先定義緩存數據的數組,可以更改大小。在聲明變量的時候用到了volatile關鍵字, 主要是因為volatile 關鍵字提醒編譯器定義的變量是易變的,編譯后的程序每次需要存儲或讀取該變量時,會直接從變量地址讀取數據。在中斷或多線程中使用volatile關鍵字可以避免不同優化等級時程序出錯,提高程序的魯棒性。
所以若是在多文件並且經常用到的變量需要用這個關鍵字。在別的.c文件中調用這些變量的時候有兩種方法,一是在相應的.h文件中聲明然后在別的.c文件中添加引用。二則是采用extern關鍵字,全局變量在外部使用時要用到extern關鍵字。
extern volatile uint8_t rx_len; extern volatile uint8_t recv_end_flag; extern uint8_t aRxBuffer1[100]; extern char BUFFER_SIZE;
要用到空閑中斷方式,即要開啟IDLE中斷。
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
再用DMA的串口接收函數啟動DMA接收。
從上位機接收數據后進行一個空閑中斷函數,通過IDLE的標志位進行判斷
void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ 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); // temp = hdma_usart1_rx.Instance->CNDTR;// 獲取DMA中未傳輸的數據個數,NDTR寄存器分析見下面 rx_len = BUFFER_SIZE - temp; //總計數減去未傳輸的數據個數,得到已經接收的數據個數 recv_end_flag = 1; // 接受完成標志位置1 } /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(&huart1); /* USER CODE BEGIN USART1_IRQn 1 */ /* USER CODE END USART1_IRQn 1 */ }
其中CNDTR是未傳輸的數據數量,有些地方是NDTR, CNDTR 寄存器在 DMA 通道結構體中定義了 CNDTR 寄存器,這個不同的芯片HAL庫里面定義的命名有點不同。
寄存器說明如上。所以可以得到已接收數據的長度,這個對於不定長度的讀寫十分的重要。得到接收數據的長度之后,就可以進行操作了。在while中。
while (1) { if(recv_end_flag ==1) { // printf("rx_len=%d\r\n",rx_len);//打印接收長度 HAL_UART_Transmit(&huart1,aRxBuffer1, rx_len,200);//接收數據打印出來 for(uint8_t i=0;i<rx_len;i++) { aRxBuffer1[i]=0;//清接收緩存 } rx_len=0;//清除計數 recv_end_flag=0;//清除接收結束標志位 } HAL_UART_Receive_DMA(&huart1,aRxBuffer1,BUFFER_SIZE);// 啟動DMA接收 /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ }
將接受到的數據通過Transmit函數發送出來到上位機。最后重新啟動DMA接收函數。
OK.