日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

STM32从零到一,从标准库移植到HAL库,UART串口1以DMA模式收发不定长数据代码详解+常见问题 一文解析

發布時間:2023/12/20 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 STM32从零到一,从标准库移植到HAL库,UART串口1以DMA模式收发不定长数据代码详解+常见问题 一文解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

本文的參考資料

感謝提供標準庫版本的CSDN同學:這兩篇文章至少是我看過的最詳細的標準庫配置DMA版本。而且代碼實測穩定能用。

STM32 | DMA配置和使用如此簡單(超詳細)_。。。| 。。。的博客-CSDN博客_stm32dma配置

STM32 | 串口DMA很難?其實就是如此簡單!(超詳細、附代碼)_。。。| 。。。的博客-CSDN博客

感謝這些同學提供的HAL庫版本參考資料:

STM32 串口實現不定長數據接收(親測有效,附代碼)_不如去睡覺的博客-CSDN博客_stm32串口不定長數據接收

STM32 HAL UART DMA不通的問題解決及注意事項_PegasusYu的博客-CSDN博客_hal_uart_transmit_dma發不出去

HAL庫的DMA發送問題_三境界的博客-CSDN博客_hal庫dma發送

STM32F4 HAL庫 串口 DMA正常模式僅發一次問題?_KK.m的博客-CSDN博客_stm32串口dma只能發送一次

閱讀須知

  • 在閱讀本文之前,建議參照標準庫參考鏈接第一個認真理解DMA串口收發的原理(因為作者的代碼就是從標準庫到HAL庫移植的),本文因為篇幅有限恕不詳述,重點放在介紹HAL庫下DMA的配置使用。如果有條件的同學可以認真學習標準庫參考鏈接第二個先學習如何使用標準庫函數完整實現DMA串口配置,再來閱讀本文會舒服很多。
  • 意法半導體在DMA功能上對HAL庫的封裝并不如標準庫那么簡單明了,效果也比標準庫遜色一些。有的時候遇到數據發不出去或者其他令人抓狂的情況,建議利用好身邊的在線調試器,在出現故障的地方設下斷點反復調試,總結經驗并訂正導致代碼不穩定的地方。
  • 配置環境

    編譯器:Keil uVision 5.29

    調試用平臺:正點原子mini開發板(stm32F103RCT6)

    低代碼框架生成:STM32CubeMX 6.4.0

    關鍵詞:串口+DMA;不定長數據傳輸;中斷

    預備工作

    DMA硬件理論

    我們究竟為啥要用DMA,正常的串口中斷接收不好么?我自己的理解是這樣子的:

  • 正常情況下,像正點原子的官方例程,在沒有DMA的時候,他的不定長數據接收邏輯是在上位機發送的數據最后一定加上一個回車符(所謂的\r\n),通過回車符來判斷是不是到了數據末尾。后面做項目的時候,你會發現許多的數據(比如一些外接設備的浮點數收發)最后是不帶回車符的,或者帶多個回車符,那怎么辦?
  • 輪詢串口內容?我都為CPU感到疲勞。而且輪詢是定時周期的,也就意味著數據的收發周期無法改變(,可能還需要占用一個寶貴的定時器)。那遇到一些會改變數據發送周期的外接設備怎么辦?
  • 使用串口空閑中斷判斷。你說的沒錯,其實DMA實現不定長接收也是通過串口空閑中斷來實現的。那就要說出DMA另一個優勢了:
  • 俗話說:“條條大路通羅馬。”但不是每條道路(數據處理的指令)都是一路暢通到羅馬(CPU)。通向CPU的道路有的十字路口眾多(總線),當然有人在負責仲裁(CPU負責總線仲裁的部分),有的在不定期施工(其他中斷),有的尚未開放。CPU搬運數據,一般來說,要經過十字路口,在等完比自己優先級高的貨物走完(其他外設和內存的讀寫)后的情況下達“你過來呀”的指令,然后從串口開始搬東西,期間還要等一些優先級比自己高的施工完畢,才能將數據搬到你要的位置(數組或者其他)。然后才是你讀取那個位置的數據。
  • 直接存儲器訪問(DMA)提供了另一種思路。開辟一條道路給串口和你要的位置直連,不用經過CPU仲裁,也不用在中斷里面搬運數據,而是讓串口直接“裝”數據到你要的位置,“串口-CPU-中斷-你要的位置”刪掉了兩個環節,也防止了中斷嵌套導致的潛在不穩定性(打斷數據傳輸導致數據丟失,或者程序直接跑飛)。
  • 總結起來,就是節省時間,提高穩定性。十分官方的解釋就是(摘抄自正點原子HAL庫手冊):DMA,全稱為 Direct Memory Access ,即 直接存儲器訪問 。 DMA 傳輸方式無需 CPU 直接控制傳輸,也沒有中斷處理方式那樣保留現場和恢復現場的過程,通過硬件為 RAM 與 I/O 設備開辟一條直接傳送數據的通路, 能 使 CPU 的效率大為提高。
  • 這段內容我個人推薦你閱讀標準庫參考鏈接第一個紅圈部分里面的內容,因為不管是HAL庫還是標準庫,說到底都是操作stm32的寄存器來實現功能,作為DMA知識的引入的話,這篇文章講的已經足夠詳細了。

    DMA的硬件配置主要就是要注意下DMA各個通道與外設的對應關系。我東施效顰,貼幾張別人的圖片,簡略的講解一下。

    (1)DMA1控制器

    從外設(TIMx[x=1、2 、3、4] 、ADC1 、SPI1、SPI/I2S2、I2Cx[x=1、2]和USARTx[x=1、2、3])產生的7個請求,通過邏輯或輸入到DMA1控制器,這意味著同時只能有一個請求有效。參見下圖的DMA1請求映像。
    外設的DMA請求,可以通過設置相應外設寄存器中的控制位,被獨立地開啟或關閉。

    DMA1 請求映像

    各個通道的DMA1請求一覽

    (2)DMA2控制器

    從外設(TIMx[5、6、7、8]、ADC3、SPI/I2S3、UART4、DAC通道1、2和SDIO)產生的5個請求,經邏輯或輸入到DMA2控制器,這意味著同時只能有一個請求有效。參見下圖的DMA2請求映像。
    外設的DMA請求,可以通過設置相應外設寄存器中的DMA控制位,被獨立地開啟或關閉。
    注意: DMA2控制器及相關請求僅存在于大容量產品和互聯型產品中。

    DMA2 請求映像

    各個通道的DMA2請求一覽

    要想讓不同的外設能夠使用DMA方式來處理數據,要根據這個表格使能對應的DMA通道才行。好消息是意法半導體也想到了這一點,所以在下文的配置中你可以驚喜的發現只需要配置外設并在DMA setting中使能DMA就可以了,通道由CubeMX自動設定,不需要再查這張表。

    配置工作

    配置的基本流程其實和標準庫的流程相差無幾。

  • 配置串口各項參數,配置DMA
  • 打開UART串口全局中斷和打開DMA全局中斷
  • ///我是在cubemx配置和點擊代碼生成,在生成后的代碼寫功能實現的分界線///
  • 封裝dma發送功能并加入等待邏輯
  • dma發送完成中斷中實現等待狀態解除功能(別忘了發送完成也算串口空閑哦~)
  • 在串口全局中斷函數里面實現空閑中斷判定,停止DMA接收后實現雙緩存輪流存儲串口的不定長數據
  • 數據處理完之后重新啟用DMA接收
  • 在主函數里面啟用上面提及的所需中斷和第一次DMA接收
  • 燒錄板子驗證效果
  • 過程詳解

    CubeMX生成

    在配置DMA和串口前的準備工作(給從標準庫剛剛遷移到HAL庫的同學看的)

    正常情況下的開發板都配置了一顆高速度的外部晶振,需要你在RCC選項卡手動打開,這和正點原子有一些不同。HSE(高速外部時鐘):石英晶振。這樣子才能讓開發板工作在類似于正點原子所有例程的72MHz的系統時鐘頻率下。

    然后在時鐘樹里面,更改系統時鐘為72MHZ(如果剛才沒改的話,那么極限就是64MHZ),更改時鐘頻率會有提示說是否讓CubeMX決定時鐘路徑,點是即可。

    記得把SYS選項卡中的Debug改成Serial Wire(如果你用的調試器是SWD的話),至少不能說禁用,不然程序寫上去就不能調試和重新寫數據咯~

    定義自己uvision工程的名字。選擇文件存儲的路徑。注意這兩者都要避免有任何中文。

    將IDE改成MDK-ARM,版本改成你用的(最接近的)Keil那個版本。

    串口與DMA配置

    串口在Connectivity(通訊)分支下面。此次以串口1為例子。模式調節為異步(兩線)通訊,參數設置從上到下依次是波特率,字節長度,校驗和,停止位,按照你正常的習慣設置即可。

    切換到DMA配置選項卡,剛開始的時候這里是一片空白,點擊添加并修改DMA請求的類別。

    添加完Rx Tx兩個通道之后,點一下其中一個。我們可以看到這些選項。

    DMA模式(Mode): 分為兩個。兩個通道都選擇Normal正常模式即可,因為我們收發數據都是處理完再準備下一次。

    • DMA_Mode_Normal(正常模式)
      一次DMA數據傳輸完后,停止DMA傳送 ,也就是只傳輸一次
    • DMA_Mode_Circular(循環傳輸模式)
      當傳輸結束時,硬件自動會將傳輸數據量寄存器進行重裝,進行下一輪的數據傳輸。 也就是多次傳輸模式

    自增地址(Increment Address): Peripheral外設和Memory內存只有一個是可以更改的,兩個通道都是這樣。記得勾選上。我們發送串口數據的時候,發送完一個字節,DMA位置的地址交給硬件向前移動就可以了。

    指針遞增模式

    外設和存儲器指針在每次傳輸后可以自動向后遞增或保持常量。當設置為增量模式時,下一個要傳輸的地址將是前一個地址加上增量值

    數據長度(Data Width): 每次操作的數據長度。兩個通道的Peripheral外設和Memory內存都是Byte字節。

    這是兩個通道的DMA請求優先級。建議是可以提高一些(雖然就啟用了兩個DMA,沒啥鳥用)優先級,兩個通道保持一致。

    優先級管理采用軟件+硬件:

    • 軟件:每個通道的優先級可以在DMA_CCRx寄存器中設置,有4個等級
      最高級>高級>中級>低級
    • 硬件:如果2個請求,它們的軟件優先級相同,則較低編號的通道比較高編號的通道有較高的優先權。比如:如果軟件優先級相同,通道2優先于通道4

    配置完之后切換到NVIC設置中。可以看到DMA全局的中斷默認勾選且不可以關閉。我們只要打開串口全局中斷即可。

    切到NVIC選項卡。和標準庫的參考文章一樣,這里我們需要注意一下DMA的中斷優先級是要高于串口中斷的優先級的,所以記得在優先級里面改過來。

    PS1:勾線根據主/副優先級排序,可以更直觀的看到各個中斷的優先級情況。

    PS2:我這個是調了4位主優先級的情況(給FreeRTOS用的),如果是別的中斷分組記得根據自己設置的中斷分組來自己調節順序就好。

    呼,設置完了。可我們的工作才剛剛開始~點擊生成代碼吧。

    DMA發送

    在寫入收發邏輯之前,我們需要一些準備工作。收發部分是完整的從標準庫參考鏈接第二個移植過來的,講解的順序也會按照這個順序來。

    我們主要在stm32f1xx_it.h/c(官方代碼框架的中斷邏輯部分)完成我們的工作。

    首先,我們要先定義三個緩沖區(作全局定義),一個發送緩沖區,兩個接收緩沖區,兩個接收緩沖區是為了做雙緩沖區,目的是為了防止后一次傳輸的數據覆蓋前一次傳輸的數據,并且留出足夠的時間讓CPU處理緩沖區數據。雙緩沖在串口DMA中有著很重要的意義并起著很大的作用!

    在main.c里面,CubeMX已經定義好了UART和DMA的句柄。

    UART_HandleTypeDef huart1;//這個不用我說吧;-) DMA_HandleTypeDef hdma_usart1_tx;//DMA用于串口發送的通道句柄。相比記憶通道編號而言,記憶句柄就方便多了。 DMA_HandleTypeDef hdma_usart1_rx;//DMA接收句柄。

    下面的代碼聲明了我們要用的一些全局變量。記得是在stm32f1xx_it.c的USER code定義區域定義哦~

    /* USER CODE BEGIN 0 */ uint8_t USART1_TX_BUF[MAX_TX_LEN]; // my_printf的發送緩沖,下文詳述其作用。 volatile uint8_t USART1_TX_FLAG = 0; // USART發送標志,啟動發送時置1,加volatile防編譯器優化 uint8_t u1rxbuf[MAX_RX_LEN]; // 數據接收緩沖1 uint8_t u2rxbuf[MAX_RX_LEN]; // 數據接收緩沖2 uint8_t WhichBufIsReady = 0; // 雙緩存指示器。 // 0:u1rxbuf 被DMA占用接收, u2rxbuf 可以讀取. // 0:u2rxbuf 被DMA占用接收, u1rxbuf 可以讀取. uint8_t *p_IsOK = u2rxbuf; // 指針——指向可以讀取的那個緩沖 uint8_t *p_IsToReceive = u1rxbuf; // 指針——指向被占用的那個緩沖 //注意定義的時候要先讓這兩個指針按照WhichBufIsReady的初始狀態先初始化一下。下文詳述為什么要這樣子。 /* USER CODE END 0 */

    你需要在stm32f1xx_it.h補充相關的宏定義,要包含的頭文件,需要extern的變量和我們要用的函數聲明。

    /* USER CODE BEGIN Includes */ #define MAX_RX_LEN (256U) // 一次性可以接受的數據字節長度,你可以自己定義。U是Unsigned的意思。 #define MAX_TX_LEN (512U) // 一次性可以發送的數據字節長度,你可以自己定義。 #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輸出的中斷函數聲明……/* USER CODE BEGIN EFP */void DMA_USART1_Tx_Data(uint8_t *buffer, uint16_t size);//數組發送串口數據void my_printf(char *format, ...);//仿制printf發送串口數據void USART1_TX_Wait(void);//發送等待函數/* USER CODE END EFP */

    需要在main.h補充一下這個:

    /* USER CODE BEGIN Includes */ #include "stm32f1xx_it.h"//包含上面的東西,不然主函數用到*p_IsToReceive會報錯。 /* USER CODE END Includes */

    發送數據上有兩種形式,一種是以數組的形式發送,此情況下要知道數組有效元素的個數;另一種就是類似“printf”的形式,此形式可以基于第一種情況稍作修改。在標準庫里面,我們需要進行這樣子的操作:

    但在HAL里面,意法半導體“貼心”地給我們直接準備了一個函數。(為什么是打了引號,下文會講……)

    HAL_UART_Transmit_DMA(&huart1, buffer, size)

    從左到右分別是串口HAL句柄,接收數據用的數組,一次性要發送的字節數目。

    普通數組發送模式

    在標準庫函數里面,代碼是這樣子的。

    void DMA_USART2_Tx_Data(u8 *buffer, u32 size) {while(USART2_TX_FLAG); //等待上一次發送完成(USART2_TX_FLAG為1即還在發送數據)USART2_TX_FLAG=1; //USART2發送標志(啟動發送)DMA1_Channel7->CMAR = (uint32_t)buffer; //設置要發送的數據地址DMA1_Channel7->CNDTR = size; //設置要發送的字節數目DMA_Cmd(DMA1_Channel7, ENABLE); //開始DMA發送 }

    但在我們這里,畫風突變:

    void DMA_USART1_Tx_Data(uint8_t *buffer, uint16_t size) {USART1_TX_Wait(); // 等待上一次發送完成(USART1_TX_FLAG為1即還在發送數據)USART1_TX_FLAG = 1; // USART1發送標志(啟動發送)HAL_UART_Transmit_DMA(&huart1, buffer, size); // 發送指定長度的數據 }

    有標準庫的同學會問了:為什么不用開關DMA呀?HAL庫幫我們封裝了DMA的使能和失能函數在發送函數里面了,所以這些交給HAL去處理就可以了。回想當時我剛開始移植的時候還傻傻的用上了這兩個函數,結果發現就是畫蛇添足。

    __HAL_DMA_DISABLE(&hdma_usart1_rx);

    __HAL_DMA_ENABLE(&hdma_usart1_rx);

    DMA的開關是簡化了,但這為后面遇到的一個bug埋下了伏筆。

    細心的同學會發現我在發送之前定義了一個等待函數,而且等待的方式是重新定義的,和標準庫函數不一樣。為什么不直接用官方的HAL函數,而是要重新封裝一個發送函數呢?

    在某些場合,你可能需要用到這樣子:

    DMA_USART1_Tx_Data("A!", strlen("A!"))//發送一個 A! 給上位機 DMA_USART1_Tx_Data("B!", strlen("B!"))//發送一個 B! 給上位機

    假如你使用了官方的HAL函數,而不是重新封裝一個帶等待的發送函數,那么你會驚喜地發現你只能發送一個 A! 出去,只要不用其他把這兩個函數隔開,你就甭想發出去 B! 。道理很簡單,意法半導體在封裝HAL的時候同時考慮了此次發送的時候上一次發送有沒有完成的判斷邏輯。如果上一次沒有發送完就再發送一次,這一次的發送請求會被直接忽略掉。

    那么我們的USART1_TX_FLAG就派上用場遼。發送的時候先置個1,發送完了在DMA發送完成中斷里面把他變回0不就可以啦~這樣子就能保證每次發送都是在通道空閑的情況下進行的。stm32f1xx_it.c里面找到DMA通道4的中斷函數:

    /*** @brief This function handles DMA1 channel4 global interrupt.*/ void DMA1_Channel4_IRQHandler(void)//嘿嘿,發送通道對應DMA4,表格還是要好好記一記的 {/* USER CODE BEGIN DMA1_Channel4_IRQn 0 */if (__HAL_DMA_GET_FLAG(&hdma_usart1_tx, DMA_FLAG_TC4) != RESET) //數據發送完成中斷{// __HAL_DMA_CLEAR_FLAG(&hdma_usart1_tx, DMA_FLAG_TC4);// 這一部分其實在 HAL_DMA_IRQHandler(&hdma_usart1_tx) 也完成了。__HAL_UART_CLEAR_IDLEFLAG(&huart1); //清除串口空閑中斷標志位,發送完成那么串口也是空閑態哦~USART1_TX_FLAG = 0; // 重置發送標志位huart1.gState = HAL_UART_STATE_READY;hdma_usart1_tx.State = HAL_DMA_STATE_READY;__HAL_UNLOCK(&hdma_usart1_tx);// 這里疑似是HAL庫函數的bug,具體可以參考我給的鏈接// huart1,hdma_usart1_tx 的狀態要手動復位成READY狀態// 不然發送函數會一直以為通道忙,就不再發送數據了!}/* 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 */ }

    其中把句柄狀態還原為ready那一部分代碼要好好注意一下。HAL庫發送函數在發送之前檢查通道是否忙是通過檢查句柄里面定義的state成員元素來實現的。因為不明原因在發送前state成員元素會被變成busy,但發送后并不會自動回位,需要用戶自己手動操作一下。

    那么為啥我和標準庫版本的等待邏輯是不一樣的呢?其實USART1_TX_Wait() 的定義是這樣子的(記得自己在USER CODE自己加上這段代碼):

    void USART1_TX_Wait(void) {uint16_t delay = 20000;while (USART1_TX_FLAG){delay--;if (delay == 0)return;} }

    如果接觸過郭老師51單片機的同學可能知道,這是等待的超時機制,超時自動退出等待并強制執行。在極端條件測試的時候,如果單純只是等待,每次發送的時候都會有一定的延時,延時不斷的累加,一旦延時嚴重到發送完成還沒來得及復位USART1_TX_FLAG=0就被拉去再發一次數據,程序就會死在while (USART1_TX_FLAG)直接不動彈了。解決的方法要么是上操作系統確保任務調配的順序合理,要么就是設置超時退出機制,當然這是以偶爾的數據傳輸失敗為代價的,但保證了整個程序的穩定性。

    類似printf形式發送數據

    自己定義帶別名的printf最大的好處是可以同時多個串口使用printf方式發送,而不會局限于fput單個定義的printf之中。

    這一段和標準庫函數參考資料的差不多,其實就是直接移植過來的,改了一下標準庫函數而已。記得放在stm32f1xx_it.c的USER code里面。

    void my_printf(char *format, ...) {//VA_LIST 是在C語言中解決變參問題的一組宏,//所在頭文件:#include <stdarg.h>,用于獲取不確定個數的參數。va_list arg_ptr;//實例化可變長參數列表USART1_TX_Wait(); //等待上一次發送完成(USART1_TX_FLAG為1即還在發送數據)va_start(arg_ptr, format);//初始化可變參數列表,設置format為可變長列表的起始點(第一個元素)// MAX_TX_LEN+1可接受的最大字符數(非字節數,UNICODE一個字符兩個字節), 防止產生數組越界vsnprintf((char *)USART1_TX_BUF, MAX_TX_LEN + 1, format, arg_ptr);//從USART1_TX_BUF的首地址開始拼合,拼合format內容;MAX_TX_LEN+1限制長度,防止產生數組越界va_end(arg_ptr); //注意必須關閉DMA_USART1_Tx_Data(USART1_TX_BUF, strlen((const char *)USART1_TX_BUF)); // 記得把buf里面的東西用HAL發出去 }

    DMA接收(帶雙緩沖)

    說到接收數據,大家應該知道定長數據不定長數據吧。實際應用中,如果你使用某傳感器模塊,一般傳感器輸出的數據包長度是固定,這就是定長數據;但使用中,我們也可能接收不定長數據,而且是很大可能,正如前面我介紹發送數據一樣,我們我們輸出的數據長度隨時都會變化,這時候就是不定長數據了。本文限于篇幅只講解不定長數據接收的工作,相信你在讀懂全文之后,也能根據我給的標準庫參考資料移植得到定長數據的接收函數。

    下面一段話幾乎照搬原文:

    介紹如何使用串口DMA接收數據前,先得講解雙緩沖!雙緩沖非常重要,如果接收中斷間隔時間非常短(即發送數據幀的速率很快),MCU來不及處理此次接收到的數據,又產生中斷,這時不能直接開啟DMA通道,否則數據會被覆蓋。有2種方式解決。

  • 在重新開啟接收DMA通道之前,將DMA_Rx_Buf緩沖區里面的數據復制到另外一個數組中,然后再開啟DMA,然后馬上處理復制出來的數據。

  • 建立雙緩沖,設置一個緩沖區標志(用來指示當前處在哪個緩沖區),每完成一次傳輸就切換一下被占用地址和就緒地址指針指向的實際數據緩沖數組,下次傳輸數據就會保存到新的緩沖區中,可以通過自定義緩存區標志來判斷和切換,這樣可以避免緩沖區數據來不及處理就被覆蓋的情況,也能為處理數據留出更多地時間(指到下次傳輸完成)。

  • 話不多說,先上代碼。

    /*** @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庫里面沒有給串口空閑中斷預留專用的回調函數 qaq// __HAL_UART_CLEAR_IDLEFLAG(&huart1);// 這一部分其實在 HAL_UART_IRQHandler(&huart1) 也完成了。HAL_UART_DMAStop(&huart1); // 把DMA接收停掉,防止速度過快導致中斷重入,數據被覆寫。uint32_t data_length = MAX_RX_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);// 數據總長度=極限接收長度-DMA剩余的接收長度if (WhichBufIsReady) //WhichBufIsReady=1{p_IsOK = u2rxbuf; // u2rxbuf 可以讀取,就緒指針指向它。p_IsToReceive = u1rxbuf; // u1rxbuf 作為下一次DMA存儲的緩沖,占用指針指向它。WhichBufIsReady = 0; //切換一下指示器狀態}else //WhichBufIsReady=0{p_IsOK = u1rxbuf; // u1rxbuf 可以讀取,就緒指針指向它。p_IsToReceive = u2rxbuf; // u2rxbuf 作為下一次DMA存儲的緩沖,占用指針指向它。WhichBufIsReady = 1; //切換一下指示器狀態}從下面開始可以處理你接收到的數據啦!舉個栗子,把你收到的數據原原本本的還回去DMA_USART1_Tx_Data(p_IsOK,data_length);//數據打回去,長度就是數據長度///不管是復制也好,放進去隊列也罷,處理你接收到的數據的代碼建議從這里結束memset((uint8_t *)p_IsToReceive, 0, MAX_RX_LEN); // 把接收數據的指針指向的緩沖區清空}/* 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); //數據處理完畢,重新啟動接收/* USER CODE END USART1_IRQn 1 */ }

    就連怎么計算數據長度都是標準庫函數移植的:

    因為接收的是不定長數據,所以必須求出數據長度,這里就用了個很巧妙的方法!DMA通道x傳輸數量寄存器(DMA_CNDTRx)在通道開啟后該寄存器變為只讀,指示剩余的待傳輸字節數目。寄存器內容在每次DMA傳輸后遞減。所以用總緩沖區大小 - 剩下緩沖區大小即可求出使用掉的緩沖區大小,也就是接收數據的長度。注意標準庫函數返回剩余緩沖區大小的函數是DMA_GetCurrDataCounter(),而HAL庫是使用*__HAL_DMA_GET_COUNTER(&hdma_usart1_rx)*罷了,本質都是讀取DMA_CNDTRx。

    這段代碼和標準庫函數最大的區別就是DMA的失能和重新使能不是使用DMA_Cmd(XXX, XXX )而是使用了HAL_UART_DMAStop(&huart1)和HAL_UART_Receive_DMA(&huart1, p_IsToReceive, MAX_RX_LEN) ,HAL的receive函數兼有切換接收緩沖和接收使能的作用,這點要注意。

    細心的同學可能發現我用的指針都是全局變量,而標準庫函數版本是用的一個局部變量,這是因為我們在main() 里面還會用到一次占用指針來初始化函數HAL_UART_Receive_DMA(&huart1, p_IsToReceive, MAX_RX_LEN)。第二個是我們并不需要手動清除IDLE標志位,USART_ReceiveData(USART2) 也不用(其實就是庫函數里面通過讀一次串口來消除標志位,詳見參考資料),因為HAL庫中HAL_UART_IRQHandler(&huart1) 會幫我們處理掉串口的所有標志位。第三個是其實我在DMA通道發送完成中斷中手動清除了IDLE標志位,因為發送完成,串口也是空閑態哦~但這個時候可不是完整收到數據的時候。

    最后在main函數里頭,我們要做最后的初始化工作:

    /* USER CODE BEGIN 2 *///__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);// 這一段其實是有爭議的,有人說手冊講了如果RXNE接收非空中斷沒有使能,那么IDLE中斷無效// 但我試了一下關掉,不會這樣子,所以就沒鳥他// 開啟串口1空閑中斷__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);// 開啟DMA發送通道的發送完成中斷,才能實現封裝發送函數里面的等待功能__HAL_DMA_ENABLE_IT(&hdma_usart1_tx, DMA_IT_TC);// 清除空閑標志位,防止中斷誤入__HAL_UART_CLEAR_IDLEFLAG(&huart1);// 立即就要打開DMA接收// 不然DMA沒有提前準備,第一次接收的數據是讀取不出來的HAL_UART_Receive_DMA(&huart1, p_IsToReceive, MAX_RX_LEN);/* USER CODE END 2 */

    問題解析

    Q1:為什么我的串口壓根就沒有反應?

  • 認真檢查串口中斷,DMA中斷有沒有打開,在main函數里面有沒有加上中斷使能代碼。
  • 檢查一下main函數里面串口的初始化程序的這一部分:
  • MX_GPIO_Init(); MX_DMA_Init(); MX_USART1_UART_Init(); ……

    如果你發現代碼和我的不一樣,DMA初始化放在了UART串口初始化的后面,恭喜你又踩到了HAL的一個bug。DMA必須先于UART初始化才能成功,雖然我也不知道為什么。

    偷懶的方法是在main函數里面直接位置對調一下,一勞永逸的方法是在這里修改一下。

    點擊初始化函數對應的那一行,然后用上移鍵和下移鍵把DMA調到UART前面即可。

    Q2:有時候會出現串口信息發送不全的情況。

    檢查兩個地方:宏定義和等待函數。

    宏定義有問題一般表現為發送的數據末尾丟失。

    #define MAX_RX_LEN (256U) // 接收的最長限制,如果你是接收完之后立馬返回給上位機,這里要看一看,特別是測試的時候喜歡搞巨長無比的字符串的同學。 #define MAX_TX_LEN (512U) // 發送的最長限制,如果發送的數據太長這里就要改大

    等待函數有問題一般表現為發送的數據中間或者開頭丟失,末尾卻好好的。

    void USART1_TX_Wait(void) {uint16_t delay = 20000;//這里的delay可以根據你發送的數據長度動態調節,如果中間斷片建議讓delay數值更大,//給更多的時間進行發送。只要最后系統不會卡死就好。while (USART1_TX_FLAG){delay--;if (delay == 0)return;} }

    后記

  • 如果不是使用自定義的標志位來操作,而是使用DMA自帶的標志位來判斷,效果可能會好一些。
  • 數據吞吐量大的場合要么上操作系統,要么搞DMA降低壓力,要么用網口之類有更成熟接收協議的通訊渠道
  • 參考程序代碼下載鏈接: STM32DMA串口不定長數據收發+FreeRTOS操作系統參考代碼
  • 看完之后能用上,點贊收藏是美德~
  • 總結

    以上是生活随笔為你收集整理的STM32从零到一,从标准库移植到HAL库,UART串口1以DMA模式收发不定长数据代码详解+常见问题 一文解析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。