1.概述
Modbus是一种串行通信协议,主要应用于电子控制器上的一种通用语言。常用的ModbusRTU、Modbus ASCII和ModbusTCP,Modbus ASCII相对来说要用的少一些。
Modbus ASCII协议是一种主从式串行异步半双工通信协议,是基于RS485\RS422\RS232物理层的通信的,在协议通信中每个字符通信格式规定为1个起始位、7个数据位、1个校验位、1个停止位,并且有LRC错误校验。
Modbus RTU采用的是字节流(字节值为0x00至0xFF)传输,多数是无法用文本显示出来的(可打印出来的字符在0x20至0x7E之间,只有95个),而Modbus ASCII采用的是可打印字符0到9(字节为0x30至0x39)、A至F(字节为0x41至0x46)、冒号(字节0x3A)、回车符(字节为0x0D)、换行符(字节为0x0A)进行传输,也可以说是文本传输。Modbus ASCII其实跟ModbusRTU很相似,其寄存器种类及主要功能命令都一样,只是传输数据的格式略有区别,简单说除了有效数据以可打印的ASCII码通信外,就是在ModbusRTU的数据帧前面加了帧头冒号(:),把ModbusRTU的CRC校验去掉,换成LRC校验,并增加了结束符回车换行,其它数据内容完全一样。请参看下面两张图:
从图中可以看出,ModbusRTU传输的字节流就是Modbus指令,对应的ASCII码可以说是乱码,多数不是打印字符。而Modbus ASCII是用可显示(可打印)的字符组装成Modbus指令,把它们转换为对应的ASCII码值,加上帧头冒号的ASCII码值(0x3A)和帧尾的回车换行符的ASCII码值(0x0D、0x0A),包装成字节流形式的信息帧作为最终指令,再传输发送出去(返回信息帧也是一样的格式)。因此,形成Modbus ASCII命令功能码,我们首先要用可显示ASCII码字符拼写指令,然后再将它们转换为相应的ASCII值进行发送,同样收到返回信息后,也要先将ASCII值转换为对应的ASCII可显示字符,就能看出具体是哪个功能码返回的信息了,然后再把这些ASCII可显示字符转换对应的多数不可显示的Modbus字节流返回信息。
由于两个可显示ASCII字符对应的一个不可显示字节,每个ASCII字符又占一个字节,所以Modbus ASCII的信息帧就比ModbusRTU要长一倍多,一般情况下,Modbus功能码0x01、0x02、0x03、0x04的ModbusRTU指令是8个字节,而Modbus ASCII的指令就会达到17个字节,所以传输效率要低一些,适用于对传输速率要求不高的场景。
ModbusRTU和ModbusTCP不涉及ASCII字符转换,可以用字节流直接形成指令,具体可参考我另外两篇文章“C# ModbusRTU命令功能码”和"C# ModbusTCP命令功能码"。
2.Modbus ASCII 通信协议寄存器种类
寄存器种类 | 说明 | 与PLC比较 | 举例说明 |
---|---|---|---|
线圈状态 Coil Status | 数字量输出、继电器输出,可读可写 | DO数字量输出或内部读写位变量 | 电磁阀输出、继电器 |
离散量输入 Input Status | 数字量输入,只读 | DI数字量输入或内部只读位量 | 按钮输入、拨码开关、接近开关 |
保持寄存器 Holding Register | 输出参数、保持参数,可读可写 | AO模拟量输出或内部读写寄存器 | 模拟量输出设定、变量阀输出大小 |
输入寄存器 Input Register | 输入参数,只读 | AI模拟量输入或内部只读寄存器 | 模拟量输入、现场工程量信号采集 |
3.Modbus ASCII的八条主要功能码命令
功能码 | 作 用 |
---|---|
0x01 | 读线圈 |
0x02 | 读离散量输入 |
0x03 | 读保持寄存器 |
0x04 | 读输入寄存器 |
0x05 | 写单个线圈 |
0x06 | 写单个寄存器 |
0x0F | 写多个线圈 |
0x10 | 写多个寄存器 |
4.Modbus ASCII的八条命令生成代码
在前面我已经简单介绍了Modbus ASCII指令形成的基本原理了,它实际就是对ModbusRTU的8条指令做了相应的修改,一是把CRC替换为LRC,二是增加帧头和帧尾,三是转换为可显示的ASCII字符,四是再转换为对应的ASCII值的byte数组。和我在ModbusRTU生成的八条功能代码一样,本文的ModbusASCII的八条代码一样是byte数组类型,不涉及Modbus总线传输部分,因为它可以即可以应用于串口通信,也可以应用于RS232/485/422转网络传输的环境,如想了解串行总线的传输代码,请参考我另一篇文章"C# 串口发送ModbusRTU、ModbusASCII的8条主要功能代码并获取返回的响应信息"。
下面我们还是需要先声明三个枚举,其中有几条枚举项现在用不上,其实还是为了能列出来更多的Modbus指令和在取Modbus返回数据时使用的。
/// <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,//从站(服务器)故障
}
下面的代码就是Modbus ASCII的主要八条功能码指令的代码了,返回类型是byte数组,可以直接代入程序中使用。
#region ModbusASCII
public class ModbusASCII
{
#region 常量
/// <summary>
/// 可读取的线圈的最大数量
/// </summary>
public const ushort MAX_COILS_IN_READ_NUM = 2000;
/// <summary>
/// 可读取的离散量的最大数量
/// </summary>
public const ushort MAX_DISCRETE_INPUTS_IN_READ_NUM = 2000;
/// <summary>
/// 可读取的保持寄存器的最大数量
/// </summary>
public const ushort MAX_HOLDING_REGISTERS_IN_READ_NUM = 125;
/// <summary>
/// 可读取的输入寄存器的最大数量
/// </summary>
public const ushort MAX_INPUT_REGISTERS_IN_READ_NUM = 125;
/// <summary>
/// 可写入的线圈的最大数量
/// </summary>
public const ushort MAX_COILS_IN_WRITE_NUM = 1968;
/// <summary>
/// 可写入的保持寄存器的最大数量
/// </summary>
public const ushort MAX_HOLDING_REGISTERS_IN_WRITE_NUM = 123;
/// <summary>
/// 可读取读/写的保持寄存器的最大数量
/// </summary>
public const ushort MAX_HOLDING_REGISTERS_TO_READ_IN_READWRITE_NUM = 125;
/// <summary>
/// 可写取读/写的保持寄存器的最大数量
/// </summary>
public const ushort MAX_HOLDING_REGISTERS_TO_WRITE_IN_READWRITE_NUM = 121;
#endregion
#region 变量
/// <summary>
/// Modbus错误
/// </summary>
public Errors error;
#endregion
#region 内部方法
/// <summary>
/// byte[]转换为等效的十六进制字符串,例如byte example = 32,转换字符串为"20"
/// </summary>
/// <param name="source">byte[]原数组</param>
/// <param name="formatStr">转换的格式,默认为十六进制</param>
private string BytesToHexString(byte[] source)
{
StringBuilder pwd = new StringBuilder();
foreach (byte btStr in source) { pwd.AppendFormat("{0:X2}", btStr); }
return pwd.ToString();
}
#endregion
#region 读取线圈,功能码0x01
/// <summary>
/// 生成ModbusASCII读取线圈状态指令byte数组
/// </summary>
/// <param name="SlaveId">从站号(服务器端)</param>
/// <param name="StartAddress">起始地址</param>
/// <param name="Numbers">读取线圈的数量</param>
/// <returns>读取线圈状态的指令byte[]</returns>
public byte[] ReadCoilStatus_0x01(byte SlaveId, ushort StartAddress, ushort Numbers)
{
error = Errors.NO_ERROR;
if (Numbers < 1)
{
error = Errors.ZERO_REGISTERS_REQUESTED;
return null;
}
if (Numbers > MAX_COILS_IN_READ_NUM)
{
error = Errors.TOO_MANY_REGISTERS_REQUESTED;
return null;
}
byte[] command = new byte[7];
try
{
//第1位是从站站号
command[0] = SlaveId;
//第2位是功能码0x01
command[1] = (byte)ModbusCodes.READ_COILS;
//第3和第4位起始地址(大端法)
Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
//第5位和第6位是线圈数量
Array.Copy(BitConverter.GetBytes(Numbers).Reverse().ToArray(), 0, command, 4, 2);
//第7位是LRC校验
command[6] = LRC8.LRCCalc(command, 0, 6);
//转换为16进制字符串后,组装成ModbusASCII命令格式,首字符为":",末尾字符为回车换行
string cmdString = ":" + BytesToHexString(command) + "\r\n";
//把ModbusASCII指令格式转换为等效的byte[]
byte[] AsciiCmd = Encoding.ASCII.GetBytes(cmdString);
return AsciiCmd;
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
}
#endregion
#region 读取离散量,功能码0x02
/// <summary>
/// 生成ModbusASCII读取离散量状态指令byte数组
/// </summary>
/// <param name="SlaveId">从站号(服务器端)</param>
/// <param name="StartAddress">起始地址</param>
/// <param name="Numbers">读取离散量的数量</param>
/// <returns>读取离散量状态的指令byte[]</returns>
public byte[] ReadInputStatus_0x02(byte SlaveId, ushort StartAddress, ushort Numbers)
{
error = Errors.NO_ERROR;
if (Numbers < 1)
{
error = Errors.ZERO_REGISTERS_REQUESTED;
return null;
}
if (Numbers > MAX_DISCRETE_INPUTS_IN_READ_NUM)
{
error = Errors.TOO_MANY_REGISTERS_REQUESTED;
return null;
}
byte[] command = new byte[7];
try
{
//第1位是从站站号
command[0] = SlaveId;
//第2位是功能码0x02
command[1] = (byte)ModbusCodes.READ_DISCRETE_INPUTS;
//第3和第4位起始地址(大端法)
Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
//第5位和第6位是线圈数量
Array.Copy(BitConverter.GetBytes(Numbers).Reverse().ToArray(), 0, command, 4, 2);
//第7位是LRC校验
command[6] = LRC8.LRCCalc(command, 0, 6);
//转换为16进制字符串后,组装成ModbusASCII命令格式,首字符为":",末尾字符为回车换行
string cmdString = ":" + BytesToHexString(command) + "\r\n";
//把ModbusASCII指令格式转换为等效的byte[]
byte[] AsciiCmd = Encoding.ASCII.GetBytes(cmdString);
return AsciiCmd;
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
}
#endregion
#region 读取保持寄存器,功能码0x03
/// <summary>
/// 生成ModbusASCII读取保持寄存器指令byte数组
/// </summary>
/// <param name="SlaveId">从站号(服务器端)</param>
/// <param name="StartAddress">起始地址</param>
/// <param name="Numbers">读取保持寄存器的数量</param>
/// <returns>读取保持寄存器的指令byte[]</returns>
public byte[] ReadHoldingRegister_0x03(byte SlaveId, ushort StartAddress, ushort Numbers)
{
error = Errors.NO_ERROR;
if (Numbers < 1)
{
error = Errors.ZERO_REGISTERS_REQUESTED;
return null;
}
if (Numbers > MAX_HOLDING_REGISTERS_IN_READ_NUM)
{
error = Errors.TOO_MANY_REGISTERS_REQUESTED;
return null;
}
byte[] command = new byte[7];
try
{
//第1位是从站站号
command[0] = SlaveId;
//第2位是功能码0x03
command[1] = (byte)ModbusCodes.READ_HOLDING_REGISTERS;
//第3和第4位起始地址(大端法)
Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
//第5位和第6位是线圈数量
Array.Copy(BitConverter.GetBytes(Numbers).Reverse().ToArray(), 0, command, 4, 2);
//第7位是LRC校验
command[6] = LRC8.LRCCalc(command, 0, 6);
//转换为16进制字符串后,组装成ModbusASCII命令格式,首字符为":",末尾字符为回车换行
string cmdString = ":" + BytesToHexString(command) + "\r\n";
//把ModbusASCII指令格式转换为等效的byte[]
byte[] AsciiCmd = Encoding.ASCII.GetBytes(cmdString);
return AsciiCmd;
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
}
#endregion
#region 读取输入寄存器,功能码0x04
/// <summary>
/// 生成ModbusASCII读取输入寄存器指令byte数组
/// </summary>
/// <param name="SlaveId">从站号(服务器端)</param>
/// <param name="StartAddress">起始地址</param>
/// <param name="Numbers">读取输入寄存器的数量</param>
/// <returns>读取输入寄存器的指令byte[]</returns>
public byte[] ReadInputRegister_0x04(byte SlaveId, ushort StartAddress, ushort Numbers)
{
error = Errors.NO_ERROR;
if (Numbers < 1)
{
error = Errors.ZERO_REGISTERS_REQUESTED;
return null;
}
if (Numbers > MAX_INPUT_REGISTERS_IN_READ_NUM)
{
error = Errors.TOO_MANY_REGISTERS_REQUESTED;
return null;
}
byte[] command = new byte[7];
try
{
//第1位是从站站号
command[0] = SlaveId;
//第2位是功能码0x04
command[1] = (byte)ModbusCodes.READ_INPUT_REGISTERS;
//第3和第4位起始地址(大端法)
Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
//第5位和第6位是线圈数量
Array.Copy(BitConverter.GetBytes(Numbers).Reverse().ToArray(), 0, command, 4, 2);
//第7位是LRC校验
command[6] = LRC8.LRCCalc(command, 0, 6);
//转换为16进制字符串后,组装成ModbusASCII命令格式,首字符为":",末尾字符为回车换行
string cmdString = ":" + BytesToHexString(command) + "\r\n";
//把ModbusASCII指令格式转换为等效的byte[]
byte[] AsciiCmd = Encoding.ASCII.GetBytes(cmdString);
return AsciiCmd;
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
}
#endregion
#region 写单个线圈,功能码0x05
/// <summary>
/// 生成ModbusASCII写单个线圈指令byte数组
/// </summary>
/// <param name="SlaveId">从站号(服务器端)</param>
/// <param name="StartAddress">起始地址</param>
/// <param name="Value">单个线圈是开启(true)还是关闭(false)</param>
/// <returns>写单个线圈的指令byte[]</returns>
public byte[] WriteSingleCoil_0x05(byte SlaveId, ushort StartAddress, bool Value)
{
error = Errors.NO_ERROR;
byte[] command = new byte[7];
try
{
//第1位是从站站号
command[0] = SlaveId;
//第2位是功能码0x05
command[1] = (byte)ModbusCodes.WRITE_SINGLE_COIL;
//第3和第4位起始地址(大端法)
Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
//第5位和第6位是开关标识,0xFF00为开,0x0000为关
command[4] = Value ? (byte)0xFF : (byte)0x00;
command[5] = 0x00;
//第7位是LRC校验
command[6] = LRC8.LRCCalc(command, 0, 6);
//转换为16进制字符串后,组装成ModbusASCII命令格式,首字符为":",末尾字符为回车换行
string cmdString = ":" + BytesToHexString(command) + "\r\n";
//把ModbusASCII指令格式转换为等效的byte[]
byte[] AsciiCmd = Encoding.ASCII.GetBytes(cmdString);
return AsciiCmd;
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
}
#endregion
#region 写单个保持寄存器,功能码0x06
/// <summary>
/// 生成ModbusASCII写单个保持寄存器指令byte数组
/// </summary>
/// <param name="SlaveId">从站号(服务器端)</param>
/// <param name="StartAddress">起始地址</param>
/// <param name="Value">单个保持寄存器的值,类型是无符号16位整数</param>
/// <returns>写单个保持寄存器的指令byte[]</returns>
public byte[] WriteSingleRegister_0x06(byte SlaveId, ushort StartAddress, ushort Value)
{
error = Errors.NO_ERROR;
byte[] command = new byte[7];
try
{
//第1位是从站站号
command[0] = SlaveId;
//第2位是功能码0x06
command[1] = (byte)ModbusCodes.WRITE_SINGLE_REGISTER;
//第3和第4位起始地址(大端法)
Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
//第5位和第6位是开关标识,0xFF00为开,0x0000为关
Array.Copy(BitConverter.GetBytes(Value).Reverse().ToArray(), 0, command, 4, 2);
//第7位是LRC校验
command[6] = LRC8.LRCCalc(command, 0, 6);
//转换为16进制字符串后,组装成ModbusASCII命令格式,首字符为":",末尾字符为回车换行
string cmdString = ":" + BytesToHexString(command) + "\r\n";
//把ModbusASCII指令格式转换为等效的byte[]
byte[] AsciiCmd = Encoding.ASCII.GetBytes(cmdString);
return AsciiCmd;
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
}
#endregion
#region 写多个线圈,功能码0x0F
/// <summary>
/// 生成ModbusASCII写多个线圈指令byte数组
/// </summary>
/// <param name="SlaveId">从站号(服务器端)</param>
/// <param name="StartAddress">起始地址</param>
/// <param name="Values">多个线圈的写入状态,从起始地址线圈开始依次排列</param>
/// <returns>写多个线圈的指令byte[]</returns>
public byte[] WriteMultipleCoil_0x0F(byte SlaveId, ushort StartAddress, bool[] Values)
{
error = Errors.NO_ERROR;
if (Values == null)
{
error = Errors.ZERO_REGISTERS_REQUESTED;
return null;
}
if (Values.Length < 1)
{
error = Errors.ZERO_REGISTERS_REQUESTED;
return null;
}
if (Values.Length > MAX_COILS_IN_WRITE_NUM)
{
error = Errors.TOO_MANY_REGISTERS_REQUESTED;
return null;
}
try
{
//线圈数值长度(按字节算)
byte ValuesBytesCount = Convert.ToByte(Math.Ceiling((double)Values.Count() / 8));
//byte ValueBytesCount = (byte)((Value.Length / 8) + ((Value.Length % 8) == 0 ? 0 : 1));
byte[] command = new byte[7 + 1 + ValuesBytesCount];
//第1位是从站站号
command[0] = SlaveId;
//第2位是功能码0x06
command[1] = (byte)ModbusCodes.WRITE_MULTIPLE_COILS;
//第3和第4位起始地址(大端法)
Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
//第5位和第6位是线圈数值长度(接位数算)
Array.Copy(BitConverter.GetBytes((ushort)Values.Length).Reverse().ToArray(), 0, command, 4, 2);
//第7位是线圈数值长度(字节数)
command[6] = ValuesBytesCount;
/* 后面是线圈ValueBytesCount个字节的数值,在Modbus中是以大端法排列,每个字节的二进值对应的线圈顺序是
* 【87654321】 【16 15 14 13 12 11 10 9】 【24 23 22 21 20 19 18 17】 ......(每个【】代表1个字节)。
* 我们使用BitArray类将传入的Values(bool数组)转换为ModbusRTU所需要的大端byte数组。BitArray是位数组,
* 位的排列是小端法,所以当用参数Value以bool数组初始化时,它会跟bool数组一样,索引0对应bool数组的索引0,
* 1对应1,依次往后排列。当它用CopyTo方法转换为byte数组时,byte数组二进制值就会变成上面说的那样的顺序---
* 【87654321】 【16 15 14 13 12 11 10 9】 【24 23 22 21 20 19 18 17】 ......
* 例如:我们输入一个bool数组,new bool[]{false,ture,false, .../中间N位都是false/... ,false,true},
* 相当于0100......0001,用BitArray转换为byte[]后,会成为1000....0010这样的值。
*
*/
byte[] dataValue = new byte[ValuesBytesCount];
BitArray Ba = new BitArray(Values);
Ba.CopyTo(dataValue, 0);
//把线圈值按字节加入command中
Array.Copy(dataValue, 0, command, 7, ValuesBytesCount);
//最后一位是LRC校验值
command[7 + ValuesBytesCount] = LRC8.LRCCalc(command, 0, 7 + ValuesBytesCount);
//转换为16进制字符串后,组装成ModbusASCII命令格式,首字符为":",末尾字符为回车换行
string cmdString = ":" + BytesToHexString(command) + "\r\n";
//把ModbusASCII指令格式转换为等效的byte[]
byte[] AsciiCmd = Encoding.ASCII.GetBytes(cmdString);
return AsciiCmd;
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
}
#endregion
#region 写多个保持寄存器,功能码0x10
/// <summary>
/// 生成ModbusASCII写多个保持寄存器指令byte数组
/// </summary>
/// <param name="SlaveId">从站号(服务器端)</param>
/// <param name="StartAddress">起始地址</param>
/// <param name="Values">多个保持寄存器的值(无符号16位),从起始地址开始依次排列</param>
/// <returns>写多个保持寄存器的指令byte[]</returns>
public byte[] WriteMultipleRegister_0x10(byte SlaveId, ushort StartAddress, ushort[] Values)
{
error = Errors.NO_ERROR;
if (Values == null)
{
error = Errors.ZERO_REGISTERS_REQUESTED;
return null;
}
if (Values.Length < 1)
{
error = Errors.ZERO_REGISTERS_REQUESTED;
return null;
}
if (Values.Length > MAX_HOLDING_REGISTERS_IN_WRITE_NUM)
{
error = Errors.TOO_MANY_REGISTERS_REQUESTED;
return null;
}
try
{
//保持寄存器的字节数
byte ValuesBytesCount = (byte)(Values.Length * 2);
byte[] command = new byte[7 + 1 + ValuesBytesCount];
//第1位是从站站号
command[0] = SlaveId;
//第2位是功能码0x06
command[1] = (byte)ModbusCodes.WRITE_MULTIPLE_REGISTERS;
//第3和第4位起始地址(大端法)
Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
//第5位和第6位是写入保持寄存器的数量
Array.Copy(BitConverter.GetBytes((ushort)Values.Length).Reverse().ToArray(), 0, command, 4, 2);
//第7位是写入保持寄存器的字节数
command[6] = ValuesBytesCount;
//将写入寄存器的值加入command中
for (int i = 0; i < Values.Length; i++)
{
Array.Copy(BitConverter.GetBytes(Values[i]).Reverse().ToArray(), 0, command, 7 + i * 2, 2);
}
//最后一位是LRC校验值
command[7 + ValuesBytesCount] = LRC8.LRCCalc(command, 0, 7 + ValuesBytesCount);
//转换为16进制字符串后,组装成ModbusASCII命令格式,首字符为":",末尾字符为回车换行
string cmdString = ":" + BytesToHexString(command) + "\r\n";
//把ModbusASCII指令格式转换为等效的byte[]
byte[] AsciiCmd = Encoding.ASCII.GetBytes(cmdString);
return AsciiCmd;
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
}
#endregion
}
#endregion
5.代码说明
以上代码需要注意几点:1、如果代码返回null,请查看error的值,如果是Errors.EXCEPTION_UNKNOWN,那么多数是由于数组Copy产生的,说明输入参数有问题。2、功能码0x0F(写多个线圈)的参数Values是bool数组,是对一组线圈操作的,顺序应按线圈地址从低到高进行赋值,即Values[0]是线圈的起始地址的开关量,Values[N]是线圈的最高地址的开关量。3、代码中的CRC校验的方法LRC8.LRCCalc()本文没有列出来,如果需要,可以参考我另一篇“C# Modbus ASCII的LRC校验代码”的文章。