1 串口通信数据帧
1.1 背景
不同的设备间建立连接往往需要通信,而串口通信是十分常用的一种。UART串口通信需要两根线来实现,一根用于串口发送,另外一更用于串口接收。UART串口发送或者接收过程中一帧数据包括1位起始位、8位数据位、1位停止位,为了提高数据的可靠性可以在停止位前加上1位奇偶校验位。
串口通信虽然十分简单,但是在不同设备间发送的数据往往不止1个字节,往往需要多个字节组成的数据包。当我们按照数据包发送时我们需要考虑到数据同步以及错误校验,因此我们可以采用定义数据帧的方式解决上述两个问题。
数据同步: 便于让接收机从数据包中提取出有效数据部分,对于数据位确定的数据帧我们可以定义帧头和帧尾就好,对于数据位不确定的数据帧我们需要在数据帧里面添加一个字节表示帧长,本文采样加入帧长的方法。
错误校验: 引入CRC校验码对数据帧的数据有效性进行校验。
1.2 发送部分
本文采用的数据帧为帧头+帧长+命令字+数据+校验码+帧尾的格式,如下所示:
帧头 | 数据长度 | 命令 | 数据 | CRC校验 | 帧尾 |
---|---|---|---|---|---|
0xAF,0xFA | XX | XX | XX | XX | 0XFF |
2字节 | 1字节 | 1字节 | n字节 | 2字节 | 1字节 |
- 帧头:帧头2个字节,如果接收到帧头代表数据包已经到来。
- 数据长度:1个字节,用于确定传输的数据为多少个字节数。
- 命令:1个字节,用户可以自定义,如0设定左电机的转速,1设定右电机的转速。
- 数据:n个字节,用户想要传输的数据。
- CRC校验:2个字节,由CRC校验函数生成,用来检测数据的可靠性。
- 帧尾:1个字节,如果接收到,代表数据传输完成。
1.2.1 发送实现
void Send_Data(USART_TypeDef* USARTx,const uint8_t *data,uint8_t len)
{
uint8_t i = 0;
for(i=0;i<len;i++)
{
USART_SendByte(USARTx,data[i]);
}
}
Send_Data(USART_TypeDef* USARTx,const uint8_t *data,uint8_t len)函数内第一个参数表示选择哪一个串口,第二个参数表示需要发送的数据,第三个参数表示发送数据的长度。
static 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;
}
CRC16_Check(const uint8_t *data,uint8_t len)函数用来生成CRC校验码,内部调用可以不用管。
void Encode_send(USART_TypeDef* USARTx,uint8_t cmd,const uint8_t *datas,uint8_t len)
{
uint8_t buf[300],i,cnt=0;
uint16_t crc16;
buf[cnt++] = 0xAF;
buf[cnt++] = 0XFA;
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>>8;
buf[cnt++] = crc16&0xFF;
buf[cnt++] = 0xFF;
Send_Data(USARTx,buf,cnt);
}
Encode_send(USART_TypeDef* USARTx,uint8_t cmd,const uint8_t *datas,uint8_t len)函数表示按照我们规定的数据帧编码,最后通过Send_Data(USARTx,buf,cnt)将数据发送出去。函数的第一个参数用来选择串口,第二个参数表示需要发送的数据,第三个数据表示发送数据的长度。
1.3 接收部分
void Encode_Handle(uint8_t cmd,const uint8_t *datas,uint8_t len)
{
//根据需要处理数据
}
Encode_Handle(uint8_t cmd,const uint8_t *datas,uint8_t len)函数表示我们接收完这一包数据对该数据处理函数,可以在函数内添加想执行的内容。
void Encode_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== 0xAF)
{
step++;
cnt = 0;
Buf[cnt++] = bytedata;
}break;
case 1://接收帧头2状态
if(bytedata== 0xFA)
{
step++;
Buf[cnt++] = bytedata;
}
else if(bytedata== 0XAF)
{
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 == 0xAF)
{
step = 1;
}
else
{
step = 0;
}
break;
case 7://接收帧尾
if(bytedata== 0xFF)//帧尾接收正确
{
Encode_Handle(cmd,data_ptr,len);//数据解析
step = 0;
}
else if(bytedata == 0xAF)
{
step = 1;
}
else
{
step = 0;
}
break;
default:step=0;break;//多余状态,正常情况下不可能出现
}
}
Encode_Receive(uint8_t bytedata)函数利用状态机接收发送端的数据,提高了数据的可靠性,在接收完数据即case 7内,我们调用了Encode_Handle(uint8_t cmd,const uint8_t *datas,uint8_t len)实现对数据的分析处理。
1.3.1
在执行函数中实现将数据发给串口打印的功能,代码如下:
void Encode_Handle(uint8_t cmd,const uint8_t *datas,uint8_t len)
{
//根据需要处理数据
for(uint8_t i=0;i<len;i++)
{
USART_SendByte(USART1,datas[i]);
}
}
利用串口调试助手发送AF FA 04 01 78 87 12 21 49 08 FF 数据包,可以看到接收窗口显示我们发送的数据。