1.简述
Modbus常用的有ModbusRTU、Modbus ASCII和ModbusTCP。其中ModbusTCP不涉及自身协议的校验,另外两种Modbus的校验方法分为两种,一种是对每个单独报文的奇偶校验,另一个是对每帧数据的帧校验。
(1)、奇偶校验
奇偶校验有奇校验和偶校验(如果无校验,则报文中默认有两个停止位)两种。ModbusRTU报文有11位(1位起始位,8位数据位,1位奇偶校验位,1位停止位),Modbus ASCII报文有10位(1位起始位,7位数据位,1位奇偶校验位,1位停止位),如下图:
图中的奇偶校验位采用奇校验:数据位有奇数个1,则该位为0;如果数据位有偶数个1,则该位为1,这样数据位和奇偶校验位就共有奇数个1.
图中的奇偶校验位采用偶校验:数据位有偶数个1,则该位为0;如果数据位有奇数个1,则该位为1,这样数据位和奇偶校验位就共有偶数个1.
例如:字节数据为1010 0101,其中1的数量是4,是偶数,如果是偶校验,奇偶校验位就为0;如果是奇校验,奇偶校验位就为1。
(2)、帧校验
在ModbusRTU和Modbus ASCII多数用的是帧校验,帧校验有两种,一种是ModbusRTU采用的CRC(循环冗余校验),另一种是Modbus ASCII采用的LRC( 纵向冗余校验),这两种校验方式具体内容我们不再赘述,本文主要谈Modbus ASCII通信协议的LRC校验的具体实现代码,如果要查看ModbusRTU通信协议的CRC校验的代码,可以参考我另一篇文章“C# ModbusRTU的CRC校验代码”。Modbus ASCII通讯数据(信息帧)的格式为:0x3A + 从站地址码(2 char)+功能码(2 char)+数据区(char[N])+LRC校验码(2 char)+ 0x0D + 0x0A。
2.LRC8代码
LRC校验结果是8位,是一个字节,它实际就是对Modbus的PDU数据相加后取反加1,相当于PDU数据相加再取负值,代码非常简单。要注意,Modbus ASCII虽然是以ASCII码可见字符串形式传输,但是计算LRC校验码时,必须将ASCII码转换为字节流后才能进行(两个ASCII字符转换为一个byte字节),下面LRC类的第二个重载方法就是把Modbus ASCII字符串转换后再计算LRC值的。
#region LRC Class
/// <summary>
/// LRC校验
/// </summary>
public static class LRC8
{
/// <summary>
/// 纵向冗余校验LRC
/// </summary>
/// <param name="data">原数组</param>
/// <param name="offset">从头开始偏移几个byte</param>
/// <param name="length">偏移后取几个字节byte</param>
/// <returns>8位LRC</returns>
public static byte LRCCalc(byte[] data, int offset, int length)
{
byte lrc = 0;
for (int i = 0; i < length; i++)
lrc += data[i + offset];
return (byte)(-(sbyte)lrc);
}
/// <summary>
/// 纵向冗余校验LRC
/// </summary>
/// <param name="data">原数组(有效字符串,不能含帧头和帧尾)</param>
/// <param name="offset">从头开始偏移几个byte</param>
/// <param name="length">偏移后取几个字节byte</param>
/// <returns>8位LRC</returns>
public static byte LRCCalc(string data, int offset, int length)
{
try
{
data = new string(data.Replace(" ",string.Empty).ToCharArray().Skip(offset).Take(length).ToArray());
if (data.Length % 2 == 1)
throw new ArgumentException();
byte[] buffer = new byte[data.Length / 2];
for (int i = 0; i < data.Length; i += 2)
buffer[i / 2] = Convert.ToByte(data.Substring(i, 2), 16);
return LRCCalc(buffer, 0, buffer.Length);
}
catch
{
return 0;
}
}
}
#endregion
3.使用举例
我们先看一条Modbus ASCII查询输入寄存器的指令, " : 01 04 20 C1 00 02 18 < CR LF> “,这条指令第一个字符”:"是Modbus ASCII的帧头,最后两个字符< CR LF >是帧尾结束回车换行,中间的01 04 20 C1 00 02 18是其有效数据。将上面指令转换为等效的byte数组为0x3A 0x01 0x04 0x20 0xC1 0x00 0x02 0x18 0x0D 0x0A,第一个字节0x3A是ASCII码的冒号(:),最后两个字符0x0D 0x0A是回车换行符,倒数第三个字节0x18就是LRC值,它的十进制值是24。下面我们用LRC的第一个重载方法来测试。
byte[] exampleCmd = new byte[] { 0x3A, 0x01, 0x04, 0x20, 0xC1, 0x00, 0x02, 0x18, 0x0D, 0x0A };
//参数offset为1,是指跳过冒号0x3A,参数length为6,是指对0x18之前这6个字节进行LRC计算
byte lrc = LRC8.LRCCalc(exampleCmd, 1, 6);
结果是:
可以看出结果是24,即为十六进制的0x18,是完全正确的。
我们还是以上面的指令再用LRC第二个重载方法举例,需要注意的是第一个参数字符串是不能含指令中的帧头冒号(:)和帧尾回车换行符的,要取出指令中的有效字符串 “01 04 20 C1 00 02 18”,同时第三个参数指的是有效字符长度,不含空格,所以在指令中LRC值18之前的有效字符长度应为12。
string exampleCmd = "01 04 20 C1 00 02 18";
//参数length是指字符串不含空格的有效字符长度,本例中指的是01 04 20 C1 00 02共12个字符
byte lrc = LRC8.LRCCalc(exampleCmd, 0, 12);
结果是:
同样结果是24,用十六进制表示则是0x18。
4.说明
本来LRC校验代码很简单,我却写了这么多,可能让大家觉得有点拖沓冗长,那么请大家谅解一下吧,本人也只是想把Modbus校验方式和LRC代码说明白一些,莫怪莫怪!