目录
前言
一、串口通信最大痛点:粘包与分包
1、什么是分包
2、什么是粘包
二、工业通用串口帧协议格式
三、串口底层架构:环形缓冲区
四、状态机帧解析(解决粘包分包核心)
五、串口空闲中断(判定帧结束)
六、CRC16 校验代码(工业标准)
七、业务解耦:协议层与业务层分离
八、串口工程九大踩坑总结
九、RTOS 架构升级版
前言
在所有嵌入式通信中,串口 USART 是使用最多、BUG 最多、最需要工程架构的通信方式。
新手串口写法:
- 收到数据立刻处理
- 裸字节判断、无帧结构
- 遇到粘包、断包直接解析错乱
- 无校验、无超时、无容错
- 协议和业务耦合,完全无法迭代
量产设备必须拥有标准帧协议 + 环形缓冲区 + 状态机解析 + 超时容错架构。
本篇手把手搭建工业级串口协议栈,解决:粘包、分包、干扰乱码、超时断帧、CRC 校验、自动容错,裸机 / RTOS 通用。
一、串口通信最大痛点:粘包与分包
1、什么是分包
一帧数据被分多次接收 比如一帧 100 字节,分 3 次进串口缓冲区
2、什么是粘包
多帧数据粘在一起一次性收到 两三条指令粘成一坨,直接解析错误
解决核心:必须基于「完整数据帧」解析,不能单字节处理业务
二、工业通用串口帧协议格式
帧头2字节 + 长度1字节 + 命令1字节 + 数据域 + CRC16校验2字节 + 帧尾常用标准格式:
- 帧头:0xAA 0x55
- 数据长度:有效数据长度
- 命令码:区分不同功能
- 数据域:自定义参数
- CRC16:校验整条数据
- 帧尾:可选
优势:
- 不怕乱码、不怕丢字节
- 精准区分每一帧
- 错误帧直接丢弃、自动容错
三、串口底层架构:环形缓冲区
固定数组容易溢出、丢数据,量产全部用环形 FIFO
#define USART_BUF_LEN 256 typedef struct { uint8_t buf[USART_BUF_LEN]; uint16_t w_index; uint16_t r_index; }usart_fifo_t; usart_fifo_t g_usart_fifo; //入队 void usart_fifo_push(uint8_t dat) { uint16_t next = (g_usart_fifo.w_index + 1) % USART_BUF_LEN; if(next != g_usart_fifo.r_index) { g_usart_fifo.buf[g_usart_fifo.w_index] = dat; g_usart_fifo.w_index = next; } } //出队 uint8_t usart_fifo_pop(uint8_t *dat) { if(g_usart_fifo.r_index == g_usart_fifo.w_index) return 0; *dat = g_usart_fifo.buf[g_usart_fifo.r_index]; g_usart_fifo.r_index = (g_usart_fifo.r_index + 1) % USART_BUF_LEN; return 1; } //获取缓存数量 uint16_t usart_fifo_cnt(void) { return (g_usart_fifo.w_index - g_usart_fifo.r_index + USART_BUF_LEN) % USART_BUF_LEN; }中断里只做一件事:压入环形缓冲区
void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t dat = USART_ReceiveData(USART1); usart_fifo_push(dat); } }四、状态机帧解析
采用空闲中断 + 状态机解析串口超过 20ms 无新数据判定一帧接收完毕
typedef enum { USART_IDLE, USART_GET_HEAD, USART_GET_LEN, USART_GET_DATA, USART_GET_CRC }usart_state_t; void usart_parse_process(void) { static usart_state_t state = USART_IDLE; static uint8_t tmp_buf[128]; static uint16_t idx = 0; static uint16_t crc = 0; uint8_t dat; while(usart_fifo_pop(&dat)) { switch(state) { case USART_IDLE: idx = 0; //匹配帧头 0xAA 0x55 if(dat == 0xAA) { tmp_buf[idx++] = dat; state = USART_GET_HEAD; } break; case USART_GET_HEAD: if(dat == 0x55) { tmp_buf[idx++] = dat; state = USART_GET_LEN; } else { state = USART_IDLE; } break; case USART_GET_LEN: tmp_buf[idx++] = dat; state = USART_GET_DATA; break; case USART_GET_DATA: //根据长度接收剩余数据 if(idx < tmp_buf[2] + 3) { tmp_buf[idx++] = dat; } else { //最后两字节CRC tmp_buf[idx++] = dat; state = USART_GET_CRC; } break; case USART_GET_CRC: tmp_buf[idx++] = dat; //CRC校验判断 //crc_check(tmp_buf, idx-2) == 正确 if(1) { //完整合法帧,执行业务处理 usart_data_handle(tmp_buf, idx); } state = USART_IDLE; idx = 0; break; default: state = USART_IDLE; break; } } }五、串口空闲中断
裸机唯一精准判帧方式: 一帧数据传输完成后总线空闲,触发空闲中断,标志本次帧接收完成
void USART_Idle_IRQ(void) { if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) { USART_ClearITPendingBit(USART1, USART_IT_IDLE); //触发一次解析 usart_parse_process(); } }六、CRC16 校验代码
uint16_t crc16_calc(uint8_t *buf,uint16_t len) { uint16_t crc = 0xFFFF; uint16_t i,j; for(i=0;i<len;i++) { crc ^= buf[i]; for(j=0;j<8;j++) { if(crc & 1) crc = (crc >> 1) ^ 0xA001; else crc >>= 1; } } return crc; }校验作用:杜绝干扰乱码、字节错误,量产设备必须带 CRC。
七、业务解耦:协议层与业务层分离
void usart_data_handle(uint8_t *buf,uint16_t len) { uint8_t cmd = buf[3]; switch(cmd) { case 0x01: //设备查询指令 break; case 0x02: //参数设置指令 break; case 0x03: //控制指令 break; default: //非法指令直接丢弃 break; } }八、串口工程九大踩坑总结
- 不用环形缓冲区:数据溢出、丢字节
- 无帧头帧尾:无法区分粘包
- 无超时判断:残留半截脏数据卡死解析
- 无 CRC 校验:干扰直接乱执行
- 中断里处理业务:导致中断过长、卡死、优先级抢占
- 单字节判断指令:完全不适合量产
- 不做容错复位:一次错误后续全部解析失败
- 串口发送不屏蔽中断:并发错乱
- 大数据不做分包发送:缓冲区溢出
九、RTOS 架构升级版
RTOS 中直接将接收数据放入队列:
xQueueSend(uart_queue,&dat,0);任务中统一解析,实时性更强、完全无阻塞。