STM32串口不定长数据接收:超时解析法+DMA+空闲中断法(附完整代码)

在STM32嵌入式开发中,串口通信是最核心的外设交互方式之一,而不定长数据的可靠接收是开发中的高频痛点:固定长度解析仅适配特定场景,结束符解析受通信协议限制,粘包问题更是容易导致数据解析异常。

本文将分享两种工业级串口接收方案,覆盖从基础到进阶的全场景需求,附完整可运行代码,彻底解决STM32串口不定长数据接收难题:

  • 基础版:超时解析法(中断+超时判断)------ 简单高效,适配中小量数据交互;
  • 进阶版:DMA+空闲中断法 ------ 低CPU占用,适配高频/大数据量数据流。

一、基础方案:超时解析法(中断+超时判断)

1.1 核心原理与适用场景

适用场景

中小量不定长数据交互(如按键指令、短报文、调试指令),对CPU占用要求不高的场景。

核心原理

检测到首个字节数据后,若后续超过设定时间(超时阈值)无新数据传入,则判定整帧数据接收完成,触发缓冲区数据解析。

方案对比
解析方案 核心逻辑 优点 缺点
固定长度解析 约定字符数后触发解析 逻辑最简单 仅适配定长协议,灵活性差
结束符解析 检测\r\n等结束符触发 适配部分通用协议 协议耦合度高,无结束符则失效
超时解析(推荐) 超时无新数据则判定结束 无协议依赖,适配任意不定长数据 CPU需轮询判断超时

1.2 完整代码实现

1.2.1 全局变量声明(需在mydefine.h外部声明)
c 复制代码
#include "stm32f1xx_hal.h"
#include <string.h>

// 串口接收缓冲区(128字节,可根据需求调整)
uint8_t uart_rx_buffer[128] = {0};
// 接收索引(记录当前存储位置)
uint16_t uart_rx_index = 0;
// 接收时间戳(记录最后1字节接收时间)
uint32_t uart_rx_ticks = 0;
// 临时存储单字节接收数据
uint8_t uart_rx_temp;
1.2.2 中断回调函数(单字节接收)

HAL库中HAL_UART_RxCpltCallback是串口接收完成中断的弱回调函数,用户重写后可实现自定义逻辑:

c 复制代码
// 串口接收完成中断回调函数(收到1字节触发)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) 
{
    if (huart->Instance == USART1) // 仅处理USART1
    {
        uart_rx_ticks = uwTick; // 更新最后接收时间戳(uwTick为HAL库系统滴答值,ms级)
        
        // 缓冲区溢出保护:避免索引超出数组范围
        if(uart_rx_index < sizeof(uart_rx_buffer) - 1)
        {
            uart_rx_buffer[uart_rx_index] = uart_rx_temp; // 存储当前接收字节
            uart_rx_index++; // 索引自增
        }
        
        // 重新启用接收中断,等待下1字节(关键:中断需手动重启)
        HAL_UART_Receive_IT(&huart1, &uart_rx_temp, 1);
    }
}
1.2.3 串口初始化(启用首次接收中断)

CubeMX生成基础初始化代码后,需手动添加"首次接收中断启用"逻辑:

c 复制代码
UART_HandleTypeDef huart1; // CubeMX自动生成的串口句柄

void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler(); // 自定义错误处理函数
  }
  
  // 启用首次接收中断(必须!否则无法触发中断回调)
  HAL_UART_Receive_IT(&huart1, &uart_rx_temp, 1); 
}
1.2.4 串口任务处理(超时判断+数据解析)

在主循环(或RTOS任务)中轮询判断超时,触发数据解析:

c 复制代码
// 超时阈值:最大数据传输间隔(可根据实际通信速率调整,如100ms)
#define UART_TIMEOUT_MS 100

void uart_task(void) 
{
    // 无数据时直接返回,减少CPU占用
    if(uart_rx_index == 0) return;
    
    // 超时判断:当前时间 - 最后接收时间 > 超时阈值
    if(uwTick - uart_rx_ticks > UART_TIMEOUT_MS)
    {
        // 解析数据(示例:回显接收到的数据)
        my_printf(&huart1, "收到串口数据:%s\n", uart_rx_buffer);
        
        // 清空缓冲区,准备下一次接收
        memset(uart_rx_buffer, 0, uart_rx_index);
        uart_rx_index = 0;
        
        // 重置缓冲区指针(可选,提升鲁棒性)
        huart1.pRxBuffPtr = uart_rx_buffer;
    }
}
1.2.5 头文件封装(usart_app.h)
c 复制代码
#ifndef __USART_APP_H__
#define __USART_APP_H__

#include "mydefine.h"
#include "stm32f1xx_hal.h"

// 自定义串口printf函数(需自行实现,替代标准printf)
int my_printf(UART_HandleTypeDef *huart, const char *format, ...);
// 串口任务函数(需在主循环/RTOS任务中调用)
void uart_task(void);

#endif

二、进阶方案:DMA+空闲中断解析法(低CPU占用)

2.1 核心优势与工作原理

核心优势
  • DMA解放CPU:串口接收的数据直接通过DMA通道写入内存,无需CPU逐字节处理,大幅降低CPU占用;
  • 硬件级空闲检测:UART空闲中断由硬件检测RX总线高电平时长(超过1个完整数据帧),精准判定数据帧结束,无软件超时误差;
  • 适配高频数据流:适合传感器周期性采集、大数据量连续传输场景。
工作原理
  1. CPU初始化DMA通道,指定串口RX数据的存储缓冲区;
  2. DMA自动监听串口RX引脚,将数据写入缓冲区,全程无需CPU干预;
  3. 当RX总线空闲(无数据传输)超过1帧时长,硬件触发空闲中断;
  4. 中断回调中停止DMA、拷贝数据、解析帧,随后重启DMA等待下一次接收。

2.2 CubeMX配置要点

  1. 串口基础配置:波特率、数据位、停止位等(如115200-8N1);
  2. DMA配置 :为USART1_RX添加DMA通道(如DMA1 Channel5),选择:
    • 普通模式:单次传输完成后停止,适配单次数据帧;
    • 循环模式:传输完成后自动重启,适配连续数据流;
  3. 中断配置:启用USART1全局中断,关闭DMA半满中断(避免冗余触发);
  4. 生成代码 :确保DMA句柄hdma_usart1_rx正确生成。

2.3 完整代码实现

2.3.1 全局变量定义
c 复制代码
#include "stm32f1xx_hal.h"
#include <string.h>

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx; // CubeMX自动生成的DMA句柄

// DMA原始接收缓冲区(直接存储串口数据)
uint8_t uart_rx_dma_buffer[128] = {0};
// 数据处理缓冲区(解析用,避免DMA缓冲区被覆盖)
uint8_t uart_dma_buffer[128] = {0};
// 解析标志位(0:无数据,1:待解析)
uint8_t uart_parse_flag = 0;
2.3.2 DMA+空闲中断回调函数

HAL库HAL_UARTEx_RxEventCallback是串口RX事件(空闲/满帧)的回调函数,专门适配DMA+空闲中断场景:

c 复制代码
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) 
{
    if (huart->Instance == USART1) // 仅处理USART1
    {
        // 1. 停止当前DMA传输(避免数据拷贝时被覆盖)
        HAL_UART_DMAStop(huart);
        
        // 2. 边界检查:预留字符串终止符空间
        if (Size > sizeof(uart_dma_buffer) - 1)
        {
            Size = sizeof(uart_dma_buffer) - 1;
        }
        
        // 3. 拷贝数据到处理缓冲区,并添加'\0'(便于格式化输出)
        memcpy(uart_dma_buffer, uart_rx_dma_buffer, Size);
        uart_dma_buffer[Size] = '\0';
        
        // 4. 置位解析标志,通知任务处理
        uart_parse_flag = 1;
        
        // 5. 清空DMA原始缓冲区(可选,根据需求)
        memset(uart_rx_dma_buffer, 0, sizeof(uart_rx_dma_buffer));
        
        // 6. 重启DMA接收,关闭半满中断(关键)
        HAL_UARTEx_ReceiveToIdle_DMA(huart, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
        __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); // 禁用DMA半满中断
    }
}
2.3.3 串口初始化(启用DMA+空闲中断)
c 复制代码
void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
  
  // 启用DMA+空闲中断接收(核心API)
  HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
  // 关闭DMA半满中断,仅保留空闲中断
  __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
}
2.3.4 数据解析任务
c 复制代码
void uart_dma_task(void) 
{
    // 无待解析数据则返回
    if(uart_parse_flag == 0) return;
    
    // 1. 清标志位,避免重复解析
    uart_parse_flag = 0;
    
    // 2. 解析数据(示例:回显)
    my_printf(&huart1, "DMA接收数据:%s\n", uart_dma_buffer);
    
    // 3. 清空处理缓冲区
    memset(uart_dma_buffer, 0, sizeof(uart_dma_buffer));
}

三、开发关键注意事项

3.1 HAL库串口API核心要点

  1. 中断类API(HAL_UART_Receive_IT)为非阻塞调用,需在回调中重新启用,否则仅触发一次中断;
  2. DMA+空闲中断依赖HAL_UARTEx_ReceiveToIdle_DMA,而非普通DMA接收API;
  3. 回调函数为"弱函数",用户重写后会覆盖HAL库默认空实现;
  4. uwTick是ms级系统滴答值,超时判断需注意溢出问题(可改用HAL_GetTick())。

3.2 粘包/封包/解包处理

  • 粘包问题:连续数据帧传输时边界模糊,超时法/空闲中断法可天然解决(以"超时/空闲"作为帧结束标志);
  • 工业级优化 :建议在数据帧中增加"帧头+长度+校验位"(如0xAA + 数据长度 + 数据 + 校验和 + 0x55),进一步提升可靠性;
  • 缓冲区溢出:所有方案均需添加边界检查,避免数组越界导致程序崩溃。

四、方案选型与总结

方案 优点 缺点 适用场景
超时解析法 逻辑简单、无硬件依赖、快速落地 CPU占用略高、软件超时有误差 短报文、低频交互(如指令控制)
DMA+空闲中断法 低CPU占用、硬件级精准、效率高 配置稍复杂、依赖DMA外设 高频数据流、大数据量(如传感器采集)

两种方案均能完美解决STM32串口不定长数据接收问题,实际开发中可根据场景选择:

  • 快速验证、小量数据:优先选超时解析法;
  • 高性能、连续数据流:优先选DMA+空闲中断法。

附:自定义my_printf实现(可选)

标准printf默认输出到调试串口,如需串口打印,可实现自定义my_printf

c 复制代码
#include <stdarg.h>

int my_printf(UART_HandleTypeDef *huart, const char *format, ...)
{
    char buf[256] = {0};
    va_list args;
    va_start(args, format);
    vsnprintf(buf, sizeof(buf), format, args);
    va_end(args);
    return HAL_UART_Transmit(huart, (uint8_t*)buf, strlen(buf), 100);
}
相关推荐
yong999011 小时前
STC15W4K32S4系列单片机驱动nRF24L01 2.4G无线接收方案
单片机·嵌入式硬件
崇山峻岭之间11 小时前
单片机蜂鸣器实验
单片机·嵌入式硬件
西城微科方案开发11 小时前
厨房电子秤MCU芯片解决方案
单片机·嵌入式硬件
深圳市晨芯阳科技有限公司11 小时前
HC7253晨芯阳高端电流检测降压LED恒流驱动器
stm32·单片机·嵌入式硬件·驱动ic·深圳市晨芯阳科技有限公司
隔窗听雨眠12 小时前
STM32/ESP32实战驱动的达林顿阵列高效复用指南
stm32·单片机·嵌入式硬件
XiYang-DING12 小时前
【Java EE】TCP(Transmission Control Protocol)
单片机·tcp/ip·java-ee
bubiyoushang88812 小时前
STM32L051 的 串口升级
stm32·单片机·嵌入式硬件
210Brian12 小时前
蓝桥杯单片机学习笔记(十二):V2026 大模板构建(上)
单片机·学习·蓝桥杯
森利威尔电子-13 小时前
森利威尔 SL3037B 替换HT7463A/HT7463B 5.5-60V宽压 峰值 0.6A
单片机·嵌入式硬件·物联网·集成电路·芯片