当前位置: 首页 > news >正文

STM32F103 HAL多实例通用USART驱动 - 高效DMA+RingBuffer方案,量产级工程模板

导言


《STM32F103_LL库+寄存器学习笔记12.2 - 串口DMA高效收发实战2:进一步提高串口接收的效率》前阵子完成的LL库与寄存器版本的代码,有一个明显的缺点是不支持多实例化。最近,计划基于HAL库系统地梳理一遍bootloader程序开发。在bootloader程序开发项目里,需要用到HAL库的串口驱动代码。所以,继续把代码优化优化,将多实例化模块做出来。

bsp_usart_hal 驱动代码特点总结:

  1. 支持多实例通用管理

    • 设计为USART_Driver_t结构体+接口函数,支持多个USART端口同时独立驱动,便于单板多串口应用、项目横向复用、一套代码多项目。
    • 各实例参数(缓冲区、DMA句柄、UART句柄)解耦,代码维护简便。
  2. DMA收发全流程、双缓冲高效传输

    • 接收采用DMA环形模式,支持半传输(HT)、全传输(TC)、IDLE三类中断协同搬运,保证任意长度数据不丢包、无粘包。
    • 发送采用DMA块传输,自动调度RingBuffer队列,避免CPU阻塞。
    • 收发均使用DMA,极大减轻CPU负担,适用于高带宽、高实时性场景。
  3. RingBuffer无缝缓存机制

    • 内置收发双向RingBuffer,自动管理协议包拼接、数据缓冲,简化上层协议处理。
    • 支持超大缓冲、溢出自动覆盖或丢弃旧数据,易于移植第三方协议栈。
  4. 健壮的错误统计与自恢复机制

    • 内置DMA传输错误、串口硬件错误等统计计数(errorDMATX/errorDMARX/errorRX),便于产线异常监控、后期故障溯源。
    • 检测到DMA错误时,自动执行自恢复:自动重启DMA、自动清理异常、可定制连续多次异常的告警或自动复位,保障系统长期稳定运行。
  5. 工程化量产导向

    • 代码结构清晰、注释标准,适用于量产项目长期维护。
    • 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,证明没有发生错误。

http://www.xdnf.cn/news/629137.html

相关文章:

  • python训练营第33天
  • Lesson 22 A glass envelope
  • HJ14 字符串排序【牛客网】
  • Spring AI 源码解析:Tool Calling链路调用流程及示例
  • 从法律视角看债务管理:湖北理元理律师事务所的实践探索
  • 【信息系统项目管理师】一文掌握高项常考题型-成本类计算
  • 巡礼中国西极·跨越昆仑天山 | 北斗卫星徽章护航昆仑科考
  • 神经算子项目实战:数据分析、可视化与实现全过程
  • 归一化 超全总结!!
  • leetcode hot100刷题日记——16.全排列
  • 探秘Transformer系列之(34)--- 量化基础
  • 开源轻量级语音合成和语音克隆模型:OuteTTS-1.0-0.6B
  • AWTK嵌入式图形框架开发备忘(二)
  • 【GESP真题解析】第 5 集 GESP 二级 2023 年 3 月编程题 2:百鸡问题
  • 【Python】【电网规划】基于经济与可靠性双目标的混合配电系统规划及可靠性评估
  • ShenNiusModularity项目源码学习(30:ShenNius.Admin.Mvc项目分析-15)
  • 可增添功能的鼠标右键优化工具
  • 【PINN】DeepXDE学习训练营(33)——pinn_forward-fractional_Poisson_1d.py
  • C++:共享指针unique_ptr的理解与应用
  • 每日定投40刀BTC(17)20250511 - 20250524
  • 什么是数据分析
  • Go基础语法与控制结构
  • ROS云课三分钟-破壁篇GCompris-一小部分支持Edu应用列表-2025
  • 部署n8n
  • 海思SVP_NPU开发适配
  • Python训练营---Day35
  • 哈希表原理与双散列实战指南
  • 超时处理机制设计:从TICK到回调
  • 刷leetcode hot100返航版--贪心5/23
  • Python性能优化利器:__slots__的深度解析与避坑指南