- Modbus软件开发实战指南
- 杨更更
- 1715字
- 2021-03-12 19:47:47
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。