Modbus库开发笔记之八:CRC循环冗余校验的研究与实现

时间:2022-03-21 16:45:42

谈到Modbus通讯自然免不了循环冗余校验(CRC),特别是在标准的串行RTU链路上是必不可少的。不仅如此在其他开发中,也经常要用到CRC 算法对各种数据进行校验。这样一来,我们就需要研究一下这个循环冗余校验(CRC)算法。

1CRC简述

循环冗余检查(CRC)是一种数据传输检错功能,对数据进行多项式计算,并将得到的结果附在帧的后面,接收设备也执行类似的算法,以保证数据传输的正确性和完整性。

CRC校验的基本思想是利用线性编码理论,在发送端根据要传送的k位二进制码序列,以一定的规则产生一个校验用的监督码(既CRC码)r位,并附在信息后边,构成一个新的二进制码序列数共(k+r)位,最后发送出去。在接收端,则根据信息码和CRC码之间所遵循的规则进行检验,以确定传送中是否出错。

CRC的本质是模2除法的余数,采用的除数不同,CRC的类型也就不一样。通常,CRC的除数用生成多项式来表示。最常用的CRC码的生成多项式有下面几种:

Modbus库开发笔记之八:CRC循环冗余校验的研究与实现

不一样的生成多项式,所以得到的结果自然也是不一样的。事实上在Modbus通讯中采用的是CRC-16的方式。

2、算法分析

CRC校验码的编码方法是用待发送的二进制数据t(x)除以生成多项式g(x),将最后的余数作为CRC校验码。其实现步骤如下:

设待发送的数据块是m位的二进制多项式t(x),生成多项式为r阶的g(x)。在数据块的末尾添加r个0,数据块的长度增加到m+r位,对应的二进制多项式为 。用生成多项式g(x)去除 ,求得余数为阶数为r-1的二进制多项式y(x)。此二进制多项式y(x)就是t(x)经过生成多项式g(x)编码的CRC校验码。用 以模2的方式减去y(x),得到二进制多项式 。 就是包含了CRC校验码的待发送字符串。

从CRC的编码规则可以看出,CRC编码实际上是将代发送的m位二进制多项式t(x)转换成了可以被g(x)除尽的m+r位二进制多项式,所以解码时可以用接收到的数据去除g(x),如果余数位零,则表示传输过程没有错误;如果余数不为零,则在传输过程中肯定存在错误。许多CRC的硬件解码电路就是按这种方式进行检错的。同时可以看作是由t(x)和CRC校验码的组合,所以解码时将接收到的二进制数据去掉尾部的r位数据,得到的就是原始数据。

实际上,真正的CRC 计算通常与上面描述的还有些不同。这是因为这种最基本的CRC除法存在一个很明显的缺陷,就是数据流的开头添加一些0并不影响最后校验的结果。为了弥补这一缺陷所以引入了两个概念:一个是“余数初始值”,另一个是“结果异或值”。所谓 “余数初始值”就是在计算CRC值前,为存储变量所赋的初值。对应的“结果异或值”就是在计算完成后,将变量值与这个值作最后的异或运算而得到校验结果。

名称

校验和位宽

生成多项式

除数(多项式)

余数初始值

结果异或值

CRC-4

4

x4+x+1

3

CRC-8

8

x8+x5+x4+1

0x31

CRC-8

8

x8+x2+x1+1

0x07

CRC-8

8

x8+x6+x4+x3+x2+x1

0x5E

CRC-12

12

x12+x11+x3+x+1

80F

CRC-16 

16

x16+x15+x2+1

0x8005

0x0000

0x0000

CRC-CCITT

16

x16+x12+x5+1

0x1021

0xFFFF

0x0000

CRC-32

32

x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x1+1

0x04C11DB7

0xFFFFFFFF

0xFFFFFFFF

CRC-32c

32

x32+x28+x27+...+x8+x6+1

1EDC6F41

说到这里我们已经可以描述一下这个算法的实现过程:

第1步:定义CRC存储变量,并给其赋值为“余数初始值”。

第2步:将数据的第一个8-bit字符与CRC存储变量进行异或,并把结果存入CRC存储变量。

第3步:CRC存储变量向右移一位,MSB补零,移出并检查LSB。

第4步:如果LSB为0,重复第三步;若LSB为1,CRC寄存器与0x31相异或。

第5步:重复第3与第4步直到8次移位全部完成。此时一个8-bit数据处理完毕。

第6步:重复第2至第5步直到所有数据全部处理完成。

第7步:最终CRC存储变量的内容与“结果异或值”进行或非操作后即为CRC值。

3、代码实现

有了前面的准备实际上我们要实现CRC校验的代码已经很简单了,实现这一过程有各种方法我们说常用的2种:一是直接计算法,就是按照前面的步骤计算出来;二是驱动表法,就是将一些数据储存起来直接获取计算。因为在Modbus中使用的是CRC-16,所以我们一次为例来实现它。

(1)直接计算法

直接计算法简单直接,便写程序也比较简单,我们以CRC-16为例,其多项式记为0x8005,因为其记过异或值为0x0000,所以可以不添加。具体代码如下:

#define Initial_Value    0x0000

#define EOR 0x0000

#define POLY16 0x8005

uint16_t CRC16(uint8_t *buf,uint16_t length)

{

uint16_t crc16,data,val;

crc16 = Initial_Value;

for(int i=0;i<length;i++)

{

if((i % 8) == 0)

{

data = (*buf++)<<8;

}

val = crc16 ^ data;

crc16 = crc16<<1;

data = data <<1;

if(val&0x8000)

{

crc16 = crc16 ^ POLY16;

}

}

return crc16;

}

(2)驱动表法

对于直接计算法,虽然简单直接,但有时候效率却是个问题,所以在Modbus通讯中我们通常采用驱动表法来实现:

//CRC_16高8位数据区

const uint8_t auchCRCHi[] = {

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

};

//CRC低位字节值表

const uint8_t auchCRCLo[] = {//CRC_16低8位数据区

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

};

/*函数功能:CRC校验码生成

输入参数:puchMsgg是要进行CRC校验的消息,usDataLen是消息中字节数

函数输出:计算出来的CRC校验码

GenerateCRC16CheckCode查表计算函数*/

static uint16_t GenerateCRC16CheckCode(uint8_t *puckMsg,uint8_t usDataLen)

{

uint8_t uchCRCHi = 0xFF ; //高CRC字节初始化

uint8_t uchCRCLo = 0xFF ; //低CRC 字节初始化

uint32_t  uIndex ; //CRC循环中的索引

//传输消息缓冲区

while (usDataLen--)

{

//计算CRC

uIndex = uchCRCLo ^ *puckMsg++ ;

uchCRCLo = uchCRCHi ^ auchCRCHi[uIndex] ;

uchCRCHi = auchCRCLo[uIndex] ;

}

//返回结果,高位在前

return (uchCRCLo << 8 |uchCRCHi) ;

}

4、结束语

CRC的应用非常广泛,特别是在做通讯时更是经常见到,所以掌握它是非常有必要的,至少会使用它。我们在开发Modbus库函数的过程中,对它也不过是有了一些比较粗浅的理解,在此记述以求共进。

若对本文档有兴趣,可已添加如下公众号有更多精彩内容:

Modbus库开发笔记之八:CRC循环冗余校验的研究与实现