概述:本文详细介绍了CRC循环冗余计算的数学原理,算法中使用的参数说明,并以Modbus协议中的CRC-16算法为例,进行手算验证,同时提供LabVIEW和C语言的直接计算CRC-16 值的代码以及C的查表计算CRC-16代码和代码原理的说明。
一、笔者个人经历
初次接触CRC校验是因为项目需要上位机软件来记录PLC寄存器中的数据,实现PLC控制全过程中关键数据的记录和查询。上位机软件使用LV进行编写,数据的获取通过Modbus TCP实现,因为当时对Modbus和CRC都不是很熟悉,就采用了最成熟简单的办法,直接调用了第三方的Modbus工具包,项目功能也是顺利实现。之后又遇到一个项目,需要上位机作为从机,回应主控的Modbus RTU指令,这次没有选择直接使用Modbus工具包,而是使用《LabVIEW宝典》中Modbus CRC-16校验算法,根据Modbus RTU协议自主编程完成了项目要求。后来因为做嵌入式单片机,又了解使用了CRC-8和CRC-16的查表算法,也是没有详细了解过CRC循环冗余校验的原理,仅仅是可以熟练实现功能。直到后来遇到一个项目,需要用示波器捕捉并分析出未知的通信帧的通信协议,帧头,帧尾很快通过对比分析了出来,通信内容也是反复实验分析出具体数据字节和位的意义,就是校验方式分析不出,但是可以肯定数据帧是一定包含校验字节的,那时才认真开始考虑CRC循环冗余算法,试图找出数据帧的校验规则,很惭愧没有分析出来,后来是通过第三方的帮助才解决了这个问题,但是当时脑中留下了很多问号和片段式的思考及部分无序的笔记,现在重新进行了整理、思考和验证,形成此文,希望可以解答对CRC循环冗余校验算法有疑问的同学的困惑。
二、CRC循环冗余校验原理
百度CRC可以很容易的获取CRC的定义:CRC(Cyclic Redundancy Check),即循环冗余校核,是一种根据网络数据包或电脑文件等数据产生简短固定位数校核码的快速算法,主要用来检测或校核数据传输或者保存后可能出现的错误。CRC利用除法及余数的原理,实现错误侦测的功能,具有原理清晰、实现简单等优点。
首先明确CRC是一种数据校验方法,与和校验、异或校验功能相同,常用于通信的双方判断通信帧在通信过程中数据传输是否正确,如校验不通过则需要考虑舍弃此通信帧,同时需根据需要判断是否需要向发送方反馈通信异常的情况。
1、模2运算
从百度到的CRC定义中可以看到CRC是利用除法和余数的原理,这里所说的除法和余数的原理就是模2算法了,下面是模2算法的百度定义。
模2运算是一种二进制算法,CRC校验技术中的核心部分。与四则运算相同,模2运算也包括模2加法、模2减法、模2乘法、模2除法四种二进制运算。与四则运算不同的是模2运算不考虑进位和借位,模2算术是编码理论中多项式运算的基础。模2算术在其他数字领域中的应用也是很广泛的。
这里可以得出模2算法是不考虑进位和借位的,也就可以理解为每位都是独立的,不会影响其他位也不会被其他位影响,这点在后文中的计算验证部分有体现。
下面是模2运算的四则运算法则:
0+0=0;0+1=1;1+0=1;1+1=0;
0-0 =0;0-1=1;1-0=1;1-1=0;
0 *0=0;0 *1=0;1*0=0;1*1=1;
0/1=0; 1/1=1;
CRC算法中主要使用到的就是模2减法和模2除法。通过上述模2减法法则可以发现,模2减法实际和异或运算在结果上是完全相同的,也就不再过多描述。这里最关键的是多位二进制的除法,这是CRC校验的核心部分。模2除法具有以下性质:
(1)每一位除的结果不影响其他位,即不向上一位借位;
(2)当被除数的位数小于除数位数时,则商为0,被除数就是余数,也可以理解为被除数首位为0时,商为0。
(3)只要被除数的位数和除数一样多,且最高位为1,不管其他位是什么数,皆可商1,也可以理解为被除数首位为1,商为1,余数为被除数与除数的模2减的结果;
(4)要保证每次除完首位都为0,才能进行右移;
(5)当最后余数的位数小于除数位数时,除法停止。
通过对上述多位二进制的模2除法性质的思考,我们可以感觉到模2除与循环异或的本质是相同的,这个可以通过下文中具体的计算过程体现。
2、CRC算法参数
这里给大家推荐一个很好用的CRC计算工具:
,这个计算工具包含了很多种CRC算法,并且标示出了具体的关键参数,从我个人的使用经历上来说,需要注意的就仅仅是CRC-16 Modbus的计算结果是高字节在前,低字节在后的,这个与实际使用中通常低字节在前高字节在后不同,需要注意一下。下面我们就以CRC-16 Modbus为例对CRC算法进行说明。
(1)标准CRC生成多项式
每种CRC算法的标准生成多项式可能是不同的,这个需要进行注意。从上面截图的左下方我们可以得知,CRC-16 Modbus的标准生成多项式是X16+X15+X2+1,其中1可以换成X0,这样就很容易可以看出,16、15、2、0这些数字代表的是多位二进制数的数位,则标准生成多项式可写为1 1000 0000 0000 0101,对应十六进制就是0x18005,也就是对应上图右侧的Poly:0x8005。这里的0x8005实际是标准生成多项式的简记式,因为标准生成多项式的最高位固定为1,故在简记式中就忽略了最高位1了,同时在程序编程中实际使用的也是简记式,这个在下文的程序部分有所体现。
(2)CRC初始值
初始值,这个也是根据具体哪种CRC标准来确定的,不同的CRC标准对应不同的计算初始值。还是以CRC-16 Modbus为例,计算初始值就是0xFFFF,对应上图Init:0xFFFF。计算初始值先与需要校验的数据的首字节数据进行异或,异或后结果进行模2除法运算,这个后续程序部分会体现。
(3)正序/反序
就像串口通信需要考虑低位先传还是高位先传一样,循环冗余计算时也需要考虑从高位开始还是低位开始,即编程时需要考虑数据进行左移还是右移。需要说明的一点是,数据进行正序或者反序,最后的结果是不相同的,这个下文也会进行验证说明。上图计算工具中是通过RefIn和RefOut来进行体现的。
RefIn:true或false表示在进行计算之前,原始数据是否翻转,如原始数据:0x34 = 0011 0100,如果REFIN为true,进行翻转之后为0010 1100 = 0x2C。
REFOUT:true或false表示运算完成之后,得到的CRC值是否进行翻转,如计算得到的CRC值:0x97 = 1001 0111,如果REFOUT为true,进行翻转之后为1110 1001 = 0xE9。
以CRC-16 Modbus为例,都是True,则表示反序循环冗余校验。这个结合下文程序会更好理解,下文也会进行相应的说明。
(4)CRC结果异或值
CRC结果异或值就是CRC循环冗余计算的结果与CRC结果异或值进行异或处理,结果为CRC计算的最终值,对应上图中的XorOut:0x0000。
3、手算CRC算法及验证
前面已经介绍了模2算法以及CRC算法的参数,下面就来验证一下上面的理论是否正确。还是以CRC-16 Modbus为例,对单字节0x12数据计算校验值。
首先需要确定CRC-16 Modbus算法的参数:
(1)标准生成多项式为X16+X15+X2+1,转换成二进制则为1 1000 0000 0000 0101;
(2)初始值为0xFFFF;
(3)采用反序的计算顺序;
(4)CRC结果异或值为0x0000;
然后就按照CRC-16 Modbus算法来进行计算,
0x12转为二进制为0001 0010;
与0xFFFF即1111 1111 1111 1111异或,结果为1111 1111 1110 1101;
反序为1011 0111 1111 1111;
下面进行模2除,被除数为1011 0111 1111 1111 0000 0000,除数为1 1000 0000 0000 0101,因为通信帧的基本单位是字节,所以被除数为1011 0111 1111 1111后面加8个0。
模2除余数为1111 1100 1011 0010;(这里需要注意一下,CRC循环冗余算法中关注的是模2除的余数,而不是商)
反序为0100 1101 0011 1111,即0x4D3F;
结果再与0x0000进行异或,最终结果为0x4D3F。
利用上面介绍的CRC计算小工具进行验证,结果如下图所示。
同理,笔者针对不同标准生成多项式进行手算实验,选择了CRC-32 对0x12进行CRC校验,手算结果与CRC计算工具结果相同。
同时针对数据的正序反序问题,选择CRC-8对0x12进行CRC校验,手算结果与CRC计算工具结果相同。
对于多字节数据的校验过程是:首先首字节与初始默认值进行异或校验,结果作为被除数进行模2除法;下一个字节与上一个字节的模2除法的结果进行异或,作为下一次模2除法的被除数;以此类推,这个在下文代码部分体现。
笔者针对多字节也进行了手算实验,选择了CRC-16 Modbus对0x12、0x34两个字节进行了CRC校验,手算结果与CRC计算工具结果相同。
三、CRC 编程实现方法
1、直接计算法
CRC算法这里还是以CRC-16 Modbus为例,其直接计算编程实现过程为:
1)设置CRC寄存器,并给其赋值0xFFFF。
2)将数据的第一个8-bit字符(将此8位高位补0为16位)与16位CRC寄存器的值进行异或,并把结果存入CRC寄存器。
3)CRC寄存器向右移(即最低位方向)一位,MSB补零,移出并检查LSB。
4)如果LSB为0,重复第三步;若LSB为1,CRC寄存器与多项式码(0xA001)相异或。此时的0xA001即为0x8005的反序。
注意:该步检查LSB应该是右移前的LSB,即第3步前的LSB。
5)重复第3与第4步直到8次移位全部完成。此时一个8-bit数据处理完毕。
6)重复第2至第5步直到所有数据全部处理完成。
7)最终CRC寄存器的内容即为CRC值。
这里对上文中提及的标准多项式和简记式的区别再进行一下说明:
在上文中手算部分可以看到实际标准多项式最高位对应被除数的那一位必定是1,与标准多项式最高位异或的结果或者说模2减的结果必定是0,因此,在步骤4)就是仅判断LSB是1还是0,进而确定是先进行异或再移位还是直接进行移位,而不参与异或运算,同时也可以理解上述步骤中仅涉及简记式进行异或或者说模2减。
读者可以结合上文中的手算部分的运算步骤来理解这里的处理步骤,关键是理解模2除和数据右移的关系,笔者这里不再进行过多说明。
LabVIEW直接计算 CRC-16 Modbus:
程序中while循环实际就是模2除法的体现,以下程序同理。
C语言直接计算CRC-16 Modbus:
unsigned short do_crc(unsigned char *ptr, int len)
{
unsigned char i;
unsigned int crc16 = 0xFFFF;
while(len--)
{
crc16 ^= *ptr++;
for (i = 0; i < 8; ++i)
{
if (crc16 &0x0001)
crc16 = (crc16 >> 0x01) ^ 0xA001;
else
crc16 = (crc16 >> 0x01);
}
}
return crc16;
}
这里有一点需要注意的是,返回的CRC16为16位数,分为两个字节,高低字节需转换(仅针对Modbus,因为modbus一般要求校验值低位在前高位在后)。
2、查表法
对于查表法,其实就是利用空间换时间,通过直接查询CRC运算结果表格来减少计算时间,这个在嵌入式单片机方面使用较多,这边还是以CRC-16 Modbus为例。
首先针对表格进行说明:
因为标准生成多项式为X16+X15+X2+1,则可以确定校验结果为2字节,因此表格分为高字节和低字节,高低位CRC数组中(即下面的 Table_CRCL[256] 和 Table_CRCH[256]中 )同下标的两个单字节数组合成一个双字节校验值。
关于下面表格中数值,是使用CRC-16,(标准生成多项式为X16+X15+X2+1;初始值0x0000;RefIn:True,RefOut:True,即反序;XorOut:0x0000)计算的0-255的CRC值。例如0x01按照上述CRC算法,结果是0xC0C1,即对应Table_CRCH[1]=0xC0和Table_CRCL[1]=0xC1。这里读者可能有疑问,为什么不是使用CRC-16 Modbus的算法?对比CRC-16 Modbus和上述算法的参数,可以看到仅仅是初始值不同,CRC-16 Modbus 初始值为0xFFFF。这里先明确,表的意义是取代模2除法的计算过程。
再回到之前的手算部分,即如下所示。
从上式可以发现,被除数从左边起,8位及之后的7位,即1111 1111,这些数位仅参与异或且全程参与异或,或者说模2减,而异或是符合交换律的,即A^B^C=A^C^B,则1111 1111也可最后再参与异或,而0x00与任何单字节数异或均为单字节数本身,则上式被除数可以改成1011 0111 0000 0000 0000 0000 ,最后的余数低字节再与原被除数高字节异或,得到的数就是符合CRC-16 Modbus算法的最终的低字节值,也就是说原被除数高字节的值仅影响模2除余数的低字节值,这也就是下文代码部分的解释。
C语言查表计算CRC-16 Modbus:
const uint8_t Table_CRCL[256] = // CRC 高位字节值表
{
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40
};
const uint8_t Table_CRCH[256] = // CRC高位位字节值表
{
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,
0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,
0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,
0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,
0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,
0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,
0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,
0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,
0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,
0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,
0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,
0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,
0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,
0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,
0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,
0x43, 0x83, 0x41, 0x81, 0x80, 0x40
};
uint16_t CRC16(uint8_t *puchMsg, uint8_t DataLen)
{
uint8_t CRCH= 0xFF ; // 高CRC字节初始化
uint8_t CRCL = 0xFF ; // 低CRC 字节初始化
uint8_t Index ; // CRC表中的索引
while (DataLen--)
{
Index = CRCL ^ *puchMsg++ ;
CRCL = CRCH ^ CRCL[Index];
CRCH = CRCH[Index];
}
return ((uint16_t)CRCL<< 8 | CRCH); // Modbus校验值一般低字节在前,高字节在后
}
四、总结
本文选择以CRC-16 Modbus算法标准进行了详细的举例及手算验证,同时笔者也对比其他算法,如CRC-8,CRC-32进行了针对性的验证,结果均证明正确。之所以选择CRC-16 Modbus,感觉这个可能是大家平时使用中较为常用的,尤其是工控领域,同时也相信读者举一反三的能力,可以按照本文介绍方法掌握其他CRC算法。本文编写期间也是对文字,计算及代码反复考量验证,力求正确性和逻辑性,转发及引用请注明出处。
——文章来自宋雨的个人分享
审核编辑:汤梓红
-
LabVIEW
+关注
关注
1970文章
3654浏览量
323396 -
crc
+关注
关注
0文章
199浏览量
29461 -
C语言
+关注
关注
180文章
7604浏览量
136723 -
代码
+关注
关注
30文章
4781浏览量
68542 -
循环冗余校验
+关注
关注
0文章
7浏览量
6540
发布评论请先 登录
相关推荐
评论