1.概述
Modbus是由Schneider Electric(施耐德电气)为PLC(可编程逻辑控制器)通信而研发的一种串行通信协议,是应用于电子控制器上的一种通用语言。常用的有两种ModbusRTU和ModbusTCP,还有一种Modbus ASCII,但是相对来说要用的少一些,如需了解,可参考我另一篇文章“C# Modbus ASCII命令功能码”。 Modbus RTU 协议是一种主从式串行异步半双工通信协议,是基于RS485\RS422\RS232物理层的通信的,在协议通信中每个字符通信格式规定为1个起始位、8个数据位、1个校验位、1\2个停止位,并且有CRC错误校验。 所谓主从,我们直接可以理解为发送请求的是主,也就是Master,常指我们用的上位机(电脑);那么响应请求的就是从,即Slave也就是PLC等485硬件设备,ModbusRTU指令中从站地址就是指的这些设备。 有人说Modbus服务器端和客户端,实际用的是网络通信用语,和上面说的主从一回事,服务器端是响应请求的,自然指的就是PLC等硬件设备,客户端是发送请求的,也就是指我们用的电脑了。在Modbus中,Slave和Server意思相同,Master和Client意思相同。
2.Modbus RTU 通信协议寄存器种类
寄存器种类 | 说明 | 与PLC比较 | 举例说明 |
线圈状态 Coil Status | 数字量输出、继电器输出,可读可写 | DO数字量输出或内部读写位变量 | 电磁阀输出、继电器 |
离散量输入 Input Status | 数字量输入,只读 | DI数字量输入或内部只读位量 | 按钮输入、拨码开关、接近开关 |
保持寄存器 Holding Register | 输出参数、保持参数,可读可写 | AO模拟量输出或内部读写寄存器 | 模拟量输出设定、变量阀输出大小 |
输入寄存器 Input Register | 输入参数,只读 | AI模拟量输入或内部只读寄存器 | 模拟量输入、现场工程量信号采集 |
3.ModbusRTU的八条主要功能码命令
功能码 | 作 用 |
0x01 | 读线圈 |
0x02 | 读离散量输入 |
0x03 | 读保持寄存器 |
0x04 | 读输入寄存器 |
0x05 | 写单个线圈 |
0x06 | 写单个寄存器 |
0x0F | 写多个线圈 |
0x10 | 写多个寄存器 |
4.ModbusRTU的八条命令生成代码
上面我们简单描述了ModbusRTU的一些基本知识,是为了我们生成ModbusRTU指令做准备,至于更多关于ModbusRTU的技术内容,大家可以自己网上查询,这里不再赘述。 ModbusRTU这8条常用指令,其实与ModbusTCP大致相同,只不过ModbusTCP多了一个7个字节的MBAP报文头和少了CRC校验,有兴趣了解的朋友可以参看我另一篇“C# ModbusTCP命令功能码”的文章。 本文介绍的是生成ModbusRTU主要八条命令的代码,它是在总线传输前的指令代码集,类型是二进制数组,不包含串口传输部分(如果需要串口传输代码,请参考我的文章"C# 串口发送ModbusRTU、ModbusASCII的8条主要功能代码并获取返回的响应信息"),但它是是串口传输的必不可少的一项,同样适用于采用ModbusRTU协议的485/422/232转网络的采集器(有人叫集中器),这种产品在多数环境下比Modbus总线更容易部署,对于熟悉网络的技术人员来说更加容易实现,避免了Modbus总线无法实现的星形连接方式,也不用担心增加120欧的终端电阻等方式,当然并不是说Modbus总线连接方式不好,Modbus总线连接有它独有的优势,这里只是就某种环境而言的。 在写这八条ModbusRTU指令代码之前,我们得先声明两个枚举,其中有几条枚举项现在用不上,但是我写上它是为了能列出来更多的Modbus指令和在取ModbusRTU返回数据时使用的。
/// <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,//从站(服务器)故障
}
下面就是ModbusRTU的主要八条命令代码,返回类型均是byte[],可直接代入程序运行。
#region Modbus RTU
public class ModbusRTU
{
#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 读取线圈,功能码0x01
/// <summary>
/// 生成ModbusRTU读取线圈状态指令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[8];
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位和第8位是CRC16校验
byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
Array.Copy(crc16, 0, command, 6, 2);
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
return command;
}
#endregion
#region 读取离散量,功能码0x02
/// <summary>
/// 生成ModbusRTU读取离散量状态指令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[8];
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位和第8位是CRC16校验
byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
Array.Copy(crc16, 0, command, 6, 2);
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
return command;
}
#endregion
#region 读取保持寄存器,功能码0x03
/// <summary>
/// 生成ModbusRTU读取保持寄存器指令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[8];
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位和第8位是CRC16校验
byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
Array.Copy(crc16, 0, command, 6, 2);
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
return command;
}
#endregion
#region 读取输入寄存器,功能码0x04
/// <summary>
/// 生成ModbusRTU读取输入寄存器指令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[8];
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位和第8位是CRC16校验
byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
Array.Copy(crc16, 0, command, 6, 2);
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
return command;
}
#endregion
#region 写单个线圈,功能码0x05
/// <summary>
/// 生成ModbusRTU写单个线圈指令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[8];
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位和第8位是CRC16校验
byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
Array.Copy(crc16, 0, command, 6, 2);
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
return command;
}
#endregion
#region 写单个保持寄存器,功能码0x06
/// <summary>
/// 生成ModbusRTU写单个保持寄存器指令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[8];
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位和第8位是CRC16校验
byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
Array.Copy(crc16, 0, command, 6, 2);
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
return command;
}
#endregion
#region 写多个线圈,功能码0x0F
/// <summary>
/// 生成ModbusRTU写多个线圈指令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[8 + 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,false,false,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);
//最后两位是CRC16校验
byte[] crc16 = CRC16.CRCCalc(command, 0, 7 + ValuesBytesCount);
Array.Copy(crc16, 0, command, 7 + ValuesBytesCount, 2);
return command;
}
catch
{
error = Errors.EXCEPTION_UNKNOWN;
return null;
}
}
#endregion
#region 写多个保持寄存器,功能码0x10
/// <summary>
/// 生成ModbusRTU写多个保持寄存器指令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[8 + 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);
}
//最后两位是CRC16校验
byte[] crc16 = CRC16.CRCCalc(command, 0, 7 + ValuesBytesCount);
Array.Copy(crc16, 0, command, 7 + ValuesBytesCount, 2);
return command;
}
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校验的方法CRC16.CRCCalc()本文没有列出来,如果需要,可以参考我另一篇“C# Modbus的CRC16校验代码”的文章。