在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个完整数据帧),精准判定数据帧结束,无软件超时误差;
- 适配高频数据流:适合传感器周期性采集、大数据量连续传输场景。
工作原理
- CPU初始化DMA通道,指定串口RX数据的存储缓冲区;
- DMA自动监听串口RX引脚,将数据写入缓冲区,全程无需CPU干预;
- 当RX总线空闲(无数据传输)超过1帧时长,硬件触发空闲中断;
- 中断回调中停止DMA、拷贝数据、解析帧,随后重启DMA等待下一次接收。
2.2 CubeMX配置要点
- 串口基础配置:波特率、数据位、停止位等(如115200-8N1);
- DMA配置 :为USART1_RX添加DMA通道(如DMA1 Channel5),选择:
- 普通模式:单次传输完成后停止,适配单次数据帧;
- 循环模式:传输完成后自动重启,适配连续数据流;
- 中断配置:启用USART1全局中断,关闭DMA半满中断(避免冗余触发);
- 生成代码 :确保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核心要点
- 中断类API(
HAL_UART_Receive_IT)为非阻塞调用,需在回调中重新启用,否则仅触发一次中断; - DMA+空闲中断依赖
HAL_UARTEx_ReceiveToIdle_DMA,而非普通DMA接收API; - 回调函数为"弱函数",用户重写后会覆盖HAL库默认空实现;
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);
}