C语言解析自定义通信协议:实现串口数据包的无差错快速解包
工业串口最怕三类问题:半包、粘包、噪声错帧。用“环形缓冲收字节+按字节状态机解析+CRC16校验”三层结构,可做到无动态分配、低RAM占用、错误帧自动重同步。
一、帧格式与环缓收数
自定义帧采用:0xAA 0x55 +长度1字节+命令1字节+载荷N字节+CRC16低+CRC16高。中断只做入队,不解析业务:
#define RB_SZ 256u
#define RB_MSK (RB_SZ-1u)
static uint8_t rb[RB_SZ];
static volatile uint16_t rb_h, rb_t;
static inline void rb_isr_put(uint8_t d){
uint16_t n=(rb_h+1)&RB_MSK;
if(n!=rb_t){ rb[rb_h]=d; rb_h=n; }
}
static int rb_get(uint8_t *d){
if(rb_h==rb_t) return -1;
*d=rb[rb_t]; rb_t=(rb_t+1)&RB_MSK; return 0;
}
2幂环缓用位与代替取模,单写单读可不加锁;多中断源写同一口再短关中断。
二、CRC16先算准
用CCITT-FALSE(多项式0x1021、初值0xFFFF、结果不取反)做工业轻量校验,资源紧可逐位实现:
uint16_t crc16_ccitt(const uint8_t *p, uint16_t len){
uint16_t crc=0xFFFF;
while(len--){
crc^=(uint16_t)(*p++)<<8;
for(uint8_t i=0;i<8;i++){
crc=(crc&0x8000)?(uint16_t)((crc<<1)^0x1021):(uint16_t)(crc<<1);
}
}
return crc;
}
若帧率很高,可预生成256项表改查表法,速度更快、多占约512字节Flash。 校验范围要和发送端严格一致:本例对“长度+命令+载荷”计算,帧头不计入,避免无谓耦合。
三、按字节状态机解包
状态只跟“当前收到第几个字段”走,任意脏字节都能回WAIT_HDR重找同步:
typedef enum{ S_H1,S_H2,S_LEN,S_CMD,S_PAY,S_CRC1,S_CRC2 }st_t;
static st_t st=S_H1;
static uint8_t len,cmd,pl[128],pidx;
static uint16_t crc_acc;
void proto_feed(uint8_t b){
switch(st){
case S_H1: if(b==0xAA) st=S_H2; break;
case S_H2: st=(b==0x55)?S_LEN:S_H1; break;
case S_LEN:
if(b==0||b>sizeof(pl)){ st=S_H1; break; }
len=b; pidx=0; crc_acc=0; st=S_CMD; break;
case S_CMD: cmd=b; crc_acc=crc16_ccitt(&b,1); st=S_PAY;
if(len==0) st=S_CRC1; break;
case S_PAY:
pl[pidx++]=b; crc_acc=crc16_ccitt(&b,1);
if(pidx>=len) st=S_CRC1; break;
case S_CRC1:
crc_acc&=0xFF; if(b!=crc_acc){ st=S_H1; break; } /* 低字节示例 */
st=S_CRC2; break;
case S_CRC2:
if(b!=((crc16_ccitt_final())&0xFF)){ st=S_H1; break; }
app_dispatch(cmd,pl,len); st=S_H1; break;
default: st=S_H1; break;
}
}
上面S_CRC1/S_CRC2按“低字节先收、高字节后收”拆分;若直接用整体CRC,可在收完高字节后一次性crc16_ccitt(长度/命令/载荷)比对。长度越界、命令未知、CRC不符都回S_H1,不从错误状态硬续,避免脏数据传播。
四、主循环与重同步
主循环只取环缓、喂状态机,不阻塞:
void uart_task(void){
uint8_t b;
while(rb_get(&b)==0){
proto_feed(b);
}
}
高波特率可改DMA+空闲中断,把一批数据逐字节喂proto_feed;ISR不调用app_dispatch重逻辑,只做“存数—置标志”。出错统计单独计数:帧头错、长度错、CRC错分别累加,便于现场定位是线缆干扰还是对端组包错误。
五、避坑要点
长度字段必须限上限,超过接收缓冲直接弃帧,防恶意/错包撑爆静态数组;
CRC别用单字节累加和,突发翻转漏检率高,工业链路口用CRC16;
状态机静态变量在多实例串口要改成结构体数组,按端口索引,别共用全局;
半包自然续解,粘包连续喂字节即可,不需缓存“整包”再搜帧头;
若载荷可能含0xAA 0x55等类帧头字节,不要靠帧头自同步,必须靠长度+CRC截断,避免误切。
这套结构在Cortex-M0+上也能跑:环缓256字节、载荷128字节、CRC逐位,115200波特单帧几十字节时CPU占比很低;要再快换CRC查表、DMA收批、状态机只处理完整段即可。





