串口通信中的数据帧构建与解析详解
串口通信中的数据帧构建与解析详解
串口通信中,数据帧是数据传输的基本单元。
串口通信是一种异步通信方式,收发双方约定好通信速率,通过两根数据线即可实现简单的时序全双工数据收发,最常用的串口通信协议由1位起始位、8位数据位和1位停止位组成,总共10位,为了提高通信可靠性,也可在停止位前增加1位奇偶校验位,但这会增加开销,每字节数据需要多传1位二进制数。
在实际使用中,往往需要传输多个字节组成的数据包,而因为串口通信中字节之间相互独立,在接收数据时面临数据包对齐和防止出错的两大问题,为了解决这两个问题,发送端通过将数据按指定格式打包,接收端使用状态机解析数据,实现串口通信可靠传输。
发送端实现过程
- 数据帧格式
帧头:用于标识数据帧的开始位置,通常选择两个字节,例如0xA5, 0x5A,因为它们对应的二进制位0与1的个数相同,分布均匀不易出错。
帧长:描述数据帧实际长度的字节,这里只使用1个字节,故帧长字节最大为255,为提高利用率,规定帧长字节描述的是数据字节的长度。
命令字节:指定数据字节的功能,例如命令字节为1表示传输温度,为2表示传输湿度等。
数据字节:数据字节长度可变,帧长字节为0表示没有数据,帧长字节为255表示有255字节数据。
校验字节:采用CRC16循环冗余校验方式,将校验字节前的所有字加入计算,得到两字节CRC16校验码。
帧尾:标识数据帧的结束,通常使用一个字节,例如0xFF。
- 实现代码
void Send(const uint8_t *data, uint8_t len) {
uint8_t i;
for (i = 0; i < len; i++) {
SendByte(data[i]); //发送一个字节
}
}
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;
}
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); //调用数据帧发送函数将打包好的数据帧发送出去
}
接收端实现过程
- 状态机解析数据
接收端采用状态机解析数据,根据不同的状态切换条件来处理接收到的数据。
状态0:等待接收帧头第1字节0xA5。
状态1:等待接收帧头第2字节0x5A。
状态2:等待接收数据长度字节。
状态3:等待接收命令字节。
状态4:等待接收数据字节。
状态5:等待接收校验字节高8位。
状态6:等待接收校验字节低8位。
状态7:等待接收帧尾字节0xFF。
- 状态转换关系图
当前状态 下一个状态 条件 操作
状态0 状态1 接收到0xA5 无
状态1 状态2 接收到0x5A 无
状态2 状态3 接收到长度字节 无
状态3 状态4 接收到命令字节且长度字节>0 无
状态4 状态5 接收到n字节数据 无
状态5 状态6 接收到命令字节且长度字节为0 无
状态6 状态7 接收到校验字节高8位 无
状态7 状态1 接收到校验字节低8位 校验正确,接收数据帧成功
状态7 状态0 接收到帧尾字节0xFF 校验错误但本次接收为0xA5
状态7 状态1 接收到非0xA5 校验错误且本次接收为非0xA5
- 实现代码
void Data_Analysis(uint8_t cmd, const uint8_t *datas, uint8_t len) {
//定义数据处理函数用来处理解析成功的数据
}
void UartInit() {
PCON &= 0x7F; //波特率不倍速
SCON = 0x50; //8位数据,可变波特率
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xFD; //设置定时初始值
TH1 = 0xFD; //设置定时重载值
ET1 = 0; //禁止定时器中断
ES = 1; //串口中断打开
TR1 = 1; //定时器1开始计时
}
void ES_timers() interrupt 4 { //接收中断
if (RI) {
RI = 0;
start_timer = 1; //开定时器标志位置1
if (recv_cnt < MAX_REX_NUM) { //在规定字符长度范围内接收数据
recv_buf[recv_cnt] = SBUF; //接收数据
recv_cnt++;
} else {
recv_cnt = MAX_REX_NUM;
}
recv_timer_cnt = 0; //每接收一帧数据就计数清0
}
}
void T0_timer() interrupt 1 { //利用1ms计数,判断是否接收完成
TR0 = 0;
if (start_timer == 1) { //软件定时器打开
recv_timer_cnt++; //计数
if (recv_timer_cnt > 5) { //如果计数超过5ms,则接收完成
recv_timer_cnt = 0;
start_timer = 0; //关闭软件定时器标志位
if (recv_cnt >= 9 && recv_buf[0] == 0x55 && recv_buf[1] == 0xAA) { //判断帧头是否正确
if (recv_buf[recv_cnt 3] == CRC16_Check(recv_buf, recv_cnt 4)) { //校验正确
Data_Analysis(recv_buf[2], &recv_buf[3], recv_buf[2]); //调用数据处理函数处理解析成功的数据
} else { //校验错误
recv_cnt = 0; //重新接收数据帧
}
} else { //帧头或校验错误
recv_cnt = 0; //重新接收数据帧
}
}
}
}