3.4.2 CRC校验

在ModbusRTU传输模式下,通信报文(帧)包括一个基于循环冗余校验(CRC)方法的差错校验字段。

CRC的全称是循环冗余校验,其特点是检错能力极强,开销小,易于用编码器及检测电路实现。从其检错能力来看,它不能发现的错误的几率在0.0047%以下,在Modbus通信中基本可以忽略。CRC校验包括多个版本,常用的CRC校验有CRC-8、CRC-12、CRC-16、CRC-CCITT、CRC-32等。

从性能上和开销上考虑,CRC校验均远远优于奇偶校验及算术和校验等方式,因而在数据存储和数据通信领域,CRC无处不在。例如,著名的通信协议X.25的FCS(帧检错序列)采用的是CRC-CCITT,而WinRAR、NERO、ARJ、LHA等压缩工具软件采用的是CRC-32,磁盘驱动器的读写则采用了CRC-16,通用的图像存储格式GIF、TIFF等也都用CRC作为检错手段。

而Modbus协议中,则采用了CRC-16标准校验方法。在RTU模式下,CRC自身由两个字节组成,即CRC是一个16位的值。CRC字段校验整个报文的内容,无论报文中的单个字节采用何种奇偶校验方式,整个通信报文均可应用CRC-16校验算法。CRC字段作为报文的最后字段添加到整个报文末尾。

有一点需要注意,因为CRC-16由两个字节构成,所以涉及哪个字节放在前面,哪个字节放在后面传输的问题,即大小端模式的选择问题。另外,由于Modbus协议规定寄存器为16位(即两个字节)长度,因此大小端问题的存在给很多初学者造成了困扰。下一节将重点讲解大小端模式。

接收设备在接收信息时,会通过CRC算法重新计算,并把计算值与CRC字段中接收的实际值进行比较。若两者不同,则产生一个错误,并返回一个异常响应报文(帧)告知发送设备。

Modbus协议中的RTU校验码(CRC)计算,运算规则(即CRC计算方法)如下:

(1)预置一个值为0xFFFF的16位寄存器,此寄存器为CRC寄存器。

(2)把第1个8位二进制数据(即通信消息帧的第1个字节)与16位的CRC寄存器的相异或,异或的结果仍存放于该CRC寄存器中。

(3)把CRC寄存器的内容右移一位,用0填补最高位,并检测移出位是0还是1。

(4)如果移出位为零,则重复步骤(3)(再次右移一位);如果移出位为1,则CRC寄存器与0xA001进行异或。

(5)重复步骤(3)和(4),直到右移8次,这样整个8位数据全部进行了处理。

(6)重复步骤(2)~(5),进行通信消息帧下一个字节的处理。

(7)将该通信消息帧所有字节按上述步骤计算完成后,得到的16位CRC寄存器的高、低字节进行交换。即发送时首先添加低位字节,然后添加高位字节。

(8)最后得到的CRC寄存器内容即为CRC校验码。

需要强调的一点是,在CRC计算时只有串行链路上每个字符中的8个数据位参与计算,而其他比如起始位及停止位,如有奇偶校验位也包括奇偶校验位,都不参与CRC计算。

常用的CRC-16算法有查表法和计算法。

1.查表法

CRC查表法是将移位异或的计算结果做成了一个表,就是将0~256放入一个长度为16位的寄存器中的低8位,高8位填充0,然后将该寄存器与多项式0xA001按照上述步骤(3)、(4),直到8位全部移出,最后寄存器中的值就是表格中的数据,高8位、低8位分别单独一个表。

实际上,Modbus标准协议英文版提供了CRC查表算法。

函数的输入参数意义如下:

        unsigned char * puchMsg;          / * 要进行CRC校验的消息 * /
        unsigned short usDataLen;        / * 消息中字节数 * /
 1  / * 函数返回 unsigned short(即 2个字节)类型的CRC值 * /
 2  unsigned short CRC16(unsigned char * puchMsg, unsigned short usDataLen)
 3  {
 4        unsigned char uchCRCHi=0xFF;        / * 高CRC字节初始化 * /
 5        unsigned char uchCRCLo=0xFF;        / * 低CRC字节初始化 * /
 6        unsigned short uIndex;              / * CRC循环表中的索引 * /
 7
 8        while (usDataLen--)                 / * 循环处理传输缓冲区消息 * /
 9        {
10           uIndex=uchCRCHi ^ * puchMsg++;   / * 计算CRC * /
11           uchCRCHi=uchCRCLo ^ auchCRCHi[uIndex];
12           uchCRCLo=auchCRCLo[uIndex];
13        }
14
15        return (uchCRCHi <<8 | uchCRCLo);
16  }

其中,auchCRCHi和auchCRCLo分别定义如下:

 1  static unsigned char auchCRCHi[] =
 2  {
 3        0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,
          0xC1,0x81,
 4        0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,
          0x01,0xC0,
 5        0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,
          0x40,0x01,
 6        0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,
          0x80,0x41,
 7        0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x00,
          0xC1,0x81,
 8        0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,
          0x01,0xC0,
 9        0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,
          0x41,0x01,
10        0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,
          0x81,0x40,
11        0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,
          0xC1,0x81,
12        0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,
          0x01,0xC0,
13        0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,
          0x40,0x01,
14        0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,
          0x80,0x41,
15        0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,
          0xC1,0x81,
16        0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,
          0x01,0xC0,
17        0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,
          0x41,0x01,
18        0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,
          0x80,0x41,
19        0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,
          0xC1,0x81,
20        0x40
21  } ;
22
23  static char auchCRCLo[] =
24  {
25        0x00,0xC0,0xC1,0x01,0xC3,0x03,0x02,0xC2,0xC6,0x06,0x07,0xC7,0x05,
          0xC5,0xC4,
26        0x04,0xCC,0x0C,0x0D,0xCD,0x0F,0xCF,0xCE,0x0E,0x0A,0xCA,0xCB,0x0B,
          0xC9,0x09,
27        0x08,0xC8,0xD8,0x18,0x19,0xD9,0x1B,0xDB,0xDA,0x1A,0x1E,0xDE,0xDF,
          0x1F,0xDD,
28        0x1D,0x1C,0xDC,0x14,0xD4,0xD5,0x15,0xD7,0x17,0x16,0xD6,0xD2,0x12,
          0x13,0xD3,
29        0x11,0xD1,0xD0,0x10,0xF0,0x30,0x31,0xF1,0x33,0xF3,0xF2,0x32,0x36,
          0xF6,0xF7,
30        0x37,0xF5,0x35,0x34,0xF4,0x3C,0xFC,0xFD,0x3D,0xFF,0x3F,0x3E,0xFE,
          0xFA,0x3A,
31        0x3B,0xFB,0x39,0xF9,0xF8,0x38,0x28,0xE8,0xE9,0x29,0xEB,0x2B,0x2A,
          0xEA,0xEE,
32        0x2E,0x2F,0xEF,0x2D,0xED,0xEC,0x2C,0xE4,0x24,0x25,0xE5,0x27,0xE7,
          0xE6,0x26,
33        0x22,0xE2,0xE3,0x23,0xE1,0x21,0x20,0xE0,0xA0,0x60,0x61,0xA1,0x63,
          0xA3,0xA2,
34        0x62,0x66,0xA6,0xA7,0x67,0xA5,0x65,0x64,0xA4,0x6C,0xAC,0xAD,0x6D,
          0xAF,0x6F,
35        0x6E,0xAE,0xAA,0x6A,0x6B,0xAB,0x69,0xA9,0xA8,0x68,0x78,0xB8,0xB9,
          0x79,0xBB,
36        0x7B,0x7A,0xBA,0xBE,0x7E,0x7F,0xBF,0x7D,0xBD,0xBC,0x7C,0xB4,0x74,
          0x75,0xB5,
37        0x77,0xB7,0xB6,0x76,0x72,0xB2,0xB3,0x73,0xB1,0x71,0x70,0xB0,0x50,
          0x90,0x91,
38        0x51,0x93,0x53,0x52,0x92,0x96,0x56,0x57,0x97,0x55,0x95,0x94,0x54,
          0x9C,0x5C,
39        0x5D,0x9D,0x5F,0x9F,0x9E,0x5E,0x5A,0x9A,0x9B,0x5B,0x99,0x59,0x58,
          0x98,0x88,
40        0x48,0x49,0x89,0x4B,0x8B,0x8A,0x4A,0x4E,0x8E,0x8F,0x4F,0x8D,0x4D,
          0x4C,0x8C,
41        0x44,0x84,0x85,0x45,0x87,0x47,0x46,0x86,0x82,0x42,0x43,0x83,0x41,
          0x81,0x80,
42        0x40
43  };

注意:实际编程的时候,auchCRCHi[]和auchCRCLo[]的定义应该放在函数CRC16()之前。

查表法可以进一步简化如下:

 1  unsigned short CRC16(unsigned char * puchMsg, unsigned short usDataLen)
 2  {
 3         static const unsigned short usCRCTable[] =
 4         {
 5             0X0000,0XC0C1, 0XC181, 0X0140, 0XC301, 0X03C0, 0X0280, 0XC241,
 6             0XC601, 0X06C0, 0X0780, 0XC741, 0X0500, 0XC5C1, 0XC481, 0X0440,
 7             0XCC01, 0X0CC0, 0X0D80, 0XCD41, 0X0F00, 0XCFC1, 0XCE81, 0X0E40,
 8             0X0A00, 0XCAC1, 0XCB81, 0X0B40, 0XC901, 0X09C0, 0X0880, 0XC841,
 9             0XD801, 0X18C0, 0X1980, 0XD941, 0X1B00, 0XDBC1, 0XDA81, 0X1A40,
10             0X1E00, 0XDEC1, 0XDF81, 0X1F40, 0XDD01, 0X1DC0, 0X1C80, 0XDC41,
11             0X1400, 0XD4C1, 0XD581, 0X1540, 0XD701, 0X17C0, 0X1680, 0XD641,
12             0XD201, 0X12C0, 0X1380, 0XD341, 0X1100, 0XD1C1, 0XD081, 0X1040,
13             0XF001, 0X30C0, 0X3180, 0XF141, 0X3300, 0XF3C1, 0XF281, 0X3240,
14             0X3600, 0XF6C1, 0XF781, 0X3740, 0XF501, 0X35C0, 0X3480, 0XF441,
15             0X3C00, 0XFCC1, 0XFD81, 0X3D40, 0XFF01, 0X3FC0, 0X3E80, 0XFE41,
16             0XFA01, 0X3AC0, 0X3B80, 0XFB41, 0X3900, 0XF9C1, 0XF881, 0X3840,
17             0X2800, 0XE8C1, 0XE981, 0X2940, 0XEB01, 0X2BC0, 0X2A80, 0XEA41,
18             0XEE01, 0X2EC0, 0X2F80, 0XEF41, 0X2D00, 0XEDC1, 0XEC81, 0X2C40,
19             0XE401, 0X24C0, 0X2580, 0XE541, 0X2700, 0XE7C1, 0XE681, 0X2640,
20             0X2200, 0XE2C1, 0XE381, 0X2340, 0XE101, 0X21C0, 0X2080, 0XE041,
21             0XA001, 0X60C0, 0X6180, 0XA141, 0X6300, 0XA3C1, 0XA281, 0X6240,
22             0X6600, 0XA6C1, 0XA781, 0X6740, 0XA501, 0X65C0, 0X6480, 0XA441,
23             0X6C00, 0XACC1, 0XAD81, 0X6D40, 0XAF01, 0X6FC0, 0X6E80, 0XAE41,
24             0XAA01, 0X6AC0, 0X6B80, 0XAB41, 0X6900, 0XA9C1, 0XA881, 0X6840,
25             0X7800, 0XB8C1, 0XB981, 0X7940, 0XBB01, 0X7BC0, 0X7A80, 0XBA41,
26             0XBE01, 0X7EC0, 0X7F80, 0XBF41, 0X7D00, 0XBDC1, 0XBC81, 0X7C40,
27             0XB401, 0X74C0, 0X7580, 0XB541, 0X7700, 0XB7C1, 0XB681, 0X7640,
28             0X7200, 0XB2C1, 0XB381, 0X7340, 0XB101, 0X71C0, 0X7080, 0XB041,
29             0X5000, 0X90C1, 0X9181, 0X5140, 0X9301, 0X53C0, 0X5280, 0X9241,
30             0X9601, 0X56C0, 0X5780, 0X9741, 0X5500, 0X95C1, 0X9481, 0X5440,
31             0X9C01, 0X5CC0, 0X5D80, 0X9D41, 0X5F00, 0X9FC1, 0X9E81, 0X5E40,
32             0X5A00, 0X9AC1, 0X9B81, 0X5B40, 0X9901, 0X59C0, 0X5880, 0X9841,
33             0X8801, 0X48C0, 0X4980, 0X8941, 0X4B00, 0X8BC1, 0X8A81, 0X4A40,
34             0X4E00, 0X8EC1, 0X8F81, 0X4F40, 0X8D01, 0X4DC0, 0X4C80, 0X8C41,
35             0X4400, 0X84C1, 0X8581, 0X4540, 0X8701, 0X47C0, 0X4680, 0X8641,
36             0X8201, 0X42C0, 0X4380, 0X8341, 0X4100, 0X81C1, 0X8081, 0X4040
37         };
38
39         unsigned char nTemp;
40         unsigned short usRegCRC =0xFFFF;
41
42         while (usDataLen--)
43         {
44             nTemp = * puchMsg++^ usRegCRC;
45             usRegCRC >>=8;
46             usRegCRC ^=usCRCTable[nTemp];
47         }
48         return usRegCRC;
49  }

查表法的特点是:以字节为单位进行计算,速度快、语句少,但表格占用一定的程序空间。

2.计算法

计算法按位计算。这个方法可以适用于所有长度的数据校验,最为灵活;但由于是按位计算,其效率并不是最优,只适用于对速度不敏感的场合。基本的算法如下:

输入参数的意义:

         unsigned char * puchMsg;        / * 要进行CRC校验的消息 * /
         unsigned short usDataLen;      / * 消息中的字节数 * /
 1  / * 函数返回 unsigned short(即 2个字节)类型的CRC值 * /
 2  unsigned short CRC16(unsigned char * puchMsg, unsigned short usDataLen)
 3  {
 4       int i, j;                                 / * 循环变量 * /
 5       unsigned short usRegCRC =0xFFFF; / * 用于保存CRC值 * /
 6
 7       for(i =0; i <usDataLen; i++)              / * 循环处理传输缓冲区消息 * /
 8       {
 9          usRegCRC ^= * puchMsg++; / * 异或算法得到CRC值 * /
10          for(j =0; j <8; j++)                   / * 循环处理每个 bit位 * /
11          {
12               if (usRegCRC & 0x0001)
13                   usRegCRC =usRegCRC >>1 ^ 0xA001;
14               else
15                   usRegCRC >>=1;
16          }
17       }
18
19       return usRegCRC;
20  }

这里举一个简单的例子。假设从设备地址为1,要求读取输入寄存器地址30001的值,则RTU模式下具体的查询消息帧如下:

0x01,0x04,0x00,0x00,0x00,0x01,0x31,0xCA

其中,0xCA31即为CRC值。因为Modbus规定发送时CRC必须低字节在前,高字节在后,因此实际的消息帧的发送顺序为0x31,0xCA。