一、项目硬件说明
主控:STM32F103RCT6
外设:USART1,波特率 115200
场景:上位机下发不定长指令,单片机接收并解析,报文长度动态变化,没有固定长度。
核心设计原则:中断只做数据缓存,业务解析全部放到 while 主循环,禁止在中断函数做复杂处理,防止丢字节。
二、全局缓冲区定义
#include "stm32f10x.h"
#define USART_RX_BUF_SIZE 256 // 接收缓存大小,根据项目调整
uint8_t g_usart_rx_bufUSART_RX_BUF_SIZE;
uint16_t g_usart_rx_cnt = 0; // 当前接收字节计数
uint8_t g_usart_frame_ready = 0; // 一帧数据接收完成标志
三、USART1 初始化(开启接收中断 + 空闲中断)
GPIO、NVIC 基础配置省略,重点开启RXNE接收中断、IDLE空闲中断。
void USART1_Config(uint32_t baudrate)
{
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
// PA9 TX
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA10 RX
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStruct);
USART_InitStruct.USART_BaudRate = baudrate;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStruct);
// 关键:开启接收中断 + 空闲中断IDLE
USART_ITConfig(USART1, USART_IT_RXNE | USART_IT_IDLE, ENABLE);
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
USART_Cmd(USART1, ENABLE);
}
四、中断服务函数(核心)
⚠️重点坑点:空闲中断 IDLE 标志不能直接调用 ClearITPendingBit 清除 ,必须先读 SR 状态寄存器,再读 DR 数据寄存器完成标志清零,很多人在这里踩坑导致空闲中断只触发一次。
void USART1_IRQHandler(void)
{
// 收到1字节数据
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
// 防止缓冲区溢出
if(g_usart_rx_cnt < USART_RX_BUF_SIZE)
{
g_usart_rx_buf[g_usart_rx_cnt++] = USART_ReceiveData(USART1);
}
}
// 总线空闲,判定一帧不定长数据接收完毕
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
{
// IDLE中断标准清除序列,固定写法,不要省略
(void)USART_GetFlagStatus(USART1, USART_FLAG_IDLE);
(void)USART_ReceiveData(USART1);
// 过滤空帧
if(g_usart_rx_cnt > 0)
{
g_usart_frame_ready = 1;
}
}
}
五、主循环处理不定长报文
中断仅仅置位标志位,解析、打印、业务逻辑全部放在 main 的 while 循环 。
int main(void)
{
USART1_Config(115200);
while(1)
{
if(g_usart_frame_ready)
{
/*****************
* 此处处理不定长报文
* g_usart_rx_buf:数据缓存
* g_usart_rx_cnt:当前帧有效字节数
*****************/
// 示例:printf打印收到的不定长数据
for(uint16_t i = 0; i < g_usart_rx_cnt; i++)
{
printf("%02X ", g_usart_rx_buf[i]);
}
printf("\r\n");
// 清空计数与标志,准备接收下一帧
g_usart_rx_cnt = 0;
g_usart_frame_ready = 0;
}
}
}
六、HAL 库简易版本补充(可选)
CubeMX 生成工程,使用 HAL 库 IDLE 空闲中断思路一致,供快速参考
// usart.c
extern UART_HandleTypeDef huart1;
#define RX_BUF_SIZE 256
uint8_t rx_bufRX_BUF_SIZE;
uint16_t rx_cnt = 0;
uint8_t frame_ok = 0;
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
}
// 回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
if(rx_cnt < RX_BUF_SIZE)
{
rx_bufrx_cnt++ = huart->Instance->DR;
}
HAL_UART_Receive_IT(&huart1, &rx_bufrx_cnt, 1);
}
}
void HAL_UART_IdleCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
if(rx_cnt > 0) frame_ok = 1;
}
}
七、项目实战坑点汇总
- 空闲中断只触发一次
IDLE 标志清除方式错误,不能使用USART_ClearITPendingBit,必须执行读 SR、读 DR 操作。 - 短时间连续发送出现粘帧
空闲中断依靠总线空闲电平判定帧结束;如果上位机报文间隔极短,硬件识别不到空闲,多包数据合并。
解决:通信协议增加帧间隔;高频场景改用帧头帧尾状态机方案。 - 丢字节现象
中断函数内部不要写 printf、延时、复杂解析;解析逻辑移出中断;适当调高串口中断抢占优先级。 - 出现空帧
判断g_usart_rx_cnt > 0,过滤总线扰动产生的空闲中断误触发。 - 缓冲区溢出
USART_RX_BUF_SIZE根据项目最大报文长度预留,接收时做长度判断,防止数组越界。
八、方案优缺点总结
✅ 优点
- 不需要定时器,不占用额外片上外设资源
- 无需预先约定报文长度,原生支持不定长接收
- 代码量少,移植简单,大部分 STM32 芯片支持 IDLE 空闲中断
❌ 缺点
- 报文间隔过小会发生粘帧,不适合无间隔高速连续数据流
适用场景:上位机指令交互、传感器回传报文、人机交互串口命令。
源码设计思路:硬件空闲事件判定帧边界,中断层只做最轻量化 IO 搬运,业务逻辑和底层驱动解耦,满足中小型项目稳定运行要求。