STM32串口3实战:中断接收与帧头帧尾解析不定长数据
1. STM32串口3中断接收基础配置在STM32开发中串口通信是最常用的外设之一。当串口1被占用时我们可以使用其他串口如USART3进行数据收发。这里以STM32F103C8T6为例串口3对应的引脚是PB10TX和PB11RX。先来看串口3的初始化代码这里需要特别注意时钟使能的细节void USART3_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 注意GPIOB时钟和USART3时钟属于不同总线 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // GPIOB在APB2总线 RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); // USART3在APB1总线 // 配置TX引脚为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOB, GPIO_InitStructure); // 配置RX引脚为浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStructure); // 配置USART3中断优先级 NVIC_InitStructure.NVIC_IRQChannel USART3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority 2; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 配置串口参数 USART_InitStructure.USART_BaudRate 9600; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART3, USART_InitStructure); USART_ITConfig(USART3, USART_IT_RXNE, ENABLE); // 使能接收中断 USART_Cmd(USART3, ENABLE); // 使能串口 }关键细节STM32F103系列中GPIOB时钟在APB2总线而USART3时钟在APB1总线。这与USART1不同USART1时钟也在APB2所以初始化时不能像USART1那样将时钟使能写在一起。2. 固定长度数据帧接收方案对于固定长度的数据帧我们可以使用状态变量来记录当前接收位置。下面是一个接收4字节数据的实现示例uint8_t data[4]; // 接收缓冲区 uint8_t h 0; // 状态变量 uint8_t COM3_RecvFin 0; // 接收完成标志 void USART3_IRQHandler(void) { uint8_t receivedData; if(USART_GetFlagStatus(USART3, USART_IT_RXNE) ! RESET) { receivedData USART_ReceiveData(USART3); if((receivedData 0xFF) (h 0)) { h 1; data[0] receivedData; } else if(h 1) { h 2; data[1] receivedData; } else if(h 2) { h 3; data[2] receivedData; } else if(h 3) { COM3_RecvFin 1; h 0; data[3] receivedData; } else { h 0; // 错误状态重置 } USART_ClearITPendingBit(USART3, USART_IT_RXNE); while(USART_GetFlagStatus(USART3, USART_FLAG_TC) RESET); } }这种方案的优点是实现简单适合已知长度的数据帧。但缺点也很明显如果数据长度变化就需要修改代码。在实际项目中传感器数据长度通常是固定的这种方案就很适用。3. 不定长数据帧的接收与解析当处理来自蓝牙、LoRa等模块的数据时数据长度往往不固定。这时我们需要使用帧头帧尾来判断数据边界。下面是一个基于0xFF帧头和0xEE帧尾的实现uint8_t data[50]; // 足够大的缓冲区 uint8_t Start_Receiving 0; uint8_t COM3_RecvFin 0; uint8_t i 0; void USART3_IRQHandler(void) { uint8_t receivedData; if(USART_GetITStatus(USART3, USART_IT_RXNE) ! RESET) { receivedData USART_ReceiveData(USART3); if(receivedData 0xFF) { Start_Receiving 1; i 0; // 重置索引 } else if(receivedData 0xEE) { COM3_RecvFin 1; Start_Receiving 0; } if(Start_Receiving 1) { data[i] receivedData; if(i sizeof(data)) i 0; // 防止溢出 } USART_ClearITPendingBit(USART3, USART_IT_RXNE); while(USART_GetFlagStatus(USART3, USART_FLAG_TC) RESET); } }这个方案虽然能处理不定长数据但有个明显缺陷如果数据内容中出现0xEE会被误认为是帧尾。我在实际项目中就遇到过这个问题导致数据被提前截断。下面介绍更健壮的解决方案。4. 状态机实现健壮的帧解析为了解决数据内容与帧头帧尾冲突的问题我们可以实现一个接收状态机typedef enum { WAIT_HEADER, RECEIVING, WAIT_FOOTER } UART_State; UART_State rxState WAIT_HEADER; uint8_t rxBuffer[128]; uint16_t rxIndex 0; void USART3_IRQHandler(void) { uint8_t ch USART_ReceiveData(USART3); switch(rxState) { case WAIT_HEADER: if(ch 0xFF) { rxState RECEIVING; rxIndex 0; } break; case RECEIVING: if(ch 0xEE) { rxState WAIT_FOOTER; } else { rxBuffer[rxIndex] ch; if(rxIndex sizeof(rxBuffer)) { rxIndex 0; // 防止溢出 } } break; case WAIT_FOOTER: if(ch 0xEE) { // 连续两个0xEE才认为是帧尾 COM3_RecvFin 1; rxState WAIT_HEADER; } else { // 不是帧尾把之前收到的0xEE当作数据 rxBuffer[rxIndex] 0xEE; rxBuffer[rxIndex] ch; rxState RECEIVING; } break; } USART_ClearITPendingBit(USART3, USART_IT_RXNE); }这种状态机方案通过检测连续两个0xEE作为帧尾大大降低了误判概率。我在一个气象站项目中采用这种方案成功解决了雨量传感器数据中偶尔出现0xEE导致解析错误的问题。5. 缓冲区管理与数据校验在实际项目中除了帧解析外我们还需要考虑缓冲区管理和数据校验。下面是一个更完整的实现#define BUF_SIZE 128 typedef struct { uint8_t buffer[BUF_SIZE]; uint16_t head; uint16_t tail; uint16_t count; } CircularBuffer; CircularBuffer rxBuf; void Buffer_Init(CircularBuffer *buf) { buf-head 0; buf-tail 0; buf-count 0; } uint8_t Buffer_Put(CircularBuffer *buf, uint8_t data) { if(buf-count BUF_SIZE) return 0; // 缓冲区满 buf-buffer[buf-head] data; buf-head (buf-head 1) % BUF_SIZE; buf-count; return 1; } uint8_t Buffer_Get(CircularBuffer *buf, uint8_t *data) { if(buf-count 0) return 0; // 缓冲区空 *data buf-buffer[buf-tail]; buf-tail (buf-tail 1) % BUF_SIZE; buf-count--; return 1; } // 在中断中只负责填充缓冲区 void USART3_IRQHandler(void) { if(USART_GetITStatus(USART3, USART_IT_RXNE) ! RESET) { uint8_t ch USART_ReceiveData(USART3); Buffer_Put(rxBuf, ch); USART_ClearITPendingBit(USART3, USART_IT_RXNE); } } // 在主循环中解析完整帧 void Process_UART_Data(void) { static UART_State state WAIT_HEADER; static uint8_t frame[128]; static uint16_t frameIndex 0; uint8_t ch; while(Buffer_Get(rxBuf, ch)) { switch(state) { // 状态机处理逻辑同上 // ... } } }这种设计将接收和解析分离中断服务函数只做最简单的数据接收复杂的解析放在主循环中处理。同时使用环形缓冲区避免了数据丢失特别适合高速数据接收场景。6. 实际应用中的优化技巧经过多个项目的实践我总结了几个优化串口通信的技巧波特率自适应对于需要兼容不同设备的应用可以实现波特率自动检测。通过测量起始位宽度来推算波特率。超时机制添加接收超时判断当一段时间没有收到新数据时认为一帧结束。这在没有明确帧尾的协议中特别有用。DMA接收对于高速数据流可以使用DMA接收减轻CPU负担。STM32的USART都支持DMA传输。数据校验添加CRC校验或校验和验证数据完整性。一个简单的校验和实现uint8_t Check_Sum(uint8_t *data, uint16_t len) { uint8_t sum 0; for(uint16_t i0; ilen; i) { sum data[i]; } return sum; }流量控制当处理不过来时可以使用硬件流控RTS/CTS或软件流控XON/XOFF通知发送方暂停。7. 常见问题与解决方案在调试串口通信时经常会遇到各种问题。以下是几个典型问题及解决方法问题1接收数据不完整或乱码检查波特率设置是否匹配确认双方接地良好检查线缆质量和长度长距离时需降低波特率问题2只能接收不能发送检查TX/RX线是否接反确认发送使能USART_Mode_Tx已开启检查发送完成标志等待是否正确问题3中断服务函数不执行确认NVIC配置正确且已使能检查中断优先级设置是否被其他高优先级中断阻塞确认USART_ITConfig已调用问题4接收数据偶尔丢失增加接收缓冲区大小提高中断优先级改用DMA接收方式记得在调试时充分利用STM32的调试功能比如查看USART的SR寄存器状态标志这能快速定位问题原因。