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( 纵向冗余校验),这两种校验方式具体内容我们不再赘述,现在主要谈ModbusRTU通信协议的CRC校验的具体实现代码。ModbusRTU通讯数据(信息帧)的格式为:从站地址码(1byte)+功能码(1byte)+数据区(N bytes)+CRC校验码(2 bytes)。CRC寄存器为16位,它是经过CRC运算,然后高低位进行交换形成的。
2.CRC16代码
下面CRC16类是将CRC计算后高低位进行交换的结果,高位放在返回的crc16[0],低位在crc16[1],可以顺序使用而不必进行byte数组的Reverse。
CRC16类有三个重载方法,一个是参数为byte数组,另一个是通过偏移量,截取指定数量的byte数组,第三个是参数为字符串。其中参数为字符串的要注意,字符串参数只能以16进制表示,支持以逗号和空格为间隔的输入字符串,同时表示16进制的字符串元素可以加0X或0x,不加也可以。比如"01 03 40 00 00 02”、“01, 03, 04, 43, 00, 1A”、“0X01, 0X02, 00, 03, 0x00, 0x0B"均符合要求,但是不能将逗号和空格一起使用,如” 01 0X03, 0x00 02 00,2F"这是错误的。
#region 16位CRC校验
public static class CRC16
{
/// <summary>
/// CRC校验,参数data为byte数组
/// </summary>
/// <param name="data">校验数据,字节数组</param>
/// <returns>字节0是高8位,字节1是低8位</returns>
public static byte[] CRCCalc(byte[] data)
{
//crc计算赋初始值
int crc = 0xffff;
for (int i = 0; i < data.Length; i++)
{
crc = crc ^ data[i];
for (int j = 0; j < 8; j++)
{
int temp;
temp = crc & 1;
crc = crc >> 1;
crc = crc & 0x7fff;
if (temp == 1)
{
crc = crc ^ 0xa001;
}
crc = crc & 0xffff;
}
}
//CRC寄存器的高低位进行互换
byte[] crc16 = new byte[2];
//CRC寄存器的高8位变成低8位,
crc16[1] = (byte)((crc >> 8) & 0xff);
//CRC寄存器的低8位变成高8位
crc16[0] = (byte)(crc & 0xff);
return crc16;
}
/// <summary>
/// CRC校验,参数为空格或逗号间隔的字符串
/// </summary>
/// <param name="data">校验数据,逗号或空格间隔的16进制字符串(带有0x或0X也可以),逗号与空格不能混用</param>
/// <returns>字节0是高8位,字节1是低8位</returns>
public static byte[] CRCCalc(string data)
{
//分隔符是空格还是逗号进行分类,并去除输入字符串中的多余空格
IEnumerable<string> datac = data.Contains(",") ? data.Replace(" ", "").Replace("0x", "").Replace("0X", "").Trim().Split(',') : data.Replace("0x", "").Replace("0X", "").Split(' ').ToList().Where(u => u != "");
List<byte> bytedata = new List<byte>();
foreach (string str in datac)
{
bytedata.Add(byte.Parse(str, System.Globalization.NumberStyles.AllowHexSpecifier));
}
byte[] crcbuf = bytedata.ToArray();
//crc计算赋初始值
return CRCCalc(crcbuf);
}
/// <summary>
/// CRC校验,截取data中的一段进行CRC16校验
/// </summary>
/// <param name="data">校验数据,字节数组</param>
/// <param name="offset">从头开始偏移几个byte</param>
/// <param name="length">偏移后取几个字节byte</param>
/// <returns>字节0是高8位,字节1是低8位</returns>
public static byte[] CRCCalc(byte[] data, int offset, int length)
{
byte[] Tdata = data.Skip(offset).Take(length).ToArray();
return CRCCalc(Tdata);
}
}
#endregion
3.使用例子
以Modbus查询保存寄存器(功能码0x03)为例构建一条ModbusRTU指令
//从站地址
byte slaveAddress = 1;
//功能码
byte functionCode = 3;
//保持寄存器开始地址,16位寄存器需要把int转为ushort。因通信格式是高位在前,所以要Reverse()
int start = 0x4000;
byte[] Starts = BitConverter.GetBytes((ushort)start).Reverse().ToArray();
//访问保存寄存器数量
int number = 2;
byte[] Numbers = BitConverter.GetBytes((ushort)number).Reverse().ToArray();//
//Modbus功能码03通讯指令共8个字节
byte[] command = new byte[8];
command[0] = slaveAddress;
command[1] = functionCode;
Array.Copy(Starts, 0, command, 2, Starts.Length);//Starts.Length实际就是2
Array.Copy(Numbers, 0, command, 4, Numbers.Length);//Numbers.Length实际也是2
//计算CRC值
byte[] bdata = new byte[6];
Array.Copy(command, 0, bdata, 0, bdata.Length);
//CRC校验码,crc16[0]为高位,crc16[1]为低位,所以不用再Reverse()
byte[] crc16 = CRC16.CRCCalc(bdata);
//或者直接用截取一段byte[]的CRC16重载
//byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
//将CRC校验码写入指令command
Array.Copy(crc16, 0, command, 6, crc16.Length);
/*
至此,便构建完成查询保持寄存器(功能码0x03)的通讯指令command
*/
4.验证
下面我们进行验证,以16进制字符串形式输出cmd
//输出16进制结果
StringBuilder resultCmd = new StringBuilder();
foreach (var one in command)
{
resultCmd.Append(string.Format("{0:X2}", one));
resultCmd.Append(" ");
}
string cmd = resultCmd.ToString().Trim();
在VS中运行,得到cmd为下图,CRC16位校验码是:D1 CB
本人用command指令与ModbusRTU从站设备通信,返回结果证明了此指令正确,因此也说明了CRC类生成的CRC校验码完全正确。