3.1-上位机与下位机私有协议设计与封包设计(CRC校验算法)

本文介绍了上位机与下位机私有通信协议设计,包括封包概念、协议设计、封包设计及CRC校验算法的重要性。封包由包头、包尾、包长、命令码等组成,CRC校验码用于验证数据完整性,确保通信安全。

一、前言

通信协议的定制相当于上位机与下位机之间的桥梁,因此协议定制对于上位机开发与下位机开发非常重要,非常关键。至与提到的协议定制,那么上位机与下位机直接的通信有没有标准的协议可以参考呢?或者有没有现成的库可以直接使用呢?答案是否定的。即便是有也未必适合你。因为对于不同的上位机与下位机,有不同的通信方式、不同的数据特点、对传输速度要求不同、对传输稳定性要求不同等,从而导致没有一个唯一的通信协议标准。因此总结为一句话就是,没有最标准的通信协议,只有更适合你需求的通信协议。既然没有一个标准的通信协议,那么就要求开发团队,在开发的时候定制适合需求的通信协议。

二、相关名词定义

既然谈到定制通信协议,那么就来了解一下有关通信协议定制的名词吧。

  1. 封包:
    在数据通信中数据并不是直接传送,而是在数据之前或之后增加一些识别头和尾,从而把数据和数据之间分开,形成单独的数据块。从而方便发生端和接收端识别或判断一组数据是否完整传递。把这种数据包起来的动作称作“封包”。
  2. 通信协议(称作“通信接口API”更准确):
    通信协议是指双方实体完成通信或服务所必须遵循的规则和约定。比如,协议规定上位机给下位发送A(表示向下位机获取数据),下位机在收到字母A后规定给上位机返回字母B(返回给上位机的数据)。这就是一条最简单的协议。
    这里的“字母A”和“字母B”就是需要被传递的数据,在上位机与下位机通信中,常常把这个数据进行“封包”后,然后交给通信层来具体传输。

3.关于封包用到的名字定义

  • 包头:封包的起始识别符号,比如 0xFF
  • 包尾:封包的结束识别符号,比如 0xBB
  • 包长:指封包整体的字节数,可以定义为整个封包的长度,也可以定义为除去包头和包尾的字节数
  • 校验码:用于验证封包数据被接收端收到后,封包内容(数据)有没有被意外修改过。通常为2个字节
  • 数据正文:指封包内储存的真正的数据内容

三、协议设计

协议设计(通信接口API)是指设计上位机与下位之间的通信命令格式与命令功能设计。

  1. 协议名词定义
  • 命令类型码:
    指命令的类型,根据需求自定义(设置命令、数据获取命令、下位主动上报型命令)
  • 命令码:
    命令的代码,为了减少数据包的大小,一般用一个代号指代一条命令的名字
  • 命令参数:
    辅助描述命令的一些参数,长度不限,当然不能超出封包的最大长度,常用于设置命令的设置参数数据
  • 反馈命令码:
    下位机收到上位的命令后,给上位的反馈码。用于完成上位与下位机的通信确认机制
  • 反馈命令参数:
    辅助描述反馈命令的一些参数,长度不限,当然不能超出封包的最大长度,常用于向上位机返回数据
  1. 命令设计举例
    注:一般把封包格式和此命令表写成一个文档,就是“上位机与下位机通信协议文档”
  • 设置命令举例
命令类型码命令码命令参数说明
010130 31 32给下位机发送字母的指令命令代号为 01,参数为‘0’,‘1’,‘2’
命令类型码反馈命令码反馈命令参数说明
0101对指令的反馈指令,用于告诉上位命令收到了
  • 数据获取命令举例
命令类型码命令码命令参数说明
0201发出获取温度的命令 命令代号为 01
命令类型码反馈命令码反馈命令参数说明
020138 30 30返回温度为 800度,以字符格式返回
  • 下位主动上报命令举例
命令类型码命令码命令参数说明
03016572726f72206d7367下位机向上位机报警,报警代号为 01,参数为“error msg”
命令类型码反馈命令码反馈命令参数说明
0301上位机收到报警后的反馈命令

四、封包设计

  1. 为啥要设计封包
    如果对modbus熟悉的同学,一般会习惯性的以时间来做为分包的间隔。只要超出3.5个字节的时间没有收到数据,就认为是一个新的封包。这里稍作解释。对于modbus来说,上位机和下位机之间都是由硬件实现,其硬件都专心只做一件事情(相对于电脑来说),且都使用有线通信,拥有稳定的数据传输通道。因此可以使用时间间隔作为封包的间隔。但是我们现在所描述的协议是主要用于下位机硬件和上位机机软件之间的通信,其通信方式有除了“有线通信”之外,还有“无线通信”。而无线通信本身并不具有实时性。因此就不可能使用时间间隔来作为协议封包的间隔了。
    开发“无线网络”和“蓝牙”这些通信的时候,脑中要有这样一个概念:“网络通信是有不确定的延时的”。也正是因为这个原因,我们才设计了这套通信构架。

  2. 结合实际需求设计出你想要的封包格式,一般封包由如下几个部分组成

  • 命令码,参数
    这是一个封包必定包含的部分。
  • 包头,包尾
    包头和包尾可以确确定一个包的开始和结束。是否需要使用包头和包尾可以通过判断你是否需要“消息边界”来确定。
    那什么是保护消息边界呢?消息边界是指一包数据通过传输通道传输,接收方可以独立的收到这一包数据。这就叫做有消息边界。而对于面向流的传输通道的则不存在消息边界。在面向流的传输通道中,会把多个发送包数据合并成一个大包发送给接收方。接收方收到的一个大包数据其实是包含多个子包和不完整子包的数据。造成接收端无法把一个大包拆成原先小包的样子。也就是说面向流的传输通常中的数据是不间断的就像水流一样不断的流过去。因此称作“面向流的通信通道”。常见的流式通信协议有TCP通信和串口通信。常见的非流式通协议有UDP网络通信。
    对于开发中使用到了面向流的通信方式,但是根据需求想要实现把数据流拆成对应的子包,就需要给通信数据加入“消息边界”,而在开发中的消息边界就是“包头”和“包尾”。
    结论:
    面向流的通信:想要实现自定义的通信协议,肯定需要“包头”和“包尾”。
    非面向流的通信:可以不用“包头”和“包尾”,用上也可以。一般为了编辑稳定和方便,都会加入“包头”和“包尾”。
  • 校验码
    校验码是为了给接收端来验证收到的数据包是否正确无误的一个码。因此根据你的需求,你可以选择使用校验码也可以选择不用校验码。如果你的通信通道的通信不是特别稳定,就建议使用校验码。比如说在无线通信中,比如2.4G无线通信或蓝牙通信中,就建议在封包中加入校验码。如果你的通信通道很稳定且你需要加快封包的解析速度则可以去掉封包中的校验码。常用的校验码生成算法是“CRC校验码”。
  • 包长
    包长用于记录封包的长度。一般用于辅助包头和包尾来严格的实现“消息边界”。包长的另一个功能是验证收到的数据包的完整性。根据实际需求选用。
  1. 封包举例
    CRC校验码 = [命令类型码 + 命令码 + 命令参数]
    一个完整封包 = [包头 + 包长 + 命令类型码 + 命令码 + 命令参数 + CRC校验码 + 包尾]
    包头:FF
    包尾:EE FC FF EA
    包长:0A (除去包头和包尾的长度)
    命令类型码:01
    命令码:08
    命令参数:31 32 33
    CRC校验码:E32F
    注:方括号和空格只为格式化显示,并不实际包含在封包中。
  • 指令码(上位机发给下位机的封包)
    [FF 0A 01 08 31 32 33 E32F EE FC FF EA]
  • 反馈码(下位机收到指令后给上位反馈的封包)
    [FF 05 01 08 3A2F EE FC FF EA]

五、协议的安全性

注意这里只考虑,在基础通信正常的情况下,即不丢失任何通信数据的基础上,来探讨按照包头和包尾来分割数据的安全性。如果基础通信层,已经出现了丢失数据的情况,再讨论分割数据安全性没有意义。基础通信层丢失数据的情况,协议层一般做丢弃不完整数据包的处理,协议层还有失败重传机制,与多次重传失败报错机制。

  1. 包头被识别错分析:
    在基础通信层不丢失数据的情况下,并且之前没有发生过包尾识别错误的情况下,包头总是第一个被封包识别代码识别,因此不存在包头被正文数据混淆的情况。

  2. 包尾被识别错分析:
    识别包尾之前,封包识别代码先要过滤正文数据,因此非常容易出现把正文数据识别为包尾的情况,从而造成数据丢失,而且还会导致之后的包头识别被这次识别失败遗漏数据干扰。因此包尾识别错误是非常严重的,会导致通信的严重问题。

  3. 解决包尾识别错误的方法:
    通过以上分析只要解决包尾识别错误,即可避免协议封包识别错误。那么解决包尾识别错误的方法如下:
    3.1. 提高包尾的复杂度。比如从一byte的包尾变成4byte的包尾,此方法只能降低包尾被识别错误的概率

  • 包头和包尾的字符尽量选择不和数据正文频繁重复的字符。因此常见的包头和包尾使用“0xff”和“0xED”等
  • 一般要求包尾的字符数比包头的字符数多,比如:包头= “0xff”,包尾= “0xE1 0xFF 0xb2 0xff”
    3.2. 在协议中加入“包长”,即在协议中加入另一维度用来从接收数据缓存中过滤出一个完整的封包。也就是判断一个封包是否完整,不仅仅从包头,包尾来看,还从包的长度来判断。注意在使用包长判断的时候,不要在使用包头和包尾判断后从缓存中截取数据封包后,才使用包长判断。一定要在识别包头后,就开始用包长和包尾2个条件同时来判断是否一个封包结束。也就是说“包长”存在的意义是防止协议层在解析封包的时候,出现封包识别错误的问题。(通常理解包长的时候,仅仅用于验证封包是否完整)。
  1. 担心“传输数据包中的内容”与“整个封包”重叠,而导致命令识别错乱:
    数据封包中的内容与“整个封包”重叠的前提条件是,真正协议的包头丢失,从而在数据内容中查找包头,并且找到一个完整正确的包的条件下,才会发生“重叠”,我们是以“流”式处理数据,在数据不丢失的情况下,即便数据包中的数据内容,完全是一个正确封包的情况下,也不会出现协议解析错误。(也就是说,只有在通信链路上有数据丢失的情况下,协议解析才有一些出问题的概率。从以上协议的严格限制下可知,这个概率非常低。)

六、CRC校验算法

  1. 算法使用说明
  • GetCRC:
    把包长、数据正文、数据参数,放到一个数组中。把数组作为GetCRC的参数并调用GetCRC即可获取一个16bit的CRC校验码。
  • CheckCRC
    收到数据封包后,把封包的包头和包尾去掉,把剩下的部分放到一个数组中,把此数组作为CheckCRC的参数,并调用CheckCRC,即可通过此函数的返回值来判断CRC码是否匹配。
  1. C/C++代码所使用的CRC算法(与以下C#的CRC算法计算结果相同)
    crc.h
class Crc
{
private:
    Crc();
    ~Crc();
public:
    static void GetCRC(quint8 *data, int len, quint16 &crc);
    static bool CheckCRC(quint8 *data, int len);
};

crc.cpp

#include "crc.h"
Crc::Crc() {}
Crc::~Crc() {}

void Crc::GetCRC(quint8 *data, int len, quint16 &crc) {
    quint16 i,j,carry_flag, temp;
    crc = 0xffff;
    for (i = 0; i < len; i++) {
        crc = crc ^ data[i];
        for (j = 0; j < 8; j ++) {
            temp = crc;
            carry_flag = temp & 0x0001;
            crc = crc >> 1;
            if (carry_flag == 1) {
                 crc = crc ^ 0xa001;
            }
         }
    }
}

bool Crc::CheckCRC(quint8 *data, int len) {
    if (len < 2) return false;
    quint16 crcLow = data[len - 1];
    quint16 crcHigh = data[len - 2];
    quint16 crc_received = static_cast<quint16>(crcHigh << 8 | crcLow);
    quint16 crc_new;
    GetCRC(data, len - 2, crc_new);
    if (crc_new == crc_received) {
        return true;
    }
    return false;
}
  1. C#所使用的CRC代码算法(与以上C++的CRC算法计算结果相同)
    crc.cs
class CRC
{
    private CRC() {}
    
    public static byte[] GetCRC(byte[] data) {

        UInt16 i,  j,  carry_flag,  a;
        UInt16 ret = 0xffff;
        int len = data.Length;
        for (i = 0; i < len; i++) {
            ret = (UInt16)( ret ^ data[i]);
            for (j = 0; j < 8; j++) {
                a = ret;
                carry_flag = (UInt16)(a & 0x0001);
                ret = (UInt16)(ret >> 1);
                if (carry_flag == 1) {
                    ret = (UInt16)(ret ^ 0xa001);
                }
            }
        }

        byte[] rett = { 0,0 };
        rett[0] = (byte)((ret & 0xff00) >> 8);//high 8
        rett[1] = (byte)(ret & 0x00ff);//low 8

        return rett;
    }

    public static bool CheckCRC(byte[] data) {
        int len =  data.Length;
        if (len < 2) return false;

        byte crcLow = data[len - 1];
        byte crcHigh = data[len - 2];

        byte[] orgData = data.Take(len - 2).ToArray();//  2:crc
        byte[] crc = GetCRC(orgData);
        
        if (crc.Length < 2) {
            return false;
        }
        if (crcLow == crc[1] && crcHigh == crc[0]) {
            return true;
        }
        return false;
    }
}
  1. C语言所使用的CRC代码算法(与以上C++的CRC算法计算结果相同)
    crc.h
#ifndef APP_CRC_H_
#define APP_CRC_H_

#include "includes.h" //包含变量类型定义

extern void GetCRC(uint8_t *data, int16_t len, uint16_t *crc);
extern bool CheckCRC(uint8_t *data, int16_t len);

#endif //APP_CRC_H_

crc.c

#include "crc.h"

/**
 * @brief	把包长、数据正文、数据参数,放到一个数组中。
 * 			把数组作为GetCRC的参数并调用GetCRC即可获取一个16bit的CRC校验码。
 * @param	data	原始数据数组
 * @param	len		数组长度
 * @retval	crc值 16bit
 */
void GetCRC(uint8_t *data, int16_t len, uint16_t *crc) {
    uint16_t i,j,carry_flag, temp;
    *crc = 0xffff;
    for (i = 0; i < len; i++) {
        *crc = *crc ^ data[i];
        for (j = 0; j < 8; j ++) {
            temp = *crc;
            carry_flag = temp & 0x0001;
            *crc = *crc >> 1;
            if (carry_flag == 1) {
                 *crc = *crc ^ 0xa001;
            }
         }
    }
}

/**
 * @brief	收到数据封包后,把封包的包头和包尾去掉,把剩下的部分放到一个数组中,
 * 			把此数组作为CheckCRC的参数,并调用CheckCRC,
 * 			即可通过此函数的返回值来判断CRC码是否匹配。
 * @param	data	原始数据数组
 * @param	len		数组长度
 * @retval	校验是否正确
 */
bool CheckCRC(uint8_t *data, int16_t len) {
    if (len < 2) return false;
    uint16_t crcLow = data[len - 1];
    uint16_t crcHigh = data[len - 2];
    uint16_t crc_received = (uint16_t)(crcHigh << 8 | crcLow);
    uint16_t crc_new;
    GetCRC(data, len - 2, &crc_new);
    if (crc_new == crc_received) {
        return true;
    }
    return false;
}

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值