这里提一下各种通讯协议都经常使用的CRC校验,为后面的知识做铺垫。
一、CRC分析
我只是把网上能搜到的资料根据自己的理解重新写了一遍,本文大部分内容都来自于下面的两位博主:
wchonline , CRC校验算法详解
偏执灬 , CRC校验算法原理分析
1.1 数据校验
数据在传输过程(比如通过网线在两台计算机间传文件)中,由于传输信道的原因,可能会有误码现象(比如说发送数字5但接收方收到的却是6),如何发现误码呢?方法是发送额外的数据让接收方校验是否正确,这就是数据校验。
最容易想到的校验方法是和校验,就是将传送的数据(按字节方式)加起来计算出数据的总和,并将总和传给接收方,接收方收到数据后也计算总和,并与收到的总和比较看是否相同。如果传输中出现误码,那么总和一般不会相同,从而知道有误码产生,可以让发送方再发送一遍数据。
1.2 CRC校验原理
CRC(Cyclic Redundancy Check)循环冗余校验是常用的数据校验方法。CRC校验码的基本思想是利用线性编码理论, 在发送端根据要传送的k位二进制码序列,以一定的规则产生一个校验用的监督码(既CRC码)r位,并附在信息后边,构成一个新的二进制码序列数共(k+r)位,最后发送出去。在接收端,则根据信息码和CRC码之间所遵循的规则进行检验,以确定传送中是否出错,在数据存储和数据通讯领域常用。
1.3 CRC多项式(理解就行)
一般在数据传输之前,发送端与接收端会相互约定好一个除数(也是一个二进制序列,用来进行模2除法)。这个除数就是生成多项式。
Ps: 这个多项式的最高位和最低位必须为1
生成多项式的选择是 CRC 算法实现中最重要的部分,所选择的多项式必须有最大的错误检测能力,同时保证总体的碰撞概率最小。多项式最重要的属性是它的长度,也就是最高非零系数的数值,因为它直接影响着计算的校验和的长度。
在构建一个新的 CRC 多项式或者改进现有的 CRC 时,一个通用的数学原则是使用满足所有模运算不可分解多项式约束条件的多项式。
- 生成多项式的最高位和最低位必须为1。
- 当被传送信息(CRC码)任何一位发生错误时,被生成多项式做模2除后,应该使余数不为0。
- 不同位发生错误时,应该使余数不同。
- 对余数继续做模2除,应使余数循环。
1.4 CRC校验简单理解(要点)
在数据传输之前,发送方与接收方会相互约定好一个除数(多项式,进行模2算法)。
- 发送方:生成CRC校验码。CRC校验码就是将数据除以某个固定的数(比如ANSI-CRC16中,这个数是0x18005),所得到的余数就是CRC校验码。
- 接收方:接收到数据包+CRC校验码。由于CRC校验码是之前发送方求余出来的数据,将数据包+CRC校验码合并后再进行模2除法校验,理论上余数为0。如果是余数不为0,说明了传输的数据错误。
- 因此,不同协议的CRC校验码基本不一样,因为约定的除数会根据自己协议制定(例如Modbus通讯)。
如果被除数比除数小,那么余数就是被除数本身,比如说只要传一个字节,那么它的CRC就是它自己,为避免这种情况,在做除法之前先将它移位,使它大于除数,那么移多少位呢?这就与所选的固定除数有关了,左移位数比除数的位数少1。
关于模2除法中除数的选择,这个可以自己随意选择。但由 1.3 可知,随意选择的除数会导致帧校验的
正确率下降,这是不确定的,要看你选择的除数。而我们一般的除数的选择是直接去参照一些专家讲过
多次试验下来的一些除数,这些除法能极大的保证帧校验的正确率。
一般而言,crc8校验的错误率为1/256(最小单元),crc16校验的错误率为1/65536(最小单元),crc32校验的错误率为1/2^32(最小单元)。CRC16的错误率已经是非常小了,所以一般在数据不多的情况下用CRC16校验就可以了,而在整个文件的校验中一般用CRC32校验。
下面是常用标准中的除数:
通用CRC多项式
由1.3上面要求罗列出常用的多项式如下:
- CRC8-Dallas/Maxim:多项式是X8+X5+X4+1,对应的数字是0x131,左移8位
- CRC8-ATM:多项式是 X8+X2+X1+1,对应的数字是0x107(0x07),左移8位
- CRC8-CCITT:多项式是 X8+X6+X4+X3+X2+X1,对应的数字是0x15E(0x5E),左移8位
- CRC12:多项式是 X12+X11+X3+X2+1,对应的数字是0x180D,左移12位
- CRC16-CCITT:多项式是 X16+X12+X5+1,对应的数字是0x11021,左移16位
- CRC16-ANSI :多项式是 X16+X15+X2+1,对应的数字是0x18005,左移16位
- CRC32:多项式是X32+X26+X23+X22+X16+X12+X11+X10+X8+X7+X5+X4+X2+X1+1,对应数字是0x104C11DB7,左移32位
因此,在得到字节串对应的数字后,再将数字左移M位(比如ANSI-CRC16是左移16位),就得到了被除数。
Ps: 由于多项式的最高为都为1,但在代码实现的crc计算中,最高位是不使用的;使用的是上面例子中括号内的值
从上面可以看到,即使是同为CRC8校验,多项式也有多种;因此计算CRC校验要格外注意多项式是否相同。不同的多项式,起检错能力是不同的。例如Modbus协议就规定 0xA001 就是它CRC校验的多项式。
二、代码实现
算术上的除法,计算机当然会做,但因为减法有借位,很耗时间和指令!所以,计算CRC是除法,但是用XOR来代替减法。除法(XOR)的目的是逐步消掉二进制数最高位的1或0!由于过程是XOR的,所以商是没有意义的,我们不要。我们要的是余数。
2.1 顺序异或 & 反序异或
CRC校验算法,就是把需要校验的数据与多项式进行循环异或(XOR),但进行XOR的方式与实际中数据传输时,是高位先传、还是低位先传有关。对于数据高位先传的方式,XOR从数据的高位开始,我们就叫它顺序异或吧;对于数据低位先传的方式,XOR从数据的低位开始,我们就叫它反序异或吧。两种不同的异或方式,即使对应相同的多项式,计算出来的结果也是不一样的。
比如前面ccitt-crc16的正序是0x1021,如果是反转就是0x8408(就是将0x1021倒过来低位变高位)。数据传输可能是先传低位再传高位(比如串口就是低位在前高位在后)。反转的CRC算法与正序类似,只是需要注意移位的方向相反。
这个问题一般不是问题。因为如果是自己写的发送方,当然是可以随便写!(开玩笑的)。如果是写接收方,发送方要么会给详细说明,或者有源码,直接搬迁过来就行了。
2.2 代码例子
1 | unsigned char cal_table_high_first(unsigned char value) |
2.3 CRC校验表
2.2的代码计算一个字节的crc结果,如果计算多个字节的结果也是比较简单的,先计算第一个字节的crc结果,然后把第一个字节的crc结果与第二个字节进行异或,异或后的值再进行一次crc计算就可以了,多个字节也是反复这过程就好。
但是需要的运行量也是不少的,每一个字节都需要进行8次判断、移位、或异或操作。但是其实每个字节异或的结果是一定的,那就可以事先算好的CRC存入数组。采用查表法,大大减少计算量,先计算出 0x00~0xFF 每一个字节的crc校验结果,后面就可以通过表来查出每个字节的crc结果,大大 减少计算量。
下面是一个表生成程序:
1 | void create_crc_table(void) |
得到的表整理如下:
1 | static const unsigned char crc_table[] = |
采用查表法计算crc代码如下:
1 | unsigned char cal_crc_table(unsigned char *ptr, unsigned char len) |
2.4 反序异或计算的代码
反序异或与顺序异或差异在数据先判断最低位,并且数据是向右移的,并且多项式数据位需要高低位反转一下。还是以多项式:x8+x5+x4+1(二进制为:100110001)为例:则计算一个字节的crc校验代码如下:
1 | unsigned char cal_table_low_first(unsigned char value) |
三、例程:Modbus的CRC16校验
代码如下:
1 | static const UCHAR aucCRCHi[] = { |