目录
[六、CRC16 校验代码(工业标准)](#六、CRC16 校验代码(工业标准))
[九、RTOS 架构升级版](#九、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);
任务中统一解析,实时性更强、完全无阻塞。