STM32从零到一,从标准库移植到HAL库,UART串口1以DMA模式收发不定长数据代码详解+常见问题 一文解析
前言
本文的參考資料
感謝提供標(biāo)準(zhǔn)庫版本的CSDN同學(xué):這兩篇文章至少是我看過的最詳細(xì)的標(biāo)準(zhǔn)庫配置DMA版本。而且代碼實(shí)測(cè)穩(wěn)定能用。
STM32 | DMA配置和使用如此簡(jiǎn)單(超詳細(xì))_。。。| 。。。的博客-CSDN博客_stm32dma配置
STM32 | 串口DMA很難?其實(shí)就是如此簡(jiǎn)單!(超詳細(xì)、附代碼)_。。。| 。。。的博客-CSDN博客
感謝這些同學(xué)提供的HAL庫版本參考資料:
STM32 串口實(shí)現(xiàn)不定長數(shù)據(jù)接收(親測(cè)有效,附代碼)_不如去睡覺的博客-CSDN博客_stm32串口不定長數(shù)據(jù)接收
STM32 HAL UART DMA不通的問題解決及注意事項(xiàng)_PegasusYu的博客-CSDN博客_hal_uart_transmit_dma發(fā)不出去
HAL庫的DMA發(fā)送問題_三境界的博客-CSDN博客_hal庫dma發(fā)送
STM32F4 HAL庫 串口 DMA正常模式僅發(fā)一次問題?_KK.m的博客-CSDN博客_stm32串口dma只能發(fā)送一次
閱讀須知
配置環(huán)境
編譯器:Keil uVision 5.29
調(diào)試用平臺(tái):正點(diǎn)原子mini開發(fā)板(stm32F103RCT6)
低代碼框架生成:STM32CubeMX 6.4.0
關(guān)鍵詞:串口+DMA;不定長數(shù)據(jù)傳輸;中斷
預(yù)備工作
DMA硬件理論
我們究竟為啥要用DMA,正常的串口中斷接收不好么?我自己的理解是這樣子的:
這段內(nèi)容我個(gè)人推薦你閱讀標(biāo)準(zhǔn)庫參考鏈接第一個(gè)紅圈部分里面的內(nèi)容,因?yàn)椴还苁荋AL庫還是標(biāo)準(zhǔn)庫,說到底都是操作stm32的寄存器來實(shí)現(xiàn)功能,作為DMA知識(shí)的引入的話,這篇文章講的已經(jīng)足夠詳細(xì)了。
DMA的硬件配置主要就是要注意下DMA各個(gè)通道與外設(shè)的對(duì)應(yīng)關(guān)系。我東施效顰,貼幾張別人的圖片,簡(jiǎn)略的講解一下。
(1)DMA1控制器
從外設(shè)(TIMx[x=1、2 、3、4] 、ADC1 、SPI1、SPI/I2S2、I2Cx[x=1、2]和USARTx[x=1、2、3])產(chǎn)生的7個(gè)請(qǐng)求,通過邏輯或輸入到DMA1控制器,這意味著同時(shí)只能有一個(gè)請(qǐng)求有效。參見下圖的DMA1請(qǐng)求映像。
外設(shè)的DMA請(qǐng)求,可以通過設(shè)置相應(yīng)外設(shè)寄存器中的控制位,被獨(dú)立地開啟或關(guān)閉。
DMA1 請(qǐng)求映像
各個(gè)通道的DMA1請(qǐng)求一覽
(2)DMA2控制器
從外設(shè)(TIMx[5、6、7、8]、ADC3、SPI/I2S3、UART4、DAC通道1、2和SDIO)產(chǎn)生的5個(gè)請(qǐng)求,經(jīng)邏輯或輸入到DMA2控制器,這意味著同時(shí)只能有一個(gè)請(qǐng)求有效。參見下圖的DMA2請(qǐng)求映像。
外設(shè)的DMA請(qǐng)求,可以通過設(shè)置相應(yīng)外設(shè)寄存器中的DMA控制位,被獨(dú)立地開啟或關(guān)閉。
注意: DMA2控制器及相關(guān)請(qǐng)求僅存在于大容量產(chǎn)品和互聯(lián)型產(chǎn)品中。
DMA2 請(qǐng)求映像
各個(gè)通道的DMA2請(qǐng)求一覽
要想讓不同的外設(shè)能夠使用DMA方式來處理數(shù)據(jù),要根據(jù)這個(gè)表格使能對(duì)應(yīng)的DMA通道才行。好消息是意法半導(dǎo)體也想到了這一點(diǎn),所以在下文的配置中你可以驚喜的發(fā)現(xiàn)只需要配置外設(shè)并在DMA setting中使能DMA就可以了,通道由CubeMX自動(dòng)設(shè)定,不需要再查這張表。
配置工作
配置的基本流程其實(shí)和標(biāo)準(zhǔn)庫的流程相差無幾。
過程詳解
CubeMX生成
在配置DMA和串口前的準(zhǔn)備工作(給從標(biāo)準(zhǔn)庫剛剛遷移到HAL庫的同學(xué)看的)
正常情況下的開發(fā)板都配置了一顆高速度的外部晶振,需要你在RCC選項(xiàng)卡手動(dòng)打開,這和正點(diǎn)原子有一些不同。HSE(高速外部時(shí)鐘):石英晶振。這樣子才能讓開發(fā)板工作在類似于正點(diǎn)原子所有例程的72MHz的系統(tǒng)時(shí)鐘頻率下。
然后在時(shí)鐘樹里面,更改系統(tǒng)時(shí)鐘為72MHZ(如果剛才沒改的話,那么極限就是64MHZ),更改時(shí)鐘頻率會(huì)有提示說是否讓CubeMX決定時(shí)鐘路徑,點(diǎn)是即可。
記得把SYS選項(xiàng)卡中的Debug改成Serial Wire(如果你用的調(diào)試器是SWD的話),至少不能說禁用,不然程序?qū)懮先ゾ筒荒苷{(diào)試和重新寫數(shù)據(jù)咯~
定義自己uvision工程的名字。選擇文件存儲(chǔ)的路徑。注意這兩者都要避免有任何中文。
將IDE改成MDK-ARM,版本改成你用的(最接近的)Keil那個(gè)版本。
串口與DMA配置
串口在Connectivity(通訊)分支下面。此次以串口1為例子。模式調(diào)節(jié)為異步(兩線)通訊,參數(shù)設(shè)置從上到下依次是波特率,字節(jié)長度,校驗(yàn)和,停止位,按照你正常的習(xí)慣設(shè)置即可。
切換到DMA配置選項(xiàng)卡,剛開始的時(shí)候這里是一片空白,點(diǎn)擊添加并修改DMA請(qǐng)求的類別。
添加完Rx Tx兩個(gè)通道之后,點(diǎn)一下其中一個(gè)。我們可以看到這些選項(xiàng)。
DMA模式(Mode): 分為兩個(gè)。兩個(gè)通道都選擇Normal正常模式即可,因?yàn)槲覀兪瞻l(fā)數(shù)據(jù)都是處理完再準(zhǔn)備下一次。
- DMA_Mode_Normal(正常模式)
一次DMA數(shù)據(jù)傳輸完后,停止DMA傳送 ,也就是只傳輸一次 - DMA_Mode_Circular(循環(huán)傳輸模式)
當(dāng)傳輸結(jié)束時(shí),硬件自動(dòng)會(huì)將傳輸數(shù)據(jù)量寄存器進(jìn)行重裝,進(jìn)行下一輪的數(shù)據(jù)傳輸。 也就是多次傳輸模式
自增地址(Increment Address): Peripheral外設(shè)和Memory內(nèi)存只有一個(gè)是可以更改的,兩個(gè)通道都是這樣。記得勾選上。我們發(fā)送串口數(shù)據(jù)的時(shí)候,發(fā)送完一個(gè)字節(jié),DMA位置的地址交給硬件向前移動(dòng)就可以了。
指針遞增模式
外設(shè)和存儲(chǔ)器指針在每次傳輸后可以自動(dòng)向后遞增或保持常量。當(dāng)設(shè)置為增量模式時(shí),下一個(gè)要傳輸?shù)牡刂穼⑹乔耙粋€(gè)地址加上增量值
數(shù)據(jù)長度(Data Width): 每次操作的數(shù)據(jù)長度。兩個(gè)通道的Peripheral外設(shè)和Memory內(nèi)存都是Byte字節(jié)。
這是兩個(gè)通道的DMA請(qǐng)求優(yōu)先級(jí)。建議是可以提高一些(雖然就啟用了兩個(gè)DMA,沒啥鳥用)優(yōu)先級(jí),兩個(gè)通道保持一致。
優(yōu)先級(jí)管理采用軟件+硬件:
- 軟件:每個(gè)通道的優(yōu)先級(jí)可以在DMA_CCRx寄存器中設(shè)置,有4個(gè)等級(jí)
最高級(jí)>高級(jí)>中級(jí)>低級(jí) - 硬件:如果2個(gè)請(qǐng)求,它們的軟件優(yōu)先級(jí)相同,則較低編號(hào)的通道比較高編號(hào)的通道有較高的優(yōu)先權(quán)。比如:如果軟件優(yōu)先級(jí)相同,通道2優(yōu)先于通道4
配置完之后切換到NVIC設(shè)置中。可以看到DMA全局的中斷默認(rèn)勾選且不可以關(guān)閉。我們只要打開串口全局中斷即可。
切到NVIC選項(xiàng)卡。和標(biāo)準(zhǔn)庫的參考文章一樣,這里我們需要注意一下DMA的中斷優(yōu)先級(jí)是要高于串口中斷的優(yōu)先級(jí)的,所以記得在優(yōu)先級(jí)里面改過來。
PS1:勾線根據(jù)主/副優(yōu)先級(jí)排序,可以更直觀的看到各個(gè)中斷的優(yōu)先級(jí)情況。
PS2:我這個(gè)是調(diào)了4位主優(yōu)先級(jí)的情況(給FreeRTOS用的),如果是別的中斷分組記得根據(jù)自己設(shè)置的中斷分組來自己調(diào)節(jié)順序就好。
呼,設(shè)置完了。可我們的工作才剛剛開始~點(diǎn)擊生成代碼吧。
DMA發(fā)送
在寫入收發(fā)邏輯之前,我們需要一些準(zhǔn)備工作。收發(fā)部分是完整的從標(biāo)準(zhǔn)庫參考鏈接第二個(gè)移植過來的,講解的順序也會(huì)按照這個(gè)順序來。
我們主要在stm32f1xx_it.h/c(官方代碼框架的中斷邏輯部分)完成我們的工作。
首先,我們要先定義三個(gè)緩沖區(qū)(作全局定義),一個(gè)發(fā)送緩沖區(qū),兩個(gè)接收緩沖區(qū),兩個(gè)接收緩沖區(qū)是為了做雙緩沖區(qū),目的是為了防止后一次傳輸?shù)臄?shù)據(jù)覆蓋前一次傳輸?shù)臄?shù)據(jù),并且留出足夠的時(shí)間讓CPU處理緩沖區(qū)數(shù)據(jù)。雙緩沖在串口DMA中有著很重要的意義并起著很大的作用!
在main.c里面,CubeMX已經(jīng)定義好了UART和DMA的句柄。
UART_HandleTypeDef huart1;//這個(gè)不用我說吧;-) DMA_HandleTypeDef hdma_usart1_tx;//DMA用于串口發(fā)送的通道句柄。相比記憶通道編號(hào)而言,記憶句柄就方便多了。 DMA_HandleTypeDef hdma_usart1_rx;//DMA接收句柄。下面的代碼聲明了我們要用的一些全局變量。記得是在stm32f1xx_it.c的USER code定義區(qū)域定義哦~
/* USER CODE BEGIN 0 */ uint8_t USART1_TX_BUF[MAX_TX_LEN]; // my_printf的發(fā)送緩沖,下文詳述其作用。 volatile uint8_t USART1_TX_FLAG = 0; // USART發(fā)送標(biāo)志,啟動(dòng)發(fā)送時(shí)置1,加volatile防編譯器優(yōu)化 uint8_t u1rxbuf[MAX_RX_LEN]; // 數(shù)據(jù)接收緩沖1 uint8_t u2rxbuf[MAX_RX_LEN]; // 數(shù)據(jù)接收緩沖2 uint8_t WhichBufIsReady = 0; // 雙緩存指示器。 // 0:u1rxbuf 被DMA占用接收, u2rxbuf 可以讀取. // 0:u2rxbuf 被DMA占用接收, u1rxbuf 可以讀取. uint8_t *p_IsOK = u2rxbuf; // 指針——指向可以讀取的那個(gè)緩沖 uint8_t *p_IsToReceive = u1rxbuf; // 指針——指向被占用的那個(gè)緩沖 //注意定義的時(shí)候要先讓這兩個(gè)指針按照WhichBufIsReady的初始狀態(tài)先初始化一下。下文詳述為什么要這樣子。 /* USER CODE END 0 */你需要在stm32f1xx_it.h補(bǔ)充相關(guān)的宏定義,要包含的頭文件,需要extern的變量和我們要用的函數(shù)聲明。
/* USER CODE BEGIN Includes */ #define MAX_RX_LEN (256U) // 一次性可以接受的數(shù)據(jù)字節(jié)長度,你可以自己定義。U是Unsigned的意思。 #define MAX_TX_LEN (512U) // 一次性可以發(fā)送的數(shù)據(jù)字節(jié)長度,你可以自己定義。 #include "stdio.h" #include "string.h" #include <stdarg.h> //包含仿printf需要的頭文件/* USER CODE END Includes *//* Exported types ------------------------------------------------------------*//* USER CODE BEGIN ET *//* USER CODE END ET *//* Exported constants --------------------------------------------------------*//* USER CODE BEGIN EC */extern uint8_t *p_IsOK;extern uint8_t *p_IsToReceive;/* USER CODE END EC *//* Exported macro ------------------------------------------------------------*//* USER CODE BEGIN EM *//* USER CODE END EM *//* Exported functions prototypes ---------------------------------------------*///此處省略CubeMX輸出的中斷函數(shù)聲明……/* USER CODE BEGIN EFP */void DMA_USART1_Tx_Data(uint8_t *buffer, uint16_t size);//數(shù)組發(fā)送串口數(shù)據(jù)void my_printf(char *format, ...);//仿制printf發(fā)送串口數(shù)據(jù)void USART1_TX_Wait(void);//發(fā)送等待函數(shù)/* USER CODE END EFP */需要在main.h補(bǔ)充一下這個(gè):
/* USER CODE BEGIN Includes */ #include "stm32f1xx_it.h"//包含上面的東西,不然主函數(shù)用到*p_IsToReceive會(huì)報(bào)錯(cuò)。 /* USER CODE END Includes */發(fā)送數(shù)據(jù)上有兩種形式,一種是以數(shù)組的形式發(fā)送,此情況下要知道數(shù)組有效元素的個(gè)數(shù);另一種就是類似“printf”的形式,此形式可以基于第一種情況稍作修改。在標(biāo)準(zhǔn)庫里面,我們需要進(jìn)行這樣子的操作:
但在HAL里面,意法半導(dǎo)體“貼心”地給我們直接準(zhǔn)備了一個(gè)函數(shù)。(為什么是打了引號(hào),下文會(huì)講……)
HAL_UART_Transmit_DMA(&huart1, buffer, size)
從左到右分別是串口HAL句柄,接收數(shù)據(jù)用的數(shù)組,一次性要發(fā)送的字節(jié)數(shù)目。
普通數(shù)組發(fā)送模式
在標(biāo)準(zhǔn)庫函數(shù)里面,代碼是這樣子的。
void DMA_USART2_Tx_Data(u8 *buffer, u32 size) {while(USART2_TX_FLAG); //等待上一次發(fā)送完成(USART2_TX_FLAG為1即還在發(fā)送數(shù)據(jù))USART2_TX_FLAG=1; //USART2發(fā)送標(biāo)志(啟動(dòng)發(fā)送)DMA1_Channel7->CMAR = (uint32_t)buffer; //設(shè)置要發(fā)送的數(shù)據(jù)地址DMA1_Channel7->CNDTR = size; //設(shè)置要發(fā)送的字節(jié)數(shù)目DMA_Cmd(DMA1_Channel7, ENABLE); //開始DMA發(fā)送 }但在我們這里,畫風(fēng)突變:
void DMA_USART1_Tx_Data(uint8_t *buffer, uint16_t size) {USART1_TX_Wait(); // 等待上一次發(fā)送完成(USART1_TX_FLAG為1即還在發(fā)送數(shù)據(jù))USART1_TX_FLAG = 1; // USART1發(fā)送標(biāo)志(啟動(dòng)發(fā)送)HAL_UART_Transmit_DMA(&huart1, buffer, size); // 發(fā)送指定長度的數(shù)據(jù) }有標(biāo)準(zhǔn)庫的同學(xué)會(huì)問了:為什么不用開關(guān)DMA呀?HAL庫幫我們封裝了DMA的使能和失能函數(shù)在發(fā)送函數(shù)里面了,所以這些交給HAL去處理就可以了。回想當(dāng)時(shí)我剛開始移植的時(shí)候還傻傻的用上了這兩個(gè)函數(shù),結(jié)果發(fā)現(xiàn)就是畫蛇添足。
__HAL_DMA_DISABLE(&hdma_usart1_rx);
__HAL_DMA_ENABLE(&hdma_usart1_rx);
DMA的開關(guān)是簡(jiǎn)化了,但這為后面遇到的一個(gè)bug埋下了伏筆。
細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)我在發(fā)送之前定義了一個(gè)等待函數(shù),而且等待的方式是重新定義的,和標(biāo)準(zhǔn)庫函數(shù)不一樣。為什么不直接用官方的HAL函數(shù),而是要重新封裝一個(gè)發(fā)送函數(shù)呢?
在某些場(chǎng)合,你可能需要用到這樣子:
DMA_USART1_Tx_Data("A!", strlen("A!"))//發(fā)送一個(gè) A! 給上位機(jī) DMA_USART1_Tx_Data("B!", strlen("B!"))//發(fā)送一個(gè) B! 給上位機(jī)假如你使用了官方的HAL函數(shù),而不是重新封裝一個(gè)帶等待的發(fā)送函數(shù),那么你會(huì)驚喜地發(fā)現(xiàn)你只能發(fā)送一個(gè) A! 出去,只要不用其他把這兩個(gè)函數(shù)隔開,你就甭想發(fā)出去 B! 。道理很簡(jiǎn)單,意法半導(dǎo)體在封裝HAL的時(shí)候同時(shí)考慮了此次發(fā)送的時(shí)候上一次發(fā)送有沒有完成的判斷邏輯。如果上一次沒有發(fā)送完就再發(fā)送一次,這一次的發(fā)送請(qǐng)求會(huì)被直接忽略掉。
那么我們的USART1_TX_FLAG就派上用場(chǎng)遼。發(fā)送的時(shí)候先置個(gè)1,發(fā)送完了在DMA發(fā)送完成中斷里面把他變回0不就可以啦~這樣子就能保證每次發(fā)送都是在通道空閑的情況下進(jìn)行的。stm32f1xx_it.c里面找到DMA通道4的中斷函數(shù):
/*** @brief This function handles DMA1 channel4 global interrupt.*/ void DMA1_Channel4_IRQHandler(void)//嘿嘿,發(fā)送通道對(duì)應(yīng)DMA4,表格還是要好好記一記的 {/* USER CODE BEGIN DMA1_Channel4_IRQn 0 */if (__HAL_DMA_GET_FLAG(&hdma_usart1_tx, DMA_FLAG_TC4) != RESET) //數(shù)據(jù)發(fā)送完成中斷{// __HAL_DMA_CLEAR_FLAG(&hdma_usart1_tx, DMA_FLAG_TC4);// 這一部分其實(shí)在 HAL_DMA_IRQHandler(&hdma_usart1_tx) 也完成了。__HAL_UART_CLEAR_IDLEFLAG(&huart1); //清除串口空閑中斷標(biāo)志位,發(fā)送完成那么串口也是空閑態(tài)哦~USART1_TX_FLAG = 0; // 重置發(fā)送標(biāo)志位huart1.gState = HAL_UART_STATE_READY;hdma_usart1_tx.State = HAL_DMA_STATE_READY;__HAL_UNLOCK(&hdma_usart1_tx);// 這里疑似是HAL庫函數(shù)的bug,具體可以參考我給的鏈接// huart1,hdma_usart1_tx 的狀態(tài)要手動(dòng)復(fù)位成READY狀態(tài)// 不然發(fā)送函數(shù)會(huì)一直以為通道忙,就不再發(fā)送數(shù)據(jù)了!}/* USER CODE END DMA1_Channel4_IRQn 0 */HAL_DMA_IRQHandler(&hdma_usart1_tx);/* USER CODE BEGIN DMA1_Channel4_IRQn 1 *//* USER CODE END DMA1_Channel4_IRQn 1 */ }其中把句柄狀態(tài)還原為ready那一部分代碼要好好注意一下。HAL庫發(fā)送函數(shù)在發(fā)送之前檢查通道是否忙是通過檢查句柄里面定義的state成員元素來實(shí)現(xiàn)的。因?yàn)椴幻髟蛟诎l(fā)送前state成員元素會(huì)被變成busy,但發(fā)送后并不會(huì)自動(dòng)回位,需要用戶自己手動(dòng)操作一下。
那么為啥我和標(biāo)準(zhǔn)庫版本的等待邏輯是不一樣的呢?其實(shí)USART1_TX_Wait() 的定義是這樣子的(記得自己在USER CODE自己加上這段代碼):
void USART1_TX_Wait(void) {uint16_t delay = 20000;while (USART1_TX_FLAG){delay--;if (delay == 0)return;} }如果接觸過郭老師51單片機(jī)的同學(xué)可能知道,這是等待的超時(shí)機(jī)制,超時(shí)自動(dòng)退出等待并強(qiáng)制執(zhí)行。在極端條件測(cè)試的時(shí)候,如果單純只是等待,每次發(fā)送的時(shí)候都會(huì)有一定的延時(shí),延時(shí)不斷的累加,一旦延時(shí)嚴(yán)重到發(fā)送完成還沒來得及復(fù)位USART1_TX_FLAG=0就被拉去再發(fā)一次數(shù)據(jù),程序就會(huì)死在while (USART1_TX_FLAG)直接不動(dòng)彈了。解決的方法要么是上操作系統(tǒng)確保任務(wù)調(diào)配的順序合理,要么就是設(shè)置超時(shí)退出機(jī)制,當(dāng)然這是以偶爾的數(shù)據(jù)傳輸失敗為代價(jià)的,但保證了整個(gè)程序的穩(wěn)定性。
類似printf形式發(fā)送數(shù)據(jù)
自己定義帶別名的printf最大的好處是可以同時(shí)多個(gè)串口使用printf方式發(fā)送,而不會(huì)局限于fput單個(gè)定義的printf之中。
這一段和標(biāo)準(zhǔn)庫函數(shù)參考資料的差不多,其實(shí)就是直接移植過來的,改了一下標(biāo)準(zhǔn)庫函數(shù)而已。記得放在stm32f1xx_it.c的USER code里面。
void my_printf(char *format, ...) {//VA_LIST 是在C語言中解決變參問題的一組宏,//所在頭文件:#include <stdarg.h>,用于獲取不確定個(gè)數(shù)的參數(shù)。va_list arg_ptr;//實(shí)例化可變長參數(shù)列表USART1_TX_Wait(); //等待上一次發(fā)送完成(USART1_TX_FLAG為1即還在發(fā)送數(shù)據(jù))va_start(arg_ptr, format);//初始化可變參數(shù)列表,設(shè)置format為可變長列表的起始點(diǎn)(第一個(gè)元素)// MAX_TX_LEN+1可接受的最大字符數(shù)(非字節(jié)數(shù),UNICODE一個(gè)字符兩個(gè)字節(jié)), 防止產(chǎn)生數(shù)組越界vsnprintf((char *)USART1_TX_BUF, MAX_TX_LEN + 1, format, arg_ptr);//從USART1_TX_BUF的首地址開始拼合,拼合format內(nèi)容;MAX_TX_LEN+1限制長度,防止產(chǎn)生數(shù)組越界va_end(arg_ptr); //注意必須關(guān)閉DMA_USART1_Tx_Data(USART1_TX_BUF, strlen((const char *)USART1_TX_BUF)); // 記得把buf里面的東西用HAL發(fā)出去 }DMA接收(帶雙緩沖)
說到接收數(shù)據(jù),大家應(yīng)該知道定長數(shù)據(jù)和不定長數(shù)據(jù)吧。實(shí)際應(yīng)用中,如果你使用某傳感器模塊,一般傳感器輸出的數(shù)據(jù)包長度是固定,這就是定長數(shù)據(jù);但使用中,我們也可能接收不定長數(shù)據(jù),而且是很大可能,正如前面我介紹發(fā)送數(shù)據(jù)一樣,我們我們輸出的數(shù)據(jù)長度隨時(shí)都會(huì)變化,這時(shí)候就是不定長數(shù)據(jù)了。本文限于篇幅只講解不定長數(shù)據(jù)接收的工作,相信你在讀懂全文之后,也能根據(jù)我給的標(biāo)準(zhǔn)庫參考資料移植得到定長數(shù)據(jù)的接收函數(shù)。
下面一段話幾乎照搬原文:
介紹如何使用串口DMA接收數(shù)據(jù)前,先得講解雙緩沖!雙緩沖非常重要,如果接收中斷間隔時(shí)間非常短(即發(fā)送數(shù)據(jù)幀的速率很快),MCU來不及處理此次接收到的數(shù)據(jù),又產(chǎn)生中斷,這時(shí)不能直接開啟DMA通道,否則數(shù)據(jù)會(huì)被覆蓋。有2種方式解決。
在重新開啟接收DMA通道之前,將DMA_Rx_Buf緩沖區(qū)里面的數(shù)據(jù)復(fù)制到另外一個(gè)數(shù)組中,然后再開啟DMA,然后馬上處理復(fù)制出來的數(shù)據(jù)。
建立雙緩沖,設(shè)置一個(gè)緩沖區(qū)標(biāo)志(用來指示當(dāng)前處在哪個(gè)緩沖區(qū)),每完成一次傳輸就切換一下被占用地址和就緒地址指針指向的實(shí)際數(shù)據(jù)緩沖數(shù)組,下次傳輸數(shù)據(jù)就會(huì)保存到新的緩沖區(qū)中,可以通過自定義緩存區(qū)標(biāo)志來判斷和切換,這樣可以避免緩沖區(qū)數(shù)據(jù)來不及處理就被覆蓋的情況,也能為處理數(shù)據(jù)留出更多地時(shí)間(指到下次傳輸完成)。
話不多說,先上代碼。
/*** @brief This function handles USART1 global interrupt.*/ void USART1_IRQHandler(void) {/* USER CODE BEGIN USART1_IRQn 0 */if (RESET != __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)){ // 我記得好像HAL庫里面沒有給串口空閑中斷預(yù)留專用的回調(diào)函數(shù) qaq// __HAL_UART_CLEAR_IDLEFLAG(&huart1);// 這一部分其實(shí)在 HAL_UART_IRQHandler(&huart1) 也完成了。HAL_UART_DMAStop(&huart1); // 把DMA接收停掉,防止速度過快導(dǎo)致中斷重入,數(shù)據(jù)被覆寫。uint32_t data_length = MAX_RX_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);// 數(shù)據(jù)總長度=極限接收長度-DMA剩余的接收長度if (WhichBufIsReady) //WhichBufIsReady=1{p_IsOK = u2rxbuf; // u2rxbuf 可以讀取,就緒指針指向它。p_IsToReceive = u1rxbuf; // u1rxbuf 作為下一次DMA存儲(chǔ)的緩沖,占用指針指向它。WhichBufIsReady = 0; //切換一下指示器狀態(tài)}else //WhichBufIsReady=0{p_IsOK = u1rxbuf; // u1rxbuf 可以讀取,就緒指針指向它。p_IsToReceive = u2rxbuf; // u2rxbuf 作為下一次DMA存儲(chǔ)的緩沖,占用指針指向它。WhichBufIsReady = 1; //切換一下指示器狀態(tài)}從下面開始可以處理你接收到的數(shù)據(jù)啦!舉個(gè)栗子,把你收到的數(shù)據(jù)原原本本的還回去DMA_USART1_Tx_Data(p_IsOK,data_length);//數(shù)據(jù)打回去,長度就是數(shù)據(jù)長度///不管是復(fù)制也好,放進(jìn)去隊(duì)列也罷,處理你接收到的數(shù)據(jù)的代碼建議從這里結(jié)束memset((uint8_t *)p_IsToReceive, 0, MAX_RX_LEN); // 把接收數(shù)據(jù)的指針指向的緩沖區(qū)清空}/* USER CODE END USART1_IRQn 0 */HAL_UART_IRQHandler(&huart1);/* USER CODE BEGIN USART1_IRQn 1 */HAL_UART_Receive_DMA(&huart1, p_IsToReceive, MAX_RX_LEN); //數(shù)據(jù)處理完畢,重新啟動(dòng)接收/* USER CODE END USART1_IRQn 1 */ }就連怎么計(jì)算數(shù)據(jù)長度都是標(biāo)準(zhǔn)庫函數(shù)移植的:
因?yàn)榻邮盏氖遣欢ㄩL數(shù)據(jù),所以必須求出數(shù)據(jù)長度,這里就用了個(gè)很巧妙的方法!DMA通道x傳輸數(shù)量寄存器(DMA_CNDTRx)在通道開啟后該寄存器變?yōu)橹蛔x,指示剩余的待傳輸字節(jié)數(shù)目。寄存器內(nèi)容在每次DMA傳輸后遞減。所以用總緩沖區(qū)大小 - 剩下緩沖區(qū)大小即可求出使用掉的緩沖區(qū)大小,也就是接收數(shù)據(jù)的長度。注意標(biāo)準(zhǔn)庫函數(shù)返回剩余緩沖區(qū)大小的函數(shù)是DMA_GetCurrDataCounter(),而HAL庫是使用*__HAL_DMA_GET_COUNTER(&hdma_usart1_rx)*罷了,本質(zhì)都是讀取DMA_CNDTRx。
這段代碼和標(biāo)準(zhǔn)庫函數(shù)最大的區(qū)別就是DMA的失能和重新使能不是使用DMA_Cmd(XXX, XXX )而是使用了HAL_UART_DMAStop(&huart1)和HAL_UART_Receive_DMA(&huart1, p_IsToReceive, MAX_RX_LEN) ,HAL的receive函數(shù)兼有切換接收緩沖和接收使能的作用,這點(diǎn)要注意。
細(xì)心的同學(xué)可能發(fā)現(xiàn)我用的指針都是全局變量,而標(biāo)準(zhǔn)庫函數(shù)版本是用的一個(gè)局部變量,這是因?yàn)槲覀冊(cè)趍ain() 里面還會(huì)用到一次占用指針來初始化函數(shù)HAL_UART_Receive_DMA(&huart1, p_IsToReceive, MAX_RX_LEN)。第二個(gè)是我們并不需要手動(dòng)清除IDLE標(biāo)志位,USART_ReceiveData(USART2) 也不用(其實(shí)就是庫函數(shù)里面通過讀一次串口來消除標(biāo)志位,詳見參考資料),因?yàn)镠AL庫中HAL_UART_IRQHandler(&huart1) 會(huì)幫我們處理掉串口的所有標(biāo)志位。第三個(gè)是其實(shí)我在DMA通道發(fā)送完成中斷中手動(dòng)清除了IDLE標(biāo)志位,因?yàn)榘l(fā)送完成,串口也是空閑態(tài)哦~但這個(gè)時(shí)候可不是完整收到數(shù)據(jù)的時(shí)候。
最后在main函數(shù)里頭,我們要做最后的初始化工作:
/* USER CODE BEGIN 2 *///__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);// 這一段其實(shí)是有爭(zhēng)議的,有人說手冊(cè)講了如果RXNE接收非空中斷沒有使能,那么IDLE中斷無效// 但我試了一下關(guān)掉,不會(huì)這樣子,所以就沒鳥他// 開啟串口1空閑中斷__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);// 開啟DMA發(fā)送通道的發(fā)送完成中斷,才能實(shí)現(xiàn)封裝發(fā)送函數(shù)里面的等待功能__HAL_DMA_ENABLE_IT(&hdma_usart1_tx, DMA_IT_TC);// 清除空閑標(biāo)志位,防止中斷誤入__HAL_UART_CLEAR_IDLEFLAG(&huart1);// 立即就要打開DMA接收// 不然DMA沒有提前準(zhǔn)備,第一次接收的數(shù)據(jù)是讀取不出來的HAL_UART_Receive_DMA(&huart1, p_IsToReceive, MAX_RX_LEN);/* USER CODE END 2 */問題解析
Q1:為什么我的串口壓根就沒有反應(yīng)?
如果你發(fā)現(xiàn)代碼和我的不一樣,DMA初始化放在了UART串口初始化的后面,恭喜你又踩到了HAL的一個(gè)bug。DMA必須先于UART初始化才能成功,雖然我也不知道為什么。
偷懶的方法是在main函數(shù)里面直接位置對(duì)調(diào)一下,一勞永逸的方法是在這里修改一下。
點(diǎn)擊初始化函數(shù)對(duì)應(yīng)的那一行,然后用上移鍵和下移鍵把DMA調(diào)到UART前面即可。
Q2:有時(shí)候會(huì)出現(xiàn)串口信息發(fā)送不全的情況。
檢查兩個(gè)地方:宏定義和等待函數(shù)。
宏定義有問題一般表現(xiàn)為發(fā)送的數(shù)據(jù)末尾丟失。
#define MAX_RX_LEN (256U) // 接收的最長限制,如果你是接收完之后立馬返回給上位機(jī),這里要看一看,特別是測(cè)試的時(shí)候喜歡搞巨長無比的字符串的同學(xué)。 #define MAX_TX_LEN (512U) // 發(fā)送的最長限制,如果發(fā)送的數(shù)據(jù)太長這里就要改大等待函數(shù)有問題一般表現(xiàn)為發(fā)送的數(shù)據(jù)中間或者開頭丟失,末尾卻好好的。
void USART1_TX_Wait(void) {uint16_t delay = 20000;//這里的delay可以根據(jù)你發(fā)送的數(shù)據(jù)長度動(dòng)態(tài)調(diào)節(jié),如果中間斷片建議讓delay數(shù)值更大,//給更多的時(shí)間進(jìn)行發(fā)送。只要最后系統(tǒng)不會(huì)卡死就好。while (USART1_TX_FLAG){delay--;if (delay == 0)return;} }后記
總結(jié)
以上是生活随笔為你收集整理的STM32从零到一,从标准库移植到HAL库,UART串口1以DMA模式收发不定长数据代码详解+常见问题 一文解析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DataCamp课程 <Tidyvers
- 下一篇: LMD VCL Complete 202