串口通信是一种异步通信方式,收发双方约定好通信速率,通过两根数据线即可实现简单的时序全双工数据收发,最常用的串口通信协议由1位起始位、8位数据位和1位停止位组成,总共10位,为了提高通信可靠性,也可在停止位前增加1位奇偶校验位,但这会增加开销,每字节数据需要多传1位二进制数。
在实际使用中,往往需要传输多个字节组成的数据包,而因为串口通信中字节之间相互独立,在接收数据时面临数据包对齐和防止出错的两大问题,为了解决这两个问题,发送端通过将数据按指定格式打包,接收端使用状态机解析数据,实现串口通信可靠传输。
一、发送端实现过程
1. 数据帧格式
帧头:用于标识数据帧的开始位置,通常选择两个字节,例如0xA5, 0x5A,因为它们对应的二进制位0与1的个数相同,分布均匀不易出错。
帧长:描述数据帧实际长度的字节,这里只使用1个字节,故帧长字节最大为255,为提高利用率,规定帧长字节描述的是数据字节的长度。
命令字节:指定数据字节的功能,例如命令字节为1表示传输温度,为2表示传输湿度等。
数据字节:数据字节长度可变,帧长字节为0表示没有数据,帧长字节为255表示有255字节数据。
校验字节:采用CRC16循环冗余校验方式,将校验字节前的所有字加入计算,得到两字节CRC16校验码。
帧尾:标识数据帧的结束,通常使用一个字节,例如0xFF。
2. 实现代码
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); //调用数据帧发送函数将打包好的数据帧发送出去 }
二、接收端实现过程
1. 状态机解析数据
接收端采用状态机解析数据,根据不同的状态切换条件来处理接收到的数据。
状态0:等待接收帧头第1字节0xA5。
状态1:等待接收帧头第2字节0x5A。
状态2:等待接收数据长度字节。
状态3:等待接收命令字节。
状态4:等待接收数据字节。
状态5:等待接收校验字节高8位。
状态6:等待接收校验字节低8位。
状态7:等待接收帧尾字节0xFF。
2. 状态转换关系图
当前状态 | 下一个状态 | 条件 | 操作 |
状态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 |
3. 实现代码
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; //重新接收数据帧 } } } }
小伙伴们,上文介绍了“串口通信 数据帧”的内容,你了解清楚吗?希望对你有所帮助,任何问题可以给我留言,让我们下期再见吧。