導言
《STM32F103_LL庫+寄存器學習筆記12.2 - 串口DMA高效收發實戰2:進一步提高串口接收的效率》前陣子完成的LL庫與寄存器版本的代碼,有一個明顯的缺點是不支持多實例化。最近,計劃基于HAL庫系統地梳理一遍bootloader程序開發。在bootloader程序開發項目里,需要用到HAL庫的串口驅動代碼。所以,繼續把代碼優化優化,將多實例化模塊做出來。
bsp_usart_hal 驅動代碼特點總結:
-
支持多實例通用管理
- 設計為USART_Driver_t結構體+接口函數,支持多個USART端口同時獨立驅動,便于單板多串口應用、項目橫向復用、一套代碼多項目。
- 各實例參數(緩沖區、DMA句柄、UART句柄)解耦,代碼維護簡便。
-
DMA收發全流程、雙緩沖高效傳輸
- 接收采用DMA環形模式,支持半傳輸(HT)、全傳輸(TC)、IDLE三類中斷協同搬運,保證任意長度數據不丟包、無粘包。
- 發送采用DMA塊傳輸,自動調度RingBuffer隊列,避免CPU阻塞。
- 收發均使用DMA,極大減輕CPU負擔,適用于高帶寬、高實時性場景。
-
RingBuffer無縫緩存機制
- 內置收發雙向RingBuffer,自動管理協議包拼接、數據緩沖,簡化上層協議處理。
- 支持超大緩沖、溢出自動覆蓋或丟棄舊數據,易于移植第三方協議棧。
-
健壯的錯誤統計與自恢復機制
- 內置DMA傳輸錯誤、串口硬件錯誤等統計計數(errorDMATX/errorDMARX/errorRX),便于產線異常監控、后期故障溯源。
- 檢測到DMA錯誤時,自動執行自恢復:自動重啟DMA、自動清理異常、可定制連續多次異常的告警或自動復位,保障系統長期穩定運行。
-
工程化量產導向
- 代碼結構清晰、注釋標準,適用于量產項目長期維護。
- CubeMX工程直接集成,無需魔改HAL庫代碼。
- 支持多串口、多DMA。
測試效果如下,接收與發送都沒有丟包。
項目地址:
github: https://github.com/q164129345/MCU_Develop/tree/main/stm32f103_hal_usart_dma_ringbuffer
gitee(國內): https://gitee.com/wallace89/MCU_Develop/tree/main/stm32f103_hal_usart_dma_ringbuffer
一、CubeMX
1.1、Clock Configuration
1.2、USART1
波特率115200,數據長度8bits,停止位長度1bit。
使能USART1全局中斷。
USART1_RX的DMA接收模式一定要選擇“Circular”模式,長度選擇Byte(字節) = 8bits。
USART1_RX的DMA發送模式一定要選擇“Normal”模式,長度選擇Byte(字節) = 8bits。
GPIO Setting的設置如上所示。
二、代碼
2.1、bsp_usart_hal.h
/*** @file bsp_usart_hal.h* @brief STM32F1系列 USART + DMA + RingBuffer HAL庫底層驅動接口(多實例、可變緩沖區)* @author Wallace.zhang* @version 2.0.0* @date 2025-05-23*/
#ifndef __BSP_USART_HAL_H
#define __BSP_USART_HAL_H#ifdef __cplusplus
extern "C" {
#endif#include "main.h"
#include "usart.h"
#include "dma.h"
#include "lwrb/lwrb.h"/*** @brief USART驅動結構體(DMA + RingBuffer + 統計)*/
typedef struct
{volatile uint8_t txDMABusy; /**< DMA發送忙標志(1:發送中,0:空閑) */volatile uint64_t rxMsgCount; /**< 統計接收字節總數 */volatile uint64_t txMsgCount; /**< 統計發送字節總數 */volatile uint16_t dmaRxLastPos; /**< DMA接收緩沖區上次處理到的位置 */volatile uint32_t errorDMATX; /**< DMA發送錯誤統計 */volatile uint32_t errorDMARX; /**< DMA接收錯誤統計 */volatile uint32_t errorRX; /**< 串口接收錯誤統計 */DMA_HandleTypeDef *hdma_rx; /**< HAL庫的DMA RX句柄 */DMA_HandleTypeDef *hdma_tx; /**< HAL庫的DMA TX句柄 */UART_HandleTypeDef *huart; /**< HAL庫的UART句柄 *//* RX方向 */uint8_t *rxDMABuffer; /**< DMA接收緩沖區 */uint8_t *rxRBBuffer; /**< 接收RingBuffer緩存區 */uint16_t rxBufSize; /**< DMA接收/RX Ringbuffer緩沖區大小 */lwrb_t rxRB; /**< 接收RingBuffer句柄 *//* TX方向 */uint8_t *txDMABuffer; /**< DMA發送緩沖區 */uint8_t *txRBBuffer; /**< 發送RingBuffer緩存區 */uint16_t txBufSize; /**< DMA發送/TX Ringbuffer緩沖區大小 */lwrb_t txRB; /**< 發送RingBuffer句柄 */} USART_Driver_t;/*** @brief 阻塞方式發送以 NUL 結尾的字符串*/
void USART_SendString_Blocking(USART_Driver_t* const usart, const char* str);
/*** @brief USART發送DMA中斷處理函數*/
void USART_DMA_TX_Interrupt_Handler(USART_Driver_t *usart);
/*** @brief USART接收DMA中斷處理函數*/
void USART_DMA_RX_Interrupt_Handler(USART_Driver_t *usart);
/*** @brief USART空閑接收中斷處理函數(支持DMA+RingBuffer,適用于多實例)*/
void USART_RX_IDLE_Interrupt_Handler(USART_Driver_t *usart);
/*** @brief 將數據寫入指定USART驅動的發送 RingBuffer中*/
uint8_t USART_Put_TxData_To_Ringbuffer(USART_Driver_t *usart, const void* data, uint16_t len);
/*** @brief USART模塊定時任務處理函數,建議主循環1ms周期回調*/
void USART_Module_Run(USART_Driver_t *usart);
/*** @brief 獲取USART接收RingBuffer中的可讀字節數*/
uint32_t USART_Get_The_Existing_Amount_Of_Data(USART_Driver_t *usart);
/*** @brief 從USART接收RingBuffer中讀取一個字節數據*/
uint8_t USART_Take_A_Piece_Of_Data(USART_Driver_t *usart, uint8_t* data);
/*** @brief DMA傳輸錯誤后的自動恢復操作(含錯誤統計)*/
void USART_DMA_Error_Recover(USART_Driver_t *usart, uint8_t dir);
/*** @brief 初始化USART驅動,配置DMA、RingBuffer與中斷*/
void USART_Config(USART_Driver_t *usart,uint8_t *rxDMABuffer, uint8_t *rxRBBuffer, uint16_t rxBufSize,uint8_t *txDMABuffer, uint8_t *txRBBuffer, uint16_t txBufSize);#ifdef __cplusplus
}
#endif#endif /* __BSP_USART_HAL_H */
2.2、bsp_usart_hal.c
/*** @file bsp_usart_hal.c* @brief STM32F1系列 USART + DMA + RingBuffer HAL庫底層驅動實現(多實例、可變緩沖區)* @author Wallace.zhang* @version 2.0.0* @date 2025-05-23*/#include "bsp_usart_hal.h"/*** @brief 阻塞方式發送以NUL結尾字符串(調試用,非DMA)* @param usart 指向USART驅動結構體的指針* @param str 指向以'\0'結尾的字符串* @note 通過HAL庫API逐字節發送,底層會輪詢TXE位(USART_SR.TXE)。* @retval 無*/
void USART_SendString_Blocking(USART_Driver_t* usart, const char* str)
{if (!usart || !str) return;HAL_UART_Transmit(usart->huart, (uint8_t*)str, strlen(str), 1000);
}/*** @brief 配置并啟動USART的DMA接收(環形模式)* @param usart 指向USART驅動結構體的指針* @note 必須保證huart、hdma_rx已通過CubeMX正確初始化* - 調用本函數會停止原有DMA,然后重新配置DMA并啟動環形接收* - 使能USART的IDLE中斷,實現突發/不定長幀高效處理* @retval 無*/
static void USART_Received_DMA_Configure(USART_Driver_t *usart)
{if (!usart) return;HAL_DMA_Abort(usart->hdma_rx); //! 先關閉HAL_UART_Receive_DMA(usart->huart, usart->rxDMABuffer, usart->rxBufSize); //! 啟動環形DMA__HAL_UART_ENABLE_IT(usart->huart, UART_IT_IDLE); //! 使能IDLE中斷usart->dmaRxLastPos = 0;
}/*** @brief 啟動DMA方式串口發送* @param usart 指向USART驅動結構體的指針* @param data 指向待發送的數據緩沖區* @param len 待發送的數據字節數* @note 發送前需確保txDMABusy為0,否則應等待前一幀發送完成* 啟動后DMA自動填充USART_DR寄存器,實現高效異步發送* @retval 無*/
static void USART_SendString_DMA(USART_Driver_t *usart, uint8_t *data, uint16_t len)
{if (!usart || !data || len == 0 || len > usart->txBufSize) return;while (usart->txDMABusy); // 等待DMA空閑usart->txDMABusy = 1;HAL_UART_Transmit_DMA(usart->huart, data, len);
}/*** @brief 寫入數據到接收RingBuffer* @param usart 指向USART驅動結構體的指針* @param data 指向要寫入的數據緩沖區* @param len 要寫入的數據長度(單位:字節)* @retval 0 數據成功寫入,無數據丟棄* @retval 1 ringbuffer剩余空間不足,丟棄部分舊數據以容納新數據* @retval 2 數據長度超過ringbuffer總容量,僅保留新數據尾部(全部舊數據被清空)* @retval 3 輸入數據指針為空* @note* - 本函數通過lwrb庫操作ringbuffer。* - 當len > ringbuffer容量時,強行截斷,僅保留最新usart->rxRBBuffer字節。* - 若空間不足,自動調用lwrb_skip()丟棄部分舊數據。*/
static uint8_t Put_Data_Into_Ringbuffer(USART_Driver_t *usart, const void *data, uint16_t len)
{//! 檢查輸入指針是否合法if (!usart || !data) return 3;lwrb_t *rb = &usart->rxRB;uint16_t rb_size = usart->rxBufSize;//! 獲取當前RingBuffer剩余空間lwrb_sz_t free_space = lwrb_get_free(rb);//! 分三種情況處理:長度小于、等于、大于RingBuffer容量uint8_t ret = 0;if (len < rb_size) {//! 數據小于RingBuffer容量if (len <= free_space) {//! 空間充足,直接寫入lwrb_write(rb, data, len);} else {//! 空間不足,需丟棄部分舊數據lwrb_sz_t used = lwrb_get_full(rb);lwrb_sz_t skip_len = len - free_space;if (skip_len > used) {skip_len = used;}lwrb_skip(rb, skip_len); //! 跳過(丟棄)舊數據lwrb_write(rb, data, len);ret = 1;}} else if (len == rb_size) { //! 數據剛好等于RingBuffer容量if (free_space < rb_size) {lwrb_reset(rb); //! 空間不足,重置RingBufferret = 1;}lwrb_write(rb, data, len);} else { //! 數據超過RingBuffer容量,僅保留最后rb_size字節const uint8_t *byte_ptr = (const uint8_t *)data;data = (const void *)(byte_ptr + (len - rb_size));lwrb_reset(rb);lwrb_write(rb, data, rb_size);ret = 2;}return ret;
}/*** @brief 從DMA環形緩沖區搬運新收到的數據到RingBuffer(支持環繞)* @param usart 指向USART驅動結構體的指針* @note 支持IDLE、DMA HT/TC等多中斷共同調用* - 本函數計算DMA環形緩沖區的新數據,并搬運到RingBuffer* - 支持一次性或分段搬運(緩沖區環繞時自動分兩段處理)* @retval 無*/
static void USART_DMA_RX_Copy(USART_Driver_t *usart)
{uint16_t bufsize = usart->rxBufSize;uint16_t curr_pos = bufsize - __HAL_DMA_GET_COUNTER(usart->hdma_rx);uint16_t last_pos = usart->dmaRxLastPos;if (curr_pos != last_pos) {if (curr_pos > last_pos) {//! 普通情況,未環繞Put_Data_Into_Ringbuffer(usart, usart->rxDMABuffer + last_pos, curr_pos - last_pos);usart->rxMsgCount += (curr_pos - last_pos);} else {//! 環繞,分兩段處理Put_Data_Into_Ringbuffer(usart, usart->rxDMABuffer + last_pos, bufsize - last_pos);Put_Data_Into_Ringbuffer(usart, usart->rxDMABuffer, curr_pos);usart->rxMsgCount += (bufsize - last_pos) + curr_pos;}usart->dmaRxLastPos = curr_pos;}
}/*** @brief DMA接收搬運環形Buffer(推薦在IDLE、DMA中斷等調用)* @param usart 指向USART驅動結構體的指針* @retval 無*/
void USART_DMA_RX_Interrupt_Handler(USART_Driver_t *usart)
{if (!usart) return;USART_DMA_RX_Copy(usart);
}/*** @brief 串口IDLE中斷處理(需在USARTx_IRQHandler中調用)* @param usart 指向USART驅動結構體的指針* @note 檢查并清除IDLE標志,及時觸發DMA搬運* @retval 無*/
void USART_RX_IDLE_Interrupt_Handler(USART_Driver_t *usart)
{if (!usart) return;if (__HAL_UART_GET_FLAG(usart->huart, UART_FLAG_IDLE)) {__HAL_UART_CLEAR_IDLEFLAG(usart->huart);USART_DMA_RX_Copy(usart);}
}/*** @brief DMA發送完成回調(由用戶在HAL庫TxCpltCallback中調用)* @param usart 指向USART驅動結構體的指針* @note 一定要調用,否則無法再次DMA發送* @retval 無*/
void USART_DMA_TX_Interrupt_Handler(USART_Driver_t *usart)
{if (!usart) return;usart->txDMABusy = 0;
}/*** @brief 將數據寫入指定USART驅動的發送 RingBuffer 中* @param usart 指向USART驅動結構體的指針* @param data 指向要寫入的數據緩沖區* @param len 要寫入的數據長度(字節)* @retval 0 數據成功寫入,無數據丟棄* @retval 1 ringbuffer 空間不足,丟棄部分舊數據以容納新數據* @retval 2 數據長度超過 ringbuffer 總容量,僅保留最新 TX_BUFFER_SIZE 字節* @retval 3 輸入數據指針為空* @note* - 使用 lwrb 庫操作發送 RingBuffer(usart->txRB)。* - 若 len > ringbuffer 容量,會自動截斷,僅保留最新的數據。* - 若空間不足,將調用 lwrb_skip() 丟棄部分舊數據。*/
uint8_t USART_Put_TxData_To_Ringbuffer(USART_Driver_t *usart, const void* data, uint16_t len)
{if (!usart || !data) return 3; //! 檢查輸入數據指針有效性lwrb_t *rb = &usart->txRB;uint16_t capacity = usart->txBufSize;lwrb_sz_t freeSpace = lwrb_get_free(rb);uint8_t ret = 0;//! 情況1:數據長度小于ringbuffer容量if (len < capacity) {if (len <= freeSpace) {lwrb_write(rb, data, len); //! 剩余空間充足,直接寫入} else {//! 空間不足,需丟棄部分舊數據lwrb_sz_t used = lwrb_get_full(rb);lwrb_sz_t skip_len = len - freeSpace;if (skip_len > used) skip_len = used;lwrb_skip(rb, skip_len);lwrb_write(rb, data, len);ret = 1;}} else if (len == capacity) { //! 情況2:數據長度等于ringbuffer容量if (freeSpace < capacity) { //! 如果ringbuffer已有數據lwrb_reset(rb);ret = 1;}lwrb_write(rb, data, len);} else { //! 情況3:數據長度大于ringbuffer容量,僅保留最后 capacity 字節const uint8_t *ptr = (const uint8_t*)data + (len - capacity);lwrb_reset(rb);lwrb_write(rb, ptr, capacity);ret = 2;}return ret;
}/*** @brief 初始化USART驅動,配置DMA、RingBuffer與中斷* @param usart 指向USART驅動結構體的指針* @param rxDMABuffer DMA接收緩沖區指針* @param rxRBBuffer 接收RingBuffer緩沖區指針* @param rxBufSize 接收緩沖區大小* @param txDMABuffer DMA發送緩沖區指針* @param txRBBuffer 發送RingBuffer緩沖區指針* @param txBufSize 發送緩沖區大小* @retval 無* @note 需先通過CubeMX完成串口、DMA相關硬件配置和句柄賦值*/
void USART_Config(USART_Driver_t *usart,uint8_t *rxDMABuffer, uint8_t *rxRBBuffer, uint16_t rxBufSize,uint8_t *txDMABuffer, uint8_t *txRBBuffer, uint16_t txBufSize)
{if (!usart) return;usart->rxDMABuffer = rxDMABuffer;usart->rxRBBuffer = rxRBBuffer;usart->rxBufSize = rxBufSize;lwrb_init(&usart->rxRB, usart->rxRBBuffer, usart->rxBufSize);usart->txDMABuffer = txDMABuffer;usart->txRBBuffer = txRBBuffer;usart->txBufSize = txBufSize;lwrb_init(&usart->txRB, usart->txRBBuffer, usart->txBufSize);USART_Received_DMA_Configure(usart); // 初始化DMA RXusart->txDMABusy = 0;usart->dmaRxLastPos = 0;usart->rxMsgCount = 0;usart->txMsgCount = 0;usart->errorDMATX = 0;usart->errorDMARX = 0;usart->errorRX = 0;
}/*** @brief USART模塊主循環調度函數(DMA + RingBuffer高效收發)* @param usart 指向USART驅動結構體的指針* @note 建議主循環定時(如1ms)調用* - 檢查發送RingBuffer是否有待發送數據,且DMA當前空閑* - 若條件滿足,從發送RingBuffer讀取一段數據到DMA發送緩沖區,并通過DMA啟動異步發送* - 自動維護已發送數據統計* @retval 無*/
void USART_Module_Run(USART_Driver_t *usart)
{if (!usart) return;uint16_t available = lwrb_get_full(&usart->txRB);if (available && usart->txDMABusy == 0) {uint16_t len = (available > usart->txBufSize) ? usart->txBufSize : available;lwrb_read(&usart->txRB, usart->txDMABuffer, len);usart->txMsgCount += len;USART_SendString_DMA(usart, usart->txDMABuffer, len);}
}/*** @brief 獲取USART接收RingBuffer中的可讀字節數* @param usart 指向USART驅動結構體的指針* @retval uint32_t 可讀取的數據字節數* @note 通常在主循環或數據解析前調用,用于判斷是否需要讀取數據。*/
uint32_t USART_Get_The_Existing_Amount_Of_Data(USART_Driver_t *usart)
{if (!usart) return 0;return lwrb_get_full(&usart->rxRB);
}/*** @brief 從USART接收RingBuffer中讀取一個字節數據* @param usart 指向USART驅動結構體的指針* @param data 指向存放讀取結果的緩沖區指針* @retval 1 讀取成功,有新數據存入 *data* @retval 0 讀取失敗(無數據或data為NULL)* @note 本函數不會阻塞,無數據時直接返回0。*/
uint8_t USART_Take_A_Piece_Of_Data(USART_Driver_t *usart, uint8_t* data)
{if (!usart || !data) return 0;return lwrb_read(&usart->rxRB, data, 1);
}/*** @brief DMA傳輸錯誤后的自動恢復操作(含錯誤統計)* @param usart 指向USART驅動結構體* @param dir 方向:0=RX, 1=TX* @note 檢測到DMA傳輸錯誤(TE)時調用,自動進行統計并恢復* RX方向會自動重啟DMA,TX方向建議等待主循環調度新發送* @retval 無*/
void USART_DMA_Error_Recover(USART_Driver_t *usart, uint8_t dir)
{if (!usart) return;if (dir == 0) { //! RX方向usart->errorDMARX++; //! DMA接收錯誤計數 */HAL_DMA_Abort(usart->hdma_rx);HAL_UART_Receive_DMA(usart->huart, usart->rxDMABuffer, usart->rxBufSize);//! 可以加入極端情況下的USART復位等} else { //! TX方向usart->errorDMATX++; //! DMA發送錯誤計數 */HAL_DMA_Abort(usart->hdma_tx);//! 一般等待主循環觸發新的DMA發送}//! 可插入報警、日志
}
2.3、stm32f1xx_it.c
2.4、main.c
緩存分別是DMA緩存與ringbuffer緩存。
2.5、編譯代碼、下載代碼
三、測試代碼
如上所示,從成員rxMsgCount與txMsgCount看到,數據在正常收發。errorDMATX、errorDMARX、errorRX一直保持0,證明沒有發生錯誤。