实现串口通信数据帧打包与解析,串口通信可靠传输,屡试不爽的数据封包与状态机数据解析程序

前言
串口通信是一种异步通信方式,收发双方约定好通信速率,通过两根数据线即可简单的时序全双工数据收发。最常用的串口通信协议由1位起始位 8位数据位 1位停止位组成,总共10位,为了提高通信可靠性,也可在停止位前增加 1位奇偶校验位,但同时也增加可开销,每字节数据需要多传1位二进制数。

串口通信虽然简单方便,但实际使用时会发现需要传输的不止1个字节,往往需要传输n个字节组成的数据包,而因为串口通信中字节之间相互独立,在接收数据时面临 数据包对齐 和防止出错的两大问题。为了解决这两个问题,本文在发送端通过将数据按指定格式打包,在接收端使用状态机解析数据,实现串口通信可靠传输。

一、实现思路
前言指出串口通信面临 数据包对齐 和防止出错的两大问题。

数据包对齐在也叫数据帧同步,解决方法就是引入帧同步字节,也就是增加帧头、帧尾等,对于固定长度数据帧通信可以只使用帧头帧尾,对于可变长度数据帧通信还需引入描述帧长的字节。利用帧头、帧尾、帧长即可解决。
防止数据出错也叫差错控制,在通信原理中,有四种差错控制方法:检错重发、前向纠错、反馈校验、检错删除。四种差错控制方法各有其优缺点,本文采用检错删除的差错控制算法,故只需要考虑如何检错这一个问题,只需要在数据帧中增加校验字节。
一、发送端
1.1 实现过程
本文针对较为复杂的一种通信场景进行总结,需要发送变长的数据帧。其他场景可在此基础上进行简化。为了发送变长数据帧,使用帧头+帧长+命令字节+数据字节+校验字节+帧尾的格式对数据包进行打包,其实帧头和帧长已经足够解决帧对齐问题,帧尾可以去掉,为了适应更复杂的情况,这里保留帧尾。

帧头:本人喜欢使用 0xA5,0x5A两个字节作为帧头,因为它们对应的二进制位0与1的个数相同,分布均匀不易出错。
帧长:根据数据帧实际长度确定帧长字节,这里只使用1个字节,故帧长字节最大为255,为提高利用率,规定帧长字节描述的是数据字节的长度,故应重新命名为数据长度字节。
命令字节:利用命令字节指定数据字节的功能,例如命令字节为1表示传输温度,为2表示传输湿度等,1字节命令+n字节数据是工业中比较常用的一种格式。
数据字节:数据字节长度可变,帧长字节为0,表示没有数据,帧长字节为255,表示有255字节数据。
校验字节:比较简单的一种校验方式为和校验,即把校验字节前的所以字节求和,最后保留低8位作为校验字节。在MODBUS协议中常用CRC16循环冗余校验方式,将校验字节前的所以字节加入计算,得到两字节CRC16校验码,本文采用此方式。
帧尾:与帧头相似,这里使用0xFF作为帧尾。
于是我们得到以下数据帧格式

帧头 数据长度 命令 数据 CRC16校验 帧尾
A5 5A XX XX …XX … XX XX FF
2字节 1字节 1字节 n字节 … 2字节 1字节
1.2 实现代码
1.2.1 定义数据发送函数
这部分可根据需要进行修改,只需调用字节发送函数即可。

void Send(const uint8_t *data,uint8_t len)
{
uint8_t i;
for (i = 0; i < len; i++)
{
SendByte(data[i]);//发送一个字节
}
}

1.2.2 CRC16校验代码
uint16_t CRC16_Check(const uint8_t *data,uint8_t len)
{
uint16_t CRC16 = 0xFFFF;
uint8_t state,i,j;
for(i = 0; i < len; i++ )
{
CRC16 ^= data[i];
for( j = 0; j < 8; j++)
{
state = CRC16 & 0x01;
CRC16 >>= 1;
if(state)
{
CRC16 ^= 0xA001;
}
}
}
return CRC16;
}

1.2.3 数据帧打包代码
最后一句Send(buf,cnt);调用数据帧发送函数将打包好的数据帧发送出去

void Send_Cmd_Data(uint8_t cmd,const uint8_t *datas,uint8_t len)
{
uint8_t buf[300],i,cnt=0;
uint16_t crc16;
buf[cnt++] = 0x55;
buf[cnt++] = 0xAA;
buf[cnt++] = len;
buf[cnt++] = cmd;
for(i=0;i<len;i++)
{
buf[cnt++] = datas[i];
}
crc16 = CRC16_Check(buf,len+4);
buf[cnt++] = crc16&0xff;
buf[cnt++] = crc16>>8;
buf[cnt++] = 0xFF;
Send(buf,cnt);//调用数据帧发送函数将打包好的数据帧发送出去
}

二、接收端
2.1 实现过程
接收端采用状态机解析数据,初次接触状态机是在《数字电子技术基础》中实现N模计数器的过程中发现的。简单来说就是使程序在不同状态之间切换,所以只需要考虑有几种状态,以及状态之间切换的条件是什么?

帧头 数据长度 命令 数据 CRC16校验 帧尾
A5 5A XX XX …XX … XX XX FF
2字节 1字节 1字节 n字节 … 2字节 1字节
2.1.1 有几种状态?
根据这个表格可以简单确定至少有6钟状态,考虑到帧头有2个字节,用两个状态来表示,同理校验有2个字节也用2种状态来表示。故一共8种状态,分别是:

(状态0)等待接收帧头第1字节0xA5
(状态1)等待接收帧头第2字节0x5A
(状态2)等待接收数据长度字节
(状态3)等待接收命令字节
(状态4)等待接收数据字节
(状态5)等待接收校验字节高8位
(状态6)等待接收校验字节低8位
(状态7)等待接收帧尾字节0xFF
2.1.2 状态之间如何切换?
以下就是状态转换关系图,咋一看有点复杂,仔细一看思路还是比较清晰的。一般编程之前都需要在草纸上画出状态图,方便转化为代码。正确绘制状态循环图方可使程序在状态之间顺利切换,且当任意一个数据有误时也能以最高的效率自动归位,尤其是状态1的转换关系。

接收到0xA5
接收到0x5A
接收到0xA5
接收到非0x5A非0x5A
接收到长度字节
接收到命令字节且长度字节大于0
接收到n字节数据
接收到命令字节且长度字节为0
接收到校验字节高8位
接收到校验字节低8位
校验正确
校验错误但本次接收为0xA5
校验错误且本次接收为非0xA5
接收到0xFF
接收到0xA5
接收到非0xA5
状态0
状态1
状态2
状态3
状态4
状态5
状态6
状态1
校验数据
接收数据帧成功 调用数据处理函数
2.2 实现代码
2.2.1 定义数据处理函数
定义数据处理函数用来处理解析成功的数据

void Data_Analysis(uint8_t cmd,const uint8_t *datas,uint8_t len)
{
//根据需要处理数据

 

}

2.2.2 CRC16校验代码
与发送端完全相同

uint16_t CRC16_Check(const uint8_t *data,uint8_t len)
{
uint16_t CRC16 = 0xFFFF;
uint8_t state,i,j;
for(i = 0; i < len; i++ )
{
CRC16 ^= data[i];
for( j = 0; j < 8; j++)
{
state = CRC16 & 0x01;
CRC16 >>= 1;
if(state)
{
CRC16 ^= 0xA001;
}
}
}
return CRC16;
}

2.2.3 状态机解析数据
输入参数:接收到的数据字节

//接收数据
void Receive(uint8_t bytedata)
{
static uint8_t step=0,//状态变量初始化为0 在函数中必须为静态变量
static uint8_t cnt=0,Buf[300],len,cmd,*data_ptr;
static uint16_t crc16;
//进行数据解析 状态机
switch(step)
{
case 0://接收帧头1状态
if(bytedata== 0xA5)
{
step++;
cnt = 0;
Buf[cnt++] = bytedata;
}break;
case 1://接收帧头2状态
if(bytedata== 0x5A)
{
step++;
Buf[cnt++] = bytedata;
}
else if(bytedata== 0xA5)
{
step = 1;
}
else
{
step = 0;
}
break;
case 2://接收数据长度字节状态
step++;
Buf[cnt++] = bytedata;
len = bytedata;
break;
case 3://接收命令字节状态
step++;
Buf[cnt++] = bytedata;
cmd = bytedata;
data_ptr = &Buf[cnt];//记录数据指针首地址
if(len == 0)step++;//数据字节长度为0则跳过数据接收状态
break;
case 4://接收len字节数据状态
Buf[cnt++] = bytedata;
if(data_ptr + len == &Buf[cnt])//利用指针地址偏移判断是否接收完len位数据
{
step++;
}
break;
case 5://接收crc16校验高8位字节
step++;
crc16 = bytedata;
break;
case 6://接收crc16校验低8位字节
crc16 <<= 8;
crc16 += bytedata;
if(crc16 == CRC16_Check(Buf,cnt))//校验正确进入下一状态
{
step ++;
}
else if(bytedata == 0xA5)
{
step = 1;
}
else
{
step = 0;
}
break;
case 7://接收帧尾
if(bytedata== 0xFF)//帧尾接收正确
{
Data_Analysis(cmd,data_ptr,len);//数据解析
step = 0;
}
else if(bytedata == 0xA5)
{
step = 1;
}
else
{
step = 0;
}
break;
default:step=0;break;//多余状态,正常情况下不可能出现
}
}
总结
本文程序可方便的嵌入到源代码中,发送端只需调用
void Send_Cmd_Data(uint8_t cmd,const uint8_t *datas,uint8_t len);
函数发送命令和数据,接收端定义
void Data_Analysis(uint8_t cmd,const uint8_t *datas,uint8_t len);
函数解析数据。
状态机的思想参考《数字电子技术基础》,差错控制方法参考《通信原理》而通信层级关系可参考ISO模型,串口通信传输的是比特流类似于物理层,而数据帧通信传输的是数据帧类似于数据链路层,文末两个接口可以看成应用层。
————————————————
版权声明:本文为CSDN博主「简单|纯粹」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/XHL9434826546/article/details/126097374

posted @ 2022-08-27 16:24  eastgeneral  阅读(2478)  评论(0编辑  收藏  举报