10.4.6 Modbus差错校验
在Modbus串行通信中,根据传输模式(ASCII或RTU)的不同,差错校验域将采用不同的校验方法。
①ASCII模式
在ASCII模式中,报文包含一个错误校验字段,该字段由两个字符组成,其基于对全部报文内容执行的纵向冗余校验(Longitudinal Redundancy ChedLRC)计算的结果而来,计算对象不包括起始的冒号(:)和回车换行符号(CR LF)。
②RTU模式
在RTU模式中,报文同样包含一个错误校验字段。与 ASCII模式不同的是该字段由16个比特位共2字节组成,其值基于对全部报文内容执行的循环冗余校验(Cyclical Redundancy Check,CRC)计算的结果而来,计算对象包括校验域之的所有字节。
1. LRC校验
在ASCII模式中,消息是由特定的字符作为帧头和帧尾分隔的。
一条消息必须以“冒号”(:)字符(ASCII码为0x3A)开始,以“回车换行(CRLF)(ASCII码为0x0D和0x0A)结束。LRC校验算法的计算范围为“:”“CRLF”之间的字符。
从算法本质来说,LRC域自身为1字节,即包含一个8位二进制数据,由发送设备通过LRC算法把计算值附到信息末尾。接收设备在接收信息时通过LRC法重新计算值,并把计算值与LRC字段中接收的实际值进行比较。若两者不同,则产生一个错误,返回一个异常响应帧,即对报文中的所有相邻的两个8位字相加,丢弃任何进位,然后对结果进行二进制补码,计算出LRC值。
必须注意的是,计算LRC校验码的时机是在对报文中每个原始字节进行ASCII码编码之前,对每个原始字节进行LRC校验的计算操作。
LRC校验流程:
①将消息中的全部字节相加(不包括起始“:”和结束符(CRLF),并把结果送入8位数据区,舍弃进位。
②由0xFF(即全1)减去最终的数据值,产生1的补码(即二进制反码)。
③加1产生二进制补码。
以上产生的LRC值占用1字节,但实际上在通过串行链路由ASCII模式传递消息顿时,LRC的结果(1字节)被编码为2字节的ASCII字符,并将其放置在ASCII模式报文帧的CRLF字段之前。
Modbus标准协议的英文版提供了LRC算法,其中的参数意义如下unsigned char*auchMsg:含有生成LRC所使用的二进制数据的报文缓存区指针。unsigned short usDataLen:报文缓存区中的字节数。
LCR的代码如下:
/*函数返回unsigned char类型的 LRC值*/
static unsigned char LRC(unsigned char * auchMsg, unsigned short usDatalen)
{
unsigned char uchLRC=0; /*LRC字节初始化*/
while(usDataLen--) /*遍历报文缓冲区*/
uchLRC+=*auchMsg++; /*缓冲区宇节相加,自动舍弃进位*/
return ((unsigned char)(-(( char)uchLRC))); /*返回二进制补码*/
}
下面举一个简单的例子。假设从设备地址为1,要求读取输人寄存器地址30001的值,则具体的查询消息帧如下:
":" , "0" , "1" , "0" , "4" , "0" ,"0" ,"0" ,"0" ,"0" ,"0" ,"0" , "1" , "F" ,
"A" ,CR/LF
其中,“F”、“A”即为LRC值在ASCII模式下的形式,即 0xFA。
2. CRC校验
在Modbus RTU传输模式下,通信报文(帧)包括一个基于循环冗余校验方法的差错校验字段。
Modbus协议采用了CRC-16标准校验方法。在RTU模式下,CRC自身由2字节组成,即CRC是一个16位的值。CRC字段校验整个报文的内容,无论报文中的单个字节采用何种奇偶校验方式,整个通信报文均可使用CRC-16 校验算法,CRC字段作为报文的最后字段添加在整个报文末尾。
需要注意的是,因为CRC-16是由2字节组成,所以涉及哪个字节放在前面,哪个字节放在后面传输的问题,即大小端模式的选择问题。另外,由于Modbus协议规定寄存器为16位(即2字节)长度,因此大小端问题的存在给很多初学者造成了困扰,下一章我们会重点讲一下大小端的问题。
CRC校验流程:
①预置一个16位寄存器为0xFFFF(全1),称之为CRC 寄存器。
②把数据帧中的第一个字节的8位与CRC寄存器中的低字节进行异或运算,结果存回CRC寄存器。
③将CRC寄存器向右移一位,最高位填以0,最低位移出并检测是0还是1。
④如果最低位为0:重复第三步(再次右移一位)。如果最低位为1:将CRC寄存器与一个预设的固定值(0xA001)进行异或运算。
⑤重复第三步和第四步直到8次移位。这样处理完了一个完整的八位。
⑥重复第2步到第5步来处理下一个八位,直到所有的字节处理结束。
⑦将该通信消息帧的所有字节按上述步骤计算完成后,再将得到的16位CRC寄存器的高、低位字节进行交换,即发送时首先添加低位字节,然后添加高位字节。
⑧最终CRC寄存器的值就是CRC的校验码。
需要注意的是,在进行CRC计算时只有串行链路上的每个字符的8个数据位参与计算,从而起始位、停止位、奇偶校验位等都不参与CRC计算。
常用的CRC-16算法有查表法、计算法。
查表法:
CRC查表法是将位移异或的计算结果做成了一个表,即将0~256放入一个度为16位的寄存器的低8位,高8位填充 0,然后将该寄存器与多项式0xA001照上述步骤3、4直到8位全部移出,最后寄存器中的值就是表格中的数据,高8位、低8位分别单独做成一个表。实际上,Modbus标准协议的英文版提供了CRC查表算法函数的输入参数意义如下:
unsigned char * puchMsg; /*要进行CRC校验的消息*/
unsigned short usDataLen; /*消息中的字节数*/
/*函数返回 unsigned short(即2个字节)类型的 CRC值*/
unsigned short CRC16(unsigned char *puchMsg,unsigned short usDataLen)
{
unsigned charuchCRCHi=0xFF; /*高 CRC字节初始化* /
unsigned char uchCRCLo=0xFF; /*低 CRC字节初始化*/
unsigned short uIndex; /*CRC 循环表中的索引*/
while (usDataLen--) /* 循环处理传输缓冲区消息 */
{
uIndex=uchCRCHi ^ * puchMsg++; /*计算 CRC* /
uchCRCHi=uchCRCLo ^ auchCRCHi[uIndex];
uchCRCLo=auchCRCLo[uIndex];
}
return (uchCRCHi <<81uchCRCLo);
}
其中,auchCRCHi和auchCRCLo的定义分别如下:
static unsigned char auchcRCHi[] =
{
0x00,0xC1,0x81,0x48,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,0x01,
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,0xCl,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
};
static char auchCRCLo[]=
{
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,0xDB,0x18,0x19,0xD9,0x1B,0xDB,0xDA,0x1A,0x1E,0XDE,0XDE,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,0xE3,0xE2,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,0xAB,0x68,0x78,0xB8,0xB9,0x79,0xBB,
0x7B,0x7A,0xBA,0xBE,0x7E,0x7E,0xBE,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,0x8E,0x4F,0x8D,0x4D,0x4C,0x8C,
0x44,0x84,0x85,0x45,0x87,0x47,0x46,0x86,0x82,0x42,0x43,0x83,0x41,0x81,0x80,
0x40
};
注意:实际编程时,auchcRCHi[ ]和auchCRCLo[ ]的定义应该放在函数CRC-16()之前。
查表法可以进一步简化如下:
unsigned short CRC16(unsigned char * puchMsg,unsigned short usDataLen)
{
static const unsigned short usCRCTable[]=
{
0x0000,0xC0C1,0xC181,0x0140,0XC301,0X03C0,0X0280,0xc241,
0XC601,0X06C0,0x0780,0XC741,0X0500,0XC5C1,0XC481,0X0440,
0xCC01,0X0CC0,0X0D80,0XCD41,0X0F00,0XCEC1,0XCE81,0X0E40,
0X0A00,0XCAC1,0XCB81,0X0B40,0XC901,0X09C0,0X0880,0XC841,
0XD801,0x18c0,0X1980,0XD941,0X1B00,0XDBC1,0XDA81,0X1A40,
0X1E00,0XDEC1,0XDF81,0X1F40,0XDD01,0X1DC0,0X1C80,0XDC41,
0x1400,0XD4C1,0XD581,0X1540,0XD701,0X17C0,0X1680,0XD641,
0XD201,0X12c0,0X1380,0XD341,0X1100,0XD1C1,0XD081,0X1040,
0XF001,0X30C0,0X3180,0XE141,0X3300,0XE3C1,0XE281,0X3240,
0X3600,0XF6C1,0XE781,0X3740,0XE501,0X35C0,0X3480,0XE441,
0X3C00,0XFCC1,0XFD81,0X3D40,0XFF01,0X3FC0,0X3EB0,0XFE41,
0XFA01,0X3AC0,0X3B80,0XFB41,0X3900,0XE9C1,0XF881,0X3840,
0X2800,0XE8C1,0XE981,0X2940,0XEB01,0X2BC0,0X2A80,0XEA41,
0XEE01,0X2EC0,0X2F80,0XEF41,0X2D00,0XEDC1,0XEC81,0X2C40,
0XE401,0X24C0,0X2580,0XE541,0X2700,0XE7C1,0XE681,0X2640,
0x2200,0XE2C1,0XE381,0X2340,0XE101,0X21C0,0X2080,0XE041,
0XA001,0X60C0,0X6180,0XA141,0X6300,0XA3C1,0XA281,0X6240,
0X6600,0XA6c1,0XA781,0X6740,0XA501,0X65C0,0X6480,0XA441,
0X6C00,0XACC1,0XAD81,0X6D40,0XAF01,0X6EC0,0X6E80,0XAE41,
0XAA01,0X6AC0,0X6B80,0XAB41,0X6900,0XA9C1,0XA881,0X6840,
0X7800,0XB8C1,0XB981,0X7940,0XBB01,0X7BC0,0X7A80,0XBA41,
0XBE01,0X7EC0,0X7F80,0XBF41,0X7D00,0XBDC1,0XBC81,0X7C40,
0XB401,0X74C0,0X7580,0XB541,0X7700,0XB7C1,0XB681,0X7640,
0X7200,0XB2C1,0XB381,0X7340,0XB101,0X71C0,0X7080,0XB041,
0X5000,0X90c1,0X9181,0X5140,0X9301,0X53c0,0X5280,0X9241,
0X9601,0X56C0,0X5780,0X9741,0X5500,0X95c1,0X9481,0X5440,
0X9C01,0X5cc0,0X5D80,0X9D41,0X5E00,0X9FC1,0X9E81,0X5E40,
0X5A00,0X9AC1,0X9B81,0X5B40,0x9901,0x59c0,0x5880,0X9841,
0x8801,0X4BC0,0X4980,0X8941,0X4B00,0XBBC1,0X8AB1,0X4A40,
0X4E00,0X8EC1,0X8F81,0X4F40,0X8D01,0X4DC0,0X4C80,0X8C41,
0X4400,0X84c1,0x8581,0X4540,0X8701,0X47C0,0X4680,0X8641,
0x8201,0X42c0,0x4380,0X8341,0X4100,0XB1C1,0X8081,0X4040,
};
unsigned char nTemp;
unsigned short usRegCRC = 0xFFFF;
while (usDataLen--)
{
nTemp = * puchMsg ++ ^ usRegCRC;
usRegCRC >> = 8;
usRegCRC ^= usCRCTable[nTemp];
}
return usRegCRC;
}
查表法的特点是以字节为单位进行计算,速度快,语句少,但表格会占用一定的程序空间。
计算法:
计算法按位计算,适用于所有长度的数据校验,最为灵活;但由于是按位计算,其效率并不是最优的,只适用于对速度不敏感的场合。计算法的基本算法如下:
unsigned char * puchMsg; /*要进行 CRC校验的消息* /
unsigned short usDataLen; /*消息中的字节数*/
/*函数返回unsigned short(即 2个字节)类型的CRC值*/
unsigned short CRC16(unsigned char *puchMsg,unsigned short usDataLen)
{
int i,j; /*循环变量*/
unsigned shortusRegCRC =0xFFFF; /*用于保存CRC值*/
for(i=0;i < usDataLen;i++) /*循环处理传输缓冲区消息*/
{
usRegCRC ^= * puchMsg++; /*异或算法得到CRC值*/
for(j=0;j<8;j++) /*循环处理每个 bit位*/
{
if (usRegCRC &0x0001)
usRegCRC =usRegCRC >>1^0xA001;
else
usRegCRC >>=1;
}
}
return usRegCRC;
}
下面举一个简单的例子。假设从设备地址为1,要求读取输入寄存器地址30001的值,则RTU模式下的具体查询消息帧如下:
0x01,0x04,0x00,0x00,0x00,0x01,0x31,0xCA
其中,0xCA31即为CRC值。因为Modbus规定发送时 CRC必须低字节在前、高字节在后,因此实际的消息帧的发送顺序为0x31,0xCA。
如您在使用瑞萨MCU/MPU产品中有任何问题,可识别下方二维码或复制网址到浏览器中打开,进入瑞萨技术论坛寻找答案或获取在线技术支持。
https://community-ja.renesas.com/zh/forums-groups/mcu-mpu/
未完待续
推荐阅读
学习Modbus的快速方法 - RZ MPU工业控制教程连载(23)
初识Modbus - RZ MPU工业控制教程连载(24)
虚拟串口与Modbus互联 - RZ MPU工业控制教程连载(25)