1.概述
既然谈到ModbusRTU、Modbus ASCII、ModbusTCP数据返回,那我们多少会对Modbus数据发送会有了解,所以本文不再对Modbus的基础知识进行阐述,如果想了解Modbus常用的发送指令,可以参看我的文章"C# ModbusRTU命令功能码"、“C# Modbus ASCII命令功能码"和"C# ModbusTCP命令功能码”。
Modbus常用的通信指令有8条,其中功能码0x01、0x02、0x03、0x04是查询指令,0x05、0x06、0x0F、0x10是写入指令,对应的返回数据也就有8种。写入指令0x05和0x06的返回数据跟发送指令完全相同(返回异常码除外),0x0F和0x10返回数据跟发送指令的前6个字节完全相同,LRC、CRC校验码不同(ModbusTCP无此校验),写入指令的返回数据相当于确认功能,它们解析为数值是没有意义的。所以我们着重谈的是功能码0x01至0x04的返回数据的值解析。
2.解析代码
Modbus功能码0x01和0x02是查询线圈和离散量状态的,所以返回值类型为bool类型,功能码0x03和0x04是查询保持寄存器和输入寄存器的值,返回类型是数值型的。由于不同的通信硬件设备的解码字节顺序不同,也会带来返回数据的值不同,例如Modicon PLC的整形与浮点型均按3412顺序,而PLC返回的原始数据为1234,解析出来的值自然不会正确。
首先声明四个个枚举,其中有几条枚举项现在用不上,但是我写上它是为了能列出来更多的Modbus指令和生成Modbus发送指令时使用的,无用的大家可自己过滤掉。
/// <summary>
/// Modbus的类型
/// </summary>
public enum ModbusType
{
/// <summary>
/// Modbus serial RTU
/// </summary>
SERIAL_RTU = 0,
/// <summary>
/// Modbus serial ASCII
/// </summary>
SERIAL_ASCII = 1,
/// <summary>
/// Modbus TCP/IP
/// </summary>
TCP_IP = 2,
}
/// <summary>
/// Modbus指令代码
/// </summary>
public enum ModbusCodes
{
READ_COILS = 0x01, //读取线圈
READ_DISCRETE_INPUTS = 0x02, //读取离散量输入
READ_HOLDING_REGISTERS = 0x03, //读取保持寄存器
READ_INPUT_REGISTERS = 0x04, //读取输入寄存器
WRITE_SINGLE_COIL = 0x05, //写单个线圈
WRITE_SINGLE_REGISTER = 0x06, //写单个保持寄存器
READ_EXCEPTION_STATUS = 0x07, //读取异常状态
DIAGNOSTIC = 0x08, //回送诊断校验
GET_COM_EVENT_COUNTER = 0x0B, //读取事件计数
GET_COM_EVENT_LOG = 0x0C, //读取通信事件记录
WRITE_MULTIPLE_COILS = 0x0F, //写多个线圈
WRITE_MULTIPLE_REGISTERS = 0x10, //写多个保持寄存器
REPORT_SLAVE_ID = 0x11, //报告从机标识(ID)
READ_FILE_RECORD = 0x14, //读文件记录
WRITE_FILE_RECORD = 0x15, //写文件记录
MASK_WRITE_REGISTER = 0x16, //屏蔽写寄存器
READ_WRITE_MULTIPLE_REGISTERS = 0x17, //读写多个寄存器
READ_FIFO_QUEUE = 0x18, //读取队列
READ_DEVICE_IDENTIFICATION = 0x2B //读取设备标识
}
/// <summary>
/// 错误代码
/// </summary>
public enum Errors
{
NO_ERROR = 0,//无错误
EXCEPTION_UNKNOWN = 1,//未知异常
EXCEEDING_MODBUSCODE_RANGE = 2,//超出ModbusCode范围
UNPROCESSED_MODBUSCODE = 3,//没有处理的ModbusCode
WRONG_RESPONSE_ADDRESS = 4,//响应地址错误
WRONG_RESPONSE_REGISTERS = 5,//响应寄存器错误
WRONG_RESPONSE_VALUE = 6,//响应值错误
WRONG_CRC = 7,//CRC16校验错误
TOO_MANY_REGISTERS_REQUESTED = 8,//请求的寄存器数量太多
ZERO_REGISTERS_REQUESTED = 9,//零寄存器请求
EXCEPTION_ILLEGAL_FUNCTION = 20,//非法的功能码
EXCEPTION_ILLEGAL_DATA_ADDRESS = 21,//非法的数据地址
EXCEPTION_ILLEGAL_DATA_VALUE = 22,//非法的数据值
EXCEPTION_SLAVE_DEVICE_FAILURE = 23,//从站(服务器)故障
}
/// <summary>
/// Modbus返回字节大小/类型/顺序
/// </summary>
public enum ByteOrder
{
NONE = 0,
TWO_WORD_12 = 1,
TWO_WORD_21 = 2,
FOUR_INT_1234 = 3,
FOUR_INT_1243 = 4,
FOUR_INT_2134 = 5,
FOUR_INT_2143 = 6,
FOUR_INT_3412 = 7,
FOUR_INT_3421 = 8,
FOUR_INT_4312 = 9,
FOUR_INT_4321 = 10,
FOUR_FLOAT_1234 = 11,
FOUR_FLOAT_1243 = 12,
FOUR_FLOAT_2134 = 13,
FOUR_FLOAT_2143 = 14,
FOUR_FLOAT_3412 = 15,
FOUR_FLOAT_3421 = 16,
FOUR_FLOAT_4312 = 17,
FOUR_FLOAT_4321 = 18
}
下面就是解析从站返回数据的方法了,返回类型是object,是因为我们上面说过,返回类型可能是bool型和数值型,所以最后根据需要自己去强制转换。考虑到功能码0x01和0x02可能查询是多个线圈或离散量,所以解析方法返回的数据就会是一个bool数组序列,功能码0x03和0x04查询的可能是多个保持寄存器或输入寄存器,其解析回来的数值根据设备字节顺序要求,会是byte、ushort、int和float数组。
#region Modbus
public class Modbus
{
#region 内部方法
/// <summary>
/// 16进制的字符串转换成等效的byte[]数组
/// </summary>
private byte[] HexStringToBytes(string source)
{
source = source.Replace(" ", "");
if (source.Length % 2 == 1) return null;//字符串为奇数就无法转换,返回null
byte[] buffer = new byte[source.Length / 2];
for (int i = 0; i < source.Length; i += 2) buffer[i / 2] = Convert.ToByte(source.Substring(i, 2), 16);
return buffer;
}
#endregion
#region 解析ModbusRTU、Modbus ASCII、ModbusTCP命令返回的数据
/// <summary>
/// 在功能码0x01/0x02/0x03/0x04查询后,根据指定的字节顺序对从站(服务器端)返回的数据进行解析。
/// 1、功能码0x01和0x02返回的数据是bool数组,顺序是低位地址到高位地址的开关状态。
/// 2、功能码0x03和0x04返回的数据是byte、ushort、int、float数组,由byOrder参数决定。
/// 3、功能码0x05、0x06、0x0F、0x10返回数据解析为数值没有意义,如果有其他用途可以再做补充。
/// </summary>
/// <param name="data">从站(服务器端)返回的全部数据,ModbusRTU包括CRC,ModbusASCII包括帧头冒号和帧尾\r\n</param>
/// <param name="byteOrder">指定的字节顺序,只对0x03、0x04查询返回的数据有效</param>
/// <param name="modbusType">Modbus协议类型,默认为ModbusRTU</param>
/// <returns>解析后的bool/byte/ushort/int/float数组</returns>
public object ParseModbusData(byte[] data, ByteOrder byteOrder = ByteOrder.NONE, ModbusType modbusType = ModbusType.SERIAL_RTU)
{
error = Errors.NO_ERROR;
try
{
//0、根据连接类型,把原始数据进行转换为标准的byte[]
List<byte> temp = new List<byte>();
if (modbusType == ModbusType.SERIAL_RTU)
{
temp.AddRange(data);
}
else if (modbusType == ModbusType.SERIAL_ASCII)
{
//ModbusASCII帧头和帧尾检测合格后对原始数据进行转换
if (data[0] == 0x3A && data[data.Length - 2] == 0x0D && data[data.Length - 1] == 0x0A)
{
byte[] btemp = data.Skip(1).Take(data.Length - 3).ToArray();
string tempString = Encoding.ASCII.GetString(btemp);
temp.AddRange(HexStringToBytes(tempString));
}
else
{
error = Errors.WRONG_RESPONSE_VALUE;
return null;
}
}
else if (modbusType == ModbusType.TCP_IP)
{
temp = data.Skip(6).Take(data.Length - 6).ToList();
}
byte[] source = temp.ToArray();
//1、检测是否为返回命令数据
bool isCode = false;
foreach (ModbusCodes Mc in Enum.GetValues(typeof(ModbusCodes)))
{
//不是返回命令,也不是返回异常码
if (source[1] == Mc.GetHashCode() || source[1] == (Mc.GetHashCode() + 0x80))
{
isCode = true;
break;
}
}
if (!isCode)
{
error = Errors.EXCEEDING_MODBUSCODE_RANGE;
return null;
}
//2、检测CRC16或LRC是否正确(ModbusTCP无此校验)
if (modbusType == ModbusType.SERIAL_RTU)//CRC16是否正确
{
byte[] crc16Temp = CRC16.CRCCalc(source, 0, source.Length - 2);
if (crc16Temp[0] != source[source.Length - 2] && crc16Temp[1] != source[source.Length - 1])
{
error = Errors.WRONG_CRC;
return null;
}
}
if (modbusType == ModbusType.SERIAL_ASCII)//LRC是否正确
{
byte lrc = LRC8.LRCCalc(source, 0, source.Length - 1);
if (lrc != source[source.Length - 1])
{
error = Errors.WRONG_LRC;
return null;
}
}
//3、解析功能码0x01,0x02查询线圈或离散量开关量数据,所以用bool类型
if (source[1] == 0x01 || source[1] == 0x02)
{
//3.1、把有效数据从返回数据的数组中取出来
byte[] values = new byte[source[2]];
Array.Copy(source, 3, values, 0, values.Length);
//3.2、把数据放入BitArray数组,它会按byte值生成由低位到高位的bool数组,正好与Modbus返回开关量由低到高的顺序一致。
BitArray barray = new BitArray(values);
//3.3、生成bool数组,把BitArray生成的开关量取回,顺序是由低到高
bool[] results = new bool[barray.Length];
barray.CopyTo(results, 0);
//所以,功能码0x01和0x02返回的数据是bool数组,顺序是低位地址到高位地址的开关状态,bool数组是长度是以字节顺序
//转换的,所以是8(bit)的倍数,大于等于要查询的线圈或离散量的数量,从低到高取要查看的开关量数量即可,最后多余
//出来的是从站返回数据时自动补的0(false)。
return results;
}
//4、解析功能码0x03、0x04查询寄存器值的数据,在一些PLC、使用485传输或其他设备上,多数会取一个寄存器(两个字节)或
//两个寄存器的值,用以表示电流、电压、温度或者加工时用到的参数等数据,那么就会涉及到取回值是int,还是float等类型。
if (source[1] == 0x03 || source[1] == 0x04)
{
//4.1、把有效数据从返回数据的数组中取出来
byte[] values = new byte[source[2]];
Array.Copy(source, 3, values, 0, values.Length);
//4.2、根据给定参数来决定返回值,如果没有指定类型,则原样返回取出来的数据
if (byteOrder == ByteOrder.NONE)
{
return values;
}
//4.3、目前只考虑WORD、int和float常用类型
//拆解字节类型与顺序枚举:byOrder[0]表示两个或四个字节,byOrder[1]表示类型是WORD、int、float,byOder[2]表示取值时的字节顺序
string[] byOrder = Enum.GetName(typeof(ByteOrder), byteOrder).Split('_');
//4.3.1、WORD类型(无符号)
if (byOrder[0] == "TWO" && byOrder[1] == "WORD")
{
//WORD类型必须是两个字节为一组,先把数据value两个两个分好组,如果有余数,则弃掉
ushort[] result = new ushort[values.Length / 2];
//假如返回零个或一个字节数据,那肯定有误,不可能发生,但是还是防止吧。
if (result.Length == 0)
{
error = Errors.WRONG_RESPONSE_REGISTERS;
return null;
}
if (byOrder[2] == "12")
{
for (int i = 0; i < result.Length; i++)
{
//BitConvert.ToUInt16转换byte数组,byte[0]是低字节,byte[1]是高字节
result[i] = BitConverter.ToUInt16(new byte[] { values[i * 2 + 1], values[i * 2] }, 0);
//上面语句相当于以下语句
//result[i] = (ushort)((values[i * 2] << 8) | (values[i * 2 + 1] & 0x00FF));
}
return result;
}
if (byOrder[2] == "21")
{
for (int i = 0; i < result.Length; i++)
{
//BitConvert.ToUInt16转换byte数组,byte[0]是低字节,byte[1]是高字节
result[i] = BitConverter.ToUInt16(new byte[] { values[i * 2], values[i * 2 + 1] }, 0);
//上面语句相当于以下语句
//result[i] = (ushort)((values[i * 2] & 0x00FF) | (values[i * 2 + 1] << 8));
}
return result;
}
}
//4.3.2、int类型
if (byOrder[0] == "FOUR" && byOrder[1] == "INT")
{
//解析返回的int数组
int[] result = new int[values.Length / 4];
//假如返回0个至3个字节数据,那肯定有误,不可能发生,但是还是防止吧。
if (result.Length == 0)
{
error = Errors.WRONG_RESPONSE_REGISTERS;
return null;
}
for (int i = 0; i < result.Length; i++)
{
byte No1 = values[i * 4];
byte No2 = values[i * 4 + 1];
byte No3 = values[i * 4 + 2];
byte No4 = values[i * 4 + 3];
switch (byOrder[2])
{
case "1234":
//大端1234,在BitConvert.ToInt32时,输入的byte[]数组是按小端顺序,
//即为从右向左赋值顺序是1234(4 <- 3 <- 2 <- 1)
result[i] = BitConverter.ToInt32(new byte[] { No4, No3, No2, No1 }, 0);
break;
case "1243":
//大端1243,转为小端应从右向左赋值(3 <- 4 <- 2 <- 1)
result[i] = BitConverter.ToInt32(new byte[] { No3, No4, No2, No1 }, 0);
break;
case "2134":
//大端2143,转小端也可以认为是把2143前后倒过来,即4312
result[i] = BitConverter.ToInt32((new byte[] { No2, No1, No3, No4 }).Reverse().ToArray(), 0);
break;
case "2143":
result[i] = BitConverter.ToInt32(new byte[] { No3, No4, No1, No2 }, 0);
break;
case "3412":
result[i] = BitConverter.ToInt32(new byte[] { No2, No1, No4, No3 }, 0);
break;
case "3421":
result[i] = BitConverter.ToInt32(new byte[] { No1, No2, No4, No3 }, 0);
break;
case "4312":
result[i] = BitConverter.ToInt32(new byte[] { No2, No1, No3, No4 }, 0);
break;
case "4321":
result[i] = BitConverter.ToInt32(new byte[] { No1, No2, No3, No4 }, 0);
break;
}
}
return result;
}
//4.3.3、float类型
if (byOrder[0] == "FOUR" && byOrder[1] == "FLOAT")
{
//解析返回的float数组
float[] result = new float[values.Length / 4];
//假如返回0个至3个字节数据,那肯定有误,不可能发生,但是还是防止吧。
if (result.Length == 0)
{
error = Errors.WRONG_RESPONSE_REGISTERS;
return null;
}
for (int i = 0; i < result.Length; i++)
{
byte No1 = values[i * 4];
byte No2 = values[i * 4 + 1];
byte No3 = values[i * 4 + 2];
byte No4 = values[i * 4 + 3];
switch (byOrder[2])
{
case "1234":
//大端1234,在BitConvert.ToSingle)时,输入的byte[]数组是按小端顺序,
//即为从右向左赋值顺序是1234(4 <- 3 <- 2 <- 1)
result[i] = BitConverter.ToSingle(new byte[] { No4, No3, No2, No1 }, 0);
break;
case "1243":
//大端1243,转为小端应从右向左赋值(3 <- 4 <- 2 <- 1)
result[i] = BitConverter.ToSingle(new byte[] { No3, No4, No2, No1 }, 0);
break;
case "2134":
//大端2143,转小端也可以认为是把2143前后倒过来,即4312
result[i] = BitConverter.ToSingle((new byte[] { No2, No1, No3, No4 }).Reverse().ToArray(), 0);
break;
case "2143":
result[i] = BitConverter.ToSingle(new byte[] { No3, No4, No1, No2 }, 0);
break;
case "3412":
result[i] = BitConverter.ToSingle(new byte[] { No2, No1, No4, No3 }, 0);
break;
case "3421":
result[i] = BitConverter.ToSingle(new byte[] { No1, No2, No4, No3 }, 0);
break;
case "4312":
result[i] = BitConverter.ToSingle(new byte[] { No2, No1, No3, No4 }, 0);
break;
case "4321":
result[i] = BitConverter.ToSingle(new byte[] { No1, No2, No3, No4 }, 0);
break;
}
}
return result;
}
}
//5、功能码0x05、0x06、0x0F、0x10指令的返回数据与发送时基本相同,0x05、0x06完全相同,0x0F、0x10前6个字节相同,CRC16不同。
//所以取返回数据意义不大。
if (source[1] == 0x05 || source[1] == 0x06 || source[1] == 0x0F || source[1] == 0x10)
{
/*
* 有用时再写相关内容
*/
error = Errors.UNPROCESSED_MODBUSCODE;
return null;
}
//6、异常处理
if (source[1] > 0x80)
{
switch (source[2])
{
case 1:
error = Errors.EXCEPTION_ILLEGAL_FUNCTION;//非法的功能码
break;
case 2:
error = Errors.EXCEPTION_ILLEGAL_DATA_ADDRESS;//非法的数据地址
break;
case 3:
error = Errors.EXCEPTION_ILLEGAL_DATA_VALUE;//非法的数据值
break;
case 4:
error = Errors.EXCEPTION_SLAVE_DEVICE_FAILURE;//从站(服务器)故障
break;
}
return null;
}
//7、其他ModbusRTU指令,这里不做解析
error = Errors.UNPROCESSED_MODBUSCODE;
return null;
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
}
#endregion
}
#endregion
3.调用举例
上面的解析数据方法返回类型为object,如何把它转为值类型呢?我们先看一下Modicon PLC取值时的字节解码顺序:
-
32位整数解码顺序:调整双字元件的解码顺序,对于Modicon PLC,一般设置为“2-3412”顺序解码。
32 位整数解码顺序 举例: 0x0000 0001
0―1234 表示双字元件不做处理直接解码(默认值) 表示 1
1―2143 表示双字元件高低字不颠倒,但字内高低字节颠倒 表示256
2—3412表示双字元件高低字颠倒,但字内高低字节不颠倒 表示65536
3—4321表示双字元件内4个字节全部颠倒 表示 16777216 -
32位浮点数解码顺序:调整双字元件的解码顺序,对于Modicon PLC,一般设置为“2-3412”顺序解码。
32 位浮点数解码顺序 举例:0x3F80 0000
0―1234 表示双字元件不做处理直接解码(默认值) 表示 1.0
1―2143 表示双字元件高低字不颠倒,但字内高低字节颠倒 表示-5.78564e-039
2—3412表示双字元件高低字颠倒,但字内高低字节不颠倒 表示2.27795e-041
3—4321表示双字元件内4 个字节全部颠倒 表示 4.60060e-041
我们就以Modicon PLC数值举例,分别构建ModbusRTU 和 Modbus ASCII的0x03功能码返回数据,调用解析方法来验证int和float的数值结果,下面先看ModbusRTU返回数据解析的代码。
//数据值为 0x0000 0001
byte[] sources = new byte[] { 0x01, 0x03, 0x04, 0x00, 0x00, 0x00, 0x01, 0x3B, 0xF3 };
Modbus MRTU = new Modbus();
int[] backint1234 = (int[])MRTU.ParseModbusData(sources, ByteOrder.FOUR_INT_1234, ModbusType.SERIAL_RTU);//输出结果是: 1
int[] backint2143 = (int[])MRTU.ParseModbusData(sources, ByteOrder.FOUR_INT_2143, ModbusType.SERIAL_RTU);//输出结果是: 256
int[] backint3412 = (int[])MRTU.ParseModbusData(sources, ByteOrder.FOUR_INT_3412, ModbusType.SERIAL_RTU);//输出结果是: 65536
int[] backint4321 = (int[])MRTU.ParseModbusData(sources, ByteOrder.FOUR_INT_4321, ModbusType.SERIAL_RTU);//输出结果是: 16777216
//数据值为 0x3F80 0000
sources = new byte[] { 0x01, 0x03, 0x04, 0x3F, 0x80, 0x00, 0x00, 0xF4, 0xCF };
float[] backFloat1234 = (float[])MRTU.ParseModbusData(sources, ByteOrder.FOUR_FLOAT_1234, ModbusType.SERIAL_RTU);//输出结果是: 1.0
float[] backFloat2143 = (float[])MRTU.ParseModbusData(sources, ByteOrder.FOUR_FLOAT_2143, ModbusType.SERIAL_RTU);//输出结果是: -5.78564e-039
float[] backFloat3412 = (float[])MRTU.ParseModbusData(sources, ByteOrder.FOUR_FLOAT_3412, ModbusType.SERIAL_RTU);//输出结果是: 2.27795e-041
float[] backFloat4321 = (float[])MRTU.ParseModbusData(sources, ByteOrder.FOUR_FLOAT_4321, ModbusType.SERIAL_RTU);//输出结果是: 4.60060e-041
经过调用测试,程序运行结果完全正确。
我们再来看Modbus ASCII的0x03功能码返回数据解析例子,大家知道Modbus ASCII的返回数据同样是ASCII码可见字符的值,所以它的返回数据长度是ModbusRTU返回数据的2倍多。
Modbus MASCII = new Modbus();
//下面是Modbus ASCII可见字符的返回数据“: 01 03 04 00 00 00 01 F7 \r \n”的ASCII值
byte[] dataASCII = new byte[] { 0x3A, 0x30, 0x31, 0x30, 0x33, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x66, 0x37, 0x0D, 0x0A };
int[] backint1234 = (int[])MASCII.ParseModbusData(dataASCII, ByteOrder.FOUR_INT_1234, ModbusType.SERIAL_ASCII);//1
int[] backint2143 = (int[])MASCII.ParseModbusData(dataASCII, ByteOrder.FOUR_INT_2143, ModbusType.SERIAL_ASCII);//256
int[] backint3412 = (int[])MASCII.ParseModbusData(dataASCII, ByteOrder.FOUR_INT_3412, ModbusType.SERIAL_ASCII);//65536
int[] backint4321 = (int[])MASCII.ParseModbusData(dataASCII, ByteOrder.FOUR_INT_4321, ModbusType.SERIAL_ASCII);//16777216
//数据值为 0x3F80 0000
//下面是Modbus ASCII可见字符的返回数据“: 01 03 04 3F 80 00 00 39 \r \n”的ASCII值
dataASCII = new byte[] { 0x3A, 0x30, 0x31, 0x30, 0x33, 0x30, 0x34, 0x33, 0x66, 0x38, 0x30, 0x30, 0x30, 0x30, 0x30, 0x33, 0x39, 0x0D, 0x0A };
float[] backFloat1234 = (float[])MASCII.ParseModbusData(dataASCII, ByteOrder.FOUR_FLOAT_1234, ModbusType.SERIAL_ASCII);//1.0
float[] backFloat2143 = (float[])MASCII.ParseModbusData(dataASCII, ByteOrder.FOUR_FLOAT_2143, ModbusType.SERIAL_ASCII);//-5.78564e-039
float[] backFloat3412 = (float[])MASCII.ParseModbusData(dataASCII, ByteOrder.FOUR_FLOAT_3412, ModbusType.SERIAL_ASCII);//2.27795e-041
float[] backFloat4321 = (float[])MASCII.ParseModbusData(dataASCII, ByteOrder.FOUR_FLOAT_4321, ModbusType.SERIAL_ASCII);//4.60060e-041
同样在VS环境下测试结果完全正确。ModbusTCP返回数据与ModbusRTU基本一样,所以此处忽略。
4.说明
Modbus返回数据解析方法及调用我们已经完全结出代码,经过测试也是解析正确。这其中有几点要说明一下,1、结果返回如果null,请查看error的值来确定异常,如果error为Errors.UNPROCESSED_MODBUSCODE,则说明解析的不是功能码0x01-0x04的返回数据,如果error是Errors.EXCEPTION_UNKNOWN,那说明返回数据有问题。2、功能码0x01和0x02返回数据解析出来的是bool数组,它的长度要大于等于查询线圈或离散量的数量的,因为从站返回是以字节为单位的,请根据查询时的数量来自己截止,同时注意bool数组输出的开关量是从查询时的起始地址到高地址时行排列的,即bool[0]代表查询线圈或离散量的起始地址,bool[N]是高地址,是最后一个查询线圈或离散量的开关量值。3、代码中的CRC校验的方法CRC16.CRCCalc()和LRC校验方法LRC8.CRCCalc()本文没有列出来,如果需要,可以参考我另外两篇“C# Modbus的CRC16校验代码”和"C# Modbus ASCII的LRC校验代码"的文章。4、解析方法的第一个参数data,是Modbus返回的所有数据,不要自己加工后再代入,否则结果会异常。