当前位置:首页 > 工业控制 > 工业控制
[导读]工业串口最怕三类问题:半包、粘包、噪声错帧。用“环形缓冲收字节+按字节状态机解析+CRC16校验”三层结构,可做到无动态分配、低RAM占用、错误帧自动重同步。


工业串口最怕三类问题:半包、粘包、噪声错帧。用“环形缓冲收字节+按字节状态机解析+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收批、状态机只处理完整段即可。



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭