一、核心问题分析
串口接收的痛点在于数据生产(中断/回调)与消费(解析任务)速度不匹配,以及一次回调不等于一帧数据。例如:
- 一帧数据 55 7E 03 10 A4 5B CRC 被拆成三次回调:55 7E 03、10 A4、5B CRC
- 解析任务需要在数据不连续的情况下,仍能正确识别帧头、长度、数据体和校验位
二、解决方案架构
采用生产者-消费者模型:
2.1 环形缓冲区设计
环形缓冲区用于暂存未处理的字节,解耦生产与消费速度:
#define RING_BUF_SIZE 256 // 缓冲区大小,根据实际需求调整
typedef struct {
uint8_t buffer[RING_BUF_SIZE];
volatile uint16_t head; // 写指针(生产者修改)
volatile uint16_t tail; // 读指针(消费者修改)
} RingBuffer;
RingBuffer rx_ring_buf = {0};
// 生产者:向环形缓冲区写入字节(串口中断调用)
void ring_buf_write(uint8_t byte) {
uint16_t next_head = (rx_ring_buf.head + 1) % RING_BUF_SIZE;
if (next_head != rx_ring_buf.tail) { // 缓冲区未满
rx_ring_buf.buffer[rx_ring_buf.head] = byte;
rx_ring_buf.head = next_head;
} else {
// 缓冲区溢出处理:可记录错误日志
}
}
// 消费者:从环形缓冲区读取字节(解析任务调用)
bool ring_buf_read(uint8_t *byte) {
if (rx_ring_buf.head == rx_ring_buf.tail) {
return false; // 缓冲区为空
}
*byte = rx_ring_buf.buffer[rx_ring_buf.tail];
rx_ring_buf.tail = (rx_ring_buf.tail + 1) % RING_BUF_SIZE;
return true;
}
2.2 状态机设计
状态机用于记录解析进度,核心状态包括:
- WAIT_SOF:等待帧头(示例中帧头为 0x7E)
- READ_LEN:读取数据体长度
- READ_BODY:读取数据体
- READ_CRC:读取并校验CRC
// 协议帧格式示例:[帧头(1B)] + [长度(1B)] + [数据体(NB)] + [CRC(1B)]
typedef enum {
WAIT_SOF, // 等待帧头
READ_LEN, // 读取长度
READ_BODY, // 读取数据体
READ_CRC // 读取CRC
} ParseState;
typedef struct {
ParseState state; // 当前状态
uint8_t expected_len; // 期望的数据体长度
uint8_t body_pos; // 数据体接收位置
uint8_t body_buf[64]; // 数据体缓冲区(根据实际需求调整大小)
} Parser;
Parser parser = {0};
// 重置解析器
void parser_reset(void) {
parser.state = WAIT_SOF;
parser.expected_len = 0;
parser.body_pos = 0;
memset(parser.body_buf, 0, sizeof(parser.body_buf));
}
// 解析单个字节
void parser_feed(uint8_t byte) {
switch (parser.state) {
case WAIT_SOF:
if (byte == 0x7E) { // 匹配帧头
parser.state = READ_LEN;
}
break;
case READ_LEN:
if (byte > sizeof(parser.body_buf)) { // 长度超出缓冲区
parser_reset();
break;
}
parser.expected_len = byte;
parser.body_pos = 0;
parser.state = READ_BODY;
break;
case READ_BODY:
parser.body_buf[parser.body_pos++] = byte;
if (parser.body_pos == parser.expected_len) { // 数据体接收完成
parser.state = READ_CRC;
}
break;
case READ_CRC:
// 校验CRC(示例为简单异或校验,可替换为CRC16/CRC32)
uint8_t calc_crc = 0;
for (uint8_t i = 0; i < parser.expected_len; i++) {
calc_crc ^= parser.body_buf[i];
}
if (calc_crc == byte) { // CRC校验成功
// 处理完整帧:parser.body_buf 为有效数据
printf("解析成功,数据长度:%d\\n", parser.expected_len);
// 这里可添加数据处理逻辑
} else {
printf("CRC校验失败\\n");
}
parser_reset(); // 无论成功失败,重置状态机
break;
default:
parser_reset();
break;
}
}
2.3 主循环解析任务
在主循环中,持续从环形缓冲区读取字节并喂给状态机:
void protocol_poll(void) {
uint8_t byte;
while (ring_buf_read(&byte)) { // 逐字节读取
parser_feed(byte); // 喂给状态机解析
}
}
2.4 串口中断回调
在串口接收中断中,将接收到的字节写入环形缓冲区:
// 串口接收中断回调函数(以STM32 HAL库为例)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
uint8_t rx_byte;
HAL_UART_Receive_IT(huart, &rx_byte, 1); // 继续接收下一个字节
ring_buf_write(rx_byte); // 写入环形缓冲区
}
}
三、关键优化点
四、扩展与适配
- 多帧支持:如果一次回调包含多帧数据,状态机会自动处理,无需额外修改
- 变长协议:只需修改状态机的READ_LEN和READ_BODY逻辑,即可适配不同协议
- DMA接收:可将环形缓冲区与DMA结合,进一步降低CPU占用
五、总结
通过环形缓冲区+状态机的组合,我们实现了一套稳定的串口协议解析方案,能够有效处理拆分包场景。这套方案已在多个嵌入式项目中验证,可直接落地使用。
代码写得好,其实是一种艺术。稳定、清楚、可维护的代码,才是嵌入式开发的核心追求。
参考资料:
如果你需要适配特定协议(如Modbus、自定义协议),或需要优化性能,欢迎在评论区交流。


