手动阀

Good Luck To You!

串口通信中的数据帧是如何构建和解析的?

串口通信中,数据帧是数据传输的基本单元。

串口通信是一种异步通信方式,收发双方约定好通信速率,通过两根数据线即可实现简单的时序全双工数据收发,最常用的串口通信协议由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; //重新接收数据帧
            }
        }
    }
}

小伙伴们,上文介绍了“串口通信 数据帧”的内容,你了解清楚吗?希望对你有所帮助,任何问题可以给我留言,让我们下期再见吧。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

Powered By Z-BlogPHP 1.7.3

Copyright Your WebSite.Some Rights Reserved.