1. Modbus概述
Modbus原先是施耐德电气(Schneider Electric)为PLC(可编程逻辑控制器)通信而研发的一种串行通信协议,现在它已经成为工业领域通信协议的业界标准,并且现在是工业电子、工业控制、电力等设备之间常用的连接方式。Modbus协议使用的是主从通讯技术,即由主设备主动查询和操作从设备。
另外,我们也常听说另外一种协议BACnet,它是智能建筑的通信协议,主要是针对智能建筑和控制系统,用于照明、暖气、空调、门禁、火警等相关设备。通常在智能建筑方面,我们会看到支持两种协议共存的设备,它们有一定的映射关系,可以相互转换。
2.Modbus RTU、ASCII和TCP
Modbus协议目前应用于串口、以太网以及其他支持互联网的网络,它有多种协议,但是常使用的是两个,即Modbus RTU和Modbus TCP。
在串行总线上运行时,Modbus 协议又分为RTU和ASCII两种模式。RTU是Remote Terminal Unit的缩写,意思是远程终端单元。ASCII是American Standard Code for Information Interchanged的缩写,意思是美国信息交换标准代码,是将文字编辑符号、大小写字母、数字和一些不可见的控制字符进行编码的一种字符表示形式,除了空字符外,共127个字符编码。
Modbus tcp/ip协议应用于以太网链接,ModbusTCP的数据帧去掉了ModbusRTU中CRC校验,增加了MBAP报文头,可分为两部分:ADU = MBAP + PDU,即 MBAP + 功能码 + 数据域,其中MBAP是7个字节,功能码1byte,数据域长度不确定,是由其具体功能而决定的。
至于ModbusRTU、ModbusASCII和ModbusTCP具体内容,可参考小姚同学的文章https://blog.csdn.net/YiWangJiuShiXingFu/article/details/105557082,另外对于ModbusRTU详解专题可参考仰望星空e的https://blog.csdn.net/qq_36339249/article/details/90664839,对于ModbusTCP的专题可参考sgmcumt的https://blog.csdn.net/sgmcumt/article/details/87435191
3.ModbusTCP的八个主要功能码命令
功能码 | 作 用 |
---|---|
0x01 | 读线圈 |
0x02 | 读离散量输入 |
0x03 | 读保持寄存器 |
0x04 | 读输入寄存器 |
0x05 | 写单个线圈 |
0x06 | 写单个寄存器 |
0x0F | 写多个线圈 |
0x10 | 写多个寄存器 |
4.ModbusTCP的八条命令生成代码
前面讲的其实都些Modbus的基本概念,如果大概了解其含义后,就可利用下面代码生成这八条功能码命令了,返回的是二进制数组,可直接通过TCP客户端(Sockets.TcpClient)发送到ModbusTCP设备。
本程序参数id=-40000是命令行的MBAP报文头1-2字节,其默认值-40000表示采用自动增量,也可以自己修改其值,表示自定义的报文编号。同时,程序异常处理程序Action<string, Exception> ModbusException同样也给程序员外暴异常处理方法,可根据需求去定义处理异常的代码。
#region Modbus TCP
/// <summary>
/// ModbusTCP的0x01-0x04读取线圈或寄存器的4个命令方法
/// 以及0x05-0x10写入线圈或寄存器的4个命令方法
/// </summary>
public class ModbusTCP
{
#region 报文序号
private ushort _head { get; set; } = 0;
private object OBJ = 0;
//报文号从1开始递增到65530后,再复位至1重新计数
private byte[] head()
{
lock (OBJ)
{
if (_head >= 65530)
{
_head = 0;
}
_head++;
return BitConverter.GetBytes(_head);
}
}
#endregion
#region MBAP报文头
/// <summary>
/// MBAP报文头1-2字节,报文序号
/// </summary>
private byte[] Head { get; set; } = { 0x00, 0x00 };
/// <summary>
/// MBAP报文头5-6字节,从第7字节(含第7字节)至报文结束总共长度
/// </summary>
private byte[] MBAPTotalLength { get; set; } = { 0x00, 0x00 };
/// <summary>
/// MBAP报文第7字节:从站地址(站号/点名)
/// </summary>
private byte slaveAddress { get; set; } = 0x00;
/// <summary>
/// MBAP报文头
/// </summary>
private byte[] MBAP
{
get
{
byte[] _MBAP = new byte[7] { 0, 0, 0, 0, 0, 0, 0 };
Array.Copy(Head, 0, _MBAP, 0, 2);
Array.Copy(MBAPTotalLength, 0, _MBAP, 4, 2);
_MBAP[6] = slaveAddress;
return _MBAP;
}
}
#endregion
#region 读取线圈,功能码0x01
/// <summary>
/// 读取线圈命令行,功能码0x01
/// </summary>
/// <param name="SlaveAddress">从站地址(站点)</param>
/// <param name="StartAddress">开始地址</param>
/// <param name="NumberOfPoints">线圈数量</param>
/// <param name="id">MBAP报文头(事务元标识符)【1-2字节】,默认自动进行增量编号</param>
/// <returns>0x01命令行</returns>
public byte[] ReadCoilStatus_0x01(int SlaveAddress, int StartAddress, int NumberOfPoints, int id = -40000)
{
//命令行
byte[] command = new byte[12];
try
{
//数量限制
if (NumberOfPoints > 2000)
{
throw new Exception("读取线圈数量不能超过2000");
}
//
ushort mStartAddress = (ushort)IPAddress.HostToNetworkOrder((short)StartAddress);
ushort mNumberOfPoints = (ushort)IPAddress.HostToNetworkOrder((short)NumberOfPoints);
//MBAP报文头组合,高低字节换位(与主机字节相反)
if (id == -40000)
{
Head = head().Reverse().ToArray();
}
else
{
Head = BitConverter.GetBytes((ushort)id).Reverse().ToArray();
}
MBAPTotalLength = BitConverter.GetBytes((ushort)(command.Length - 6)).Reverse().ToArray();
slaveAddress = Convert.ToByte(SlaveAddress);
//线圈起始地址,读取长度
byte[] start = BitConverter.GetBytes(mStartAddress);
byte[] length = BitConverter.GetBytes(mNumberOfPoints);
//命令行加载报文头
Array.Copy(MBAP, 0, command, 0, 7);
//功能码
command[7] = 0x01;//功能码
//命令行加载起始地址
Array.Copy(start, 0, command, 8, 2);
//命令行加载读取数量
Array.Copy(length, 0, command, 10, 2);
}
catch (Exception ex)
{
ModbusException?.BeginInvoke("功能码0x01", ex, null, null);
}
return command;
}
#endregion
#region 读取离散量,功能码0x02
/// <summary>
/// 读取离散量命令行,功能码0x02
/// </summary>
/// <param name="SlaveAddress">从站地址(站点)</param>
/// <param name="StartAddress">开始地址</param>
/// <param name="NumberOfPoints">线圈数量</param>
/// <param name="id">MBAP报文头(事务元标识符)【1-2字节】,默认自动进行增量编号</param>
/// <returns>0x02命令行</returns>
public byte[] ReadInputStatus_0x02(int SlaveAddress, int StartAddress, int NumberOfPoints, int id = -40000)
{
//命令行
byte[] command = new byte[12];
try
{
//数量限制
if (NumberOfPoints > 2000)
{
throw new Exception("读取离散量不能超过2000");
}
//
//ushort mhead = (ushort)IPAddress.HostToNetworkOrder((short)head);
ushort mStartAddress = (ushort)IPAddress.HostToNetworkOrder((short)StartAddress);
ushort mNumberOfPoints = (ushort)IPAddress.HostToNetworkOrder((short)NumberOfPoints);
//MBAP报文头组合
if (id == -40000)
{
Head = head().Reverse().ToArray();
}
else
{
Head = BitConverter.GetBytes((ushort)id).Reverse().ToArray();
}
MBAPTotalLength = BitConverter.GetBytes((ushort)(command.Length - 6)).Reverse().ToArray();
slaveAddress = Convert.ToByte(SlaveAddress);
//离散量起始地址,读取长度
byte[] start = BitConverter.GetBytes(mStartAddress);
byte[] length = BitConverter.GetBytes(mNumberOfPoints);
//命令行加载报文头
Array.Copy(MBAP, 0, command, 0, 7);
//功能码
command[7] = 0x02;//功能码
//命令行加载起始地址
Array.Copy(start, 0, command, 8, 2);
//命令行加载读取数量
Array.Copy(length, 0, command, 10, 2);
}
catch (Exception ex)
{
ModbusException?.BeginInvoke("功能码0x02", ex, null, null);
}
return command;
}
#endregion
#region 读取保持寄存器,功能码0x03
/// <summary>
/// 读取保持寄存器命令行,功能码0x03
/// </summary>
/// <param name="SlaveAddress">从站地址(站点)</param>
/// <param name="StartAddress">开始地址</param>
/// <param name="NumberOfPoints">寄存器数量</param>
/// <param name="id">MBAP报文头(事务元标识符)【1-2字节】,默认自动进行增量编号</param>
/// <returns>0x03命令行</returns>
public byte[] ReadHoldingRegister_0x03(int SlaveAddress, int StartAddress, int NumberOfPoints, int id = -40000)
{
//命令行
byte[] command = new byte[12];
try
{
//数量限制
if (NumberOfPoints > 125)
{
throw new Exception("读取保持寄存器数量不能超过125");
}
//MBAP报文头组合,高低字节换位(与主机字节相反)
if (id == -40000)
{
Head = head().Reverse().ToArray();
}
else
{
Head = BitConverter.GetBytes((ushort)id).Reverse().ToArray();
}
MBAPTotalLength = BitConverter.GetBytes((ushort)(command.Length - 6)).Reverse().ToArray();
slaveAddress = Convert.ToByte(SlaveAddress);
//保持寄存器起始地址,读取长度
byte[] start = BitConverter.GetBytes((ushort)StartAddress).Reverse().ToArray();
byte[] length = BitConverter.GetBytes((ushort)NumberOfPoints).Reverse().ToArray();
//命令行加载报文头
Array.Copy(MBAP, 0, command, 0, 7);
//功能码
command[7] = 0x03;//功能码
//命令行加载起始地址
Array.Copy(start, 0, command, 8, 2);
//命令行加载读取数量
Array.Copy(length, 0, command, 10, 2);
}
catch (Exception ex)
{
ModbusException?.BeginInvoke("功能码0x03", ex, null, null);
}
return command;
}
#endregion
#region 读取输入寄存器,功能码0x04
/// <summary>
/// 读取输入寄存器命令行,功能码0x04
/// </summary>
/// <param name="head">报文序号</param>
/// <param name="SlaveAddress">从站地址(站点)</param>
/// <param name="StartAddress">开始地址</param>
/// <param name="NumberOfPoints">寄存器数量</param>
/// <param name="id">MBAP报文头(事务元标识符)【1-2字节】,默认自动进行增量编号</param>
/// <returns>0x04命令行</returns>
public byte[] ReadInputRegister_0x04(int SlaveAddress, int StartAddress, int NumberOfPoints, int id = -40000)
{
//命令行
byte[] command = new byte[12];
try
{
//数量限制
if (NumberOfPoints > 125)
{
throw new Exception("读取输入寄存器数量不能超过125");
}
//MBAP报文头组合,高低字节换位(与主机字节相反)
if (id == -40000)
{
Head = head().Reverse().ToArray();
}
else
{
Head = BitConverter.GetBytes((ushort)id).Reverse().ToArray();
}
MBAPTotalLength = BitConverter.GetBytes((ushort)(command.Length - 6)).Reverse().ToArray();
slaveAddress = Convert.ToByte(SlaveAddress);
//输入寄存器起始地址,读取长度
byte[] start = BitConverter.GetBytes((ushort)StartAddress).Reverse().ToArray();
byte[] length = BitConverter.GetBytes((ushort)NumberOfPoints).Reverse().ToArray();
//命令行加载报文头
Array.Copy(MBAP, 0, command, 0, 7);
//功能码
command[7] = 0x04;//功能码
//命令行加载起始地址
Array.Copy(start, 0, command, 8, 2);
//命令行加载读取数量
Array.Copy(length, 0, command, 10, 2);
}
catch (Exception ex)
{
ModbusException?.BeginInvoke("功能码0x04", ex, null, null);
}
return command;
}
#endregion
#region 写单个线圈,功能码0x05
/// <summary>
/// 写单个线圈命令行,功能码0x05
/// </summary>
/// <param name="head">报文序号</param>
/// <param name="SlaveAddress">从站地址(站点)</param>
/// <param name="StartAddress">开始地址</param>
/// <param name="Switch">线圈的值</param>
/// <param name="id">MBAP报文头(事务元标识符)【1-2字节】,默认自动进行增量编号</param>
/// <returns>0x05命令行</returns>
public byte[] WriteSingleCoil_0x05(int SlaveAddress, int StartAddress, bool Switch, int id = -40000)
{
//命令行
byte[] command = new byte[12];
try
{
//MBAP报文头组合,高低字节换位(与主机字节相反)
if (id == -40000)
{
Head = head().Reverse().ToArray();
}
else
{
Head = BitConverter.GetBytes((ushort)id).Reverse().ToArray();
}
MBAPTotalLength = BitConverter.GetBytes((ushort)(command.Length - 6)).Reverse().ToArray();
slaveAddress = Convert.ToByte(SlaveAddress);
//单个线圈起始地址
byte[] start = BitConverter.GetBytes((ushort)StartAddress).Reverse().ToArray();
//单个线圈写入的值
byte[] power = new byte[2];
//如果线圈写入开,则为0xFF00
if (Switch)
{
power = new byte[2] { 0xFF, 0x00 };
}
//命令行加载报文头
Array.Copy(MBAP, 0, command, 0, 7);
//功能码
command[7] = 0x05;//功能码
//命令行加载起始地址
Array.Copy(start, 0, command, 8, 2);
//命令行加载读取数量
Array.Copy(power, 0, command, 10, 2);
}
catch (Exception ex)
{
ModbusException?.BeginInvoke("功能码0x05", ex, null, null);
}
return command;
}
#endregion
#region 写单个保持寄存器,功能码0x06
/// <summary>
/// 写单个保持寄存器命令行,功能码0x06
/// </summary>
/// <param name="SlaveAddress">从站地址(站点)</param>
/// <param name="StartAddress">开始地址</param>
/// <param name="Values">单个保持寄存器值(16位,byte[0]低位,btye[1]高位)</param>
/// <param name="id">MBAP报文头(事务元标识符)【1-2字节】,默认自动进行增量编号</param>
/// <returns>0x06命令行</returns>
public byte[] WriteSingleRegister_0x06(int SlaveAddress, int StartAddress, byte[] Values, int id = -40000)
{
//命令行
byte[] command = new byte[12];
try
{
//数量限制
if (Values.Length != 2)
{
throw new Exception("写单个保持寄存器的值必须是16位");
}
//MBAP报文头组合,高低字节换位(与主机字节相反)
if (id == -40000)
{
Head = head().Reverse().ToArray();
}
else
{
Head = BitConverter.GetBytes((ushort)id).Reverse().ToArray();
}
MBAPTotalLength = BitConverter.GetBytes((ushort)(command.Length - 6)).Reverse().ToArray();
slaveAddress = Convert.ToByte(SlaveAddress);
//保持寄存器起始地址,读取长度
byte[] start = BitConverter.GetBytes((ushort)StartAddress).Reverse().ToArray();
byte[] values = Values.Reverse().ToArray();
//命令行加载报文头
Array.Copy(MBAP, 0, command, 0, 7);
//功能码
command[7] = 0x06;//功能码
//命令行加载起始地址
Array.Copy(start, 0, command, 8, 2);
//命令行加载读取数量
Array.Copy(values, 0, command, 10, 2);
}
catch (Exception ex)
{
ModbusException?.BeginInvoke("功能码0x06", ex, null, null);
}
return command;
}
#endregion
#region 写多个线圈,功能码0x0F
/// <summary>
/// 写多个线圈,功能码0x0F,数值是从低到高的bool的List序列
/// </summary>
/// <param name="SlaveAddress">从站地址(站点)</param>
/// <param name="StartAddress">开始地址</param>
/// <param name="data">线圈写入数值(bool表示),从低位到高位的List序列</param>
/// <param name="id">MBAP报文头(事务元标识符)【1-2字节】,默认自动进行增量编号</param>
/// <returns>0x0F命令行</returns>
public byte[] WriteMultipleCoil_0x0F(int SlaveAddress, int StartAddress, List<bool> data, int id = -40000)
{
if (data.Count > 1968)
{
throw new Exception("写多个线圈的数量最多为246个(1968位)");
}
//线圈数值长度(按字节算),报文【第13个字节(索引12)】
byte dataByteCount = Convert.ToByte(Math.Ceiling((double)data.Count / 8));
//命令行
byte[] command = new byte[13 + dataByteCount];
try
{
//MBAP报文头组合,高低字节换位(与主机字节相反)
if (id == -40000)
{
Head = head().Reverse().ToArray();
}
else
{
Head = BitConverter.GetBytes((ushort)id).Reverse().ToArray();
}
MBAPTotalLength = BitConverter.GetBytes((ushort)(command.Length - 6)).Reverse().ToArray();
slaveAddress = Convert.ToByte(SlaveAddress);
//保持寄存器起始地址,读取长度
byte[] start = BitConverter.GetBytes((ushort)StartAddress).Reverse().ToArray();
//命令行加载报文头【1-7字节】
Array.Copy(MBAP, 0, command, 0, 7);
//功能码
command[7] = 0x0F;//功能码【第8字节】
//命令行加载起始地址
Array.Copy(start, 0, command, 8, 2);//【9-10字节】
//线圈数值长度(接位数算),报文【第11,12字节(索引10,11)】
byte[] dataNumCount = BitConverter.GetBytes((ushort)data.Count).Reverse().ToArray();
//命令行加载线圈数值数量(按位数算)
Array.Copy(dataNumCount, 0, command, 10, 2);//【11-12字节】
//命令行加载线圈数值长度(按字节算)【13字节】
command[12] = dataByteCount;
//**线圈数据转换成字节表示
int mod = data.Count % 8;//data数值放在最后一个字节里,总共有多少位
byte[] dataValues = new byte[dataByteCount];//data数据所用的字节
for (int i = 0; i < dataByteCount; i++)//data数值分配到每个字节
{
//最后一个字节分配的data不满8位
if (mod != 0 && i == dataByteCount - 1)
{
for (int j = 0; j < mod; j++)
{
if (data[8 * i + j])
{
dataValues[i] = (byte)(dataValues[i] | (1 << j));
}
}
}
else//data数值正好能分配到每个字节,即data的最后8位正好能满一个字节
{
for (int j = 0; j < 8; j++)
{
if (data[8 * i + j])
{
dataValues[i] = (byte)(dataValues[i] | (1 << j));
}
}
}
}
//将data数值加载到命令行中
Array.Copy(dataValues, 0, command, 13, dataByteCount);//【14-dataByteCount字节】
}
catch (Exception ex)
{
ModbusException?.BeginInvoke("功能码0x0F", ex, null, null);
}
return command;
}
#endregion
#region 写多个保持寄存器,功能码0x10
/// <summary>
/// 写多个保持寄存器命令行,功能码0x10,数值是单个16位寄存器byte[2]的List序列(byte[0]低位,byte[1]高位)
/// </summary>
/// <param name="SlaveAddress">从站地址(站点)</param>
/// <param name="StartAddress">开始地址</param>
/// <param name="data">单个保持寄存器值(16位,byte[0]低位,btye[1]高位)的List序列</param>
/// <param name="id">MBAP报文头(事务元标识符)【1-2字节】,默认自动进行增量编号</param>
/// <returns>0x10命令行</returns>
public byte[] WriteMultipleRegister_0x10(int SlaveAddress, int StartAddress, List<byte[]> data, int id = -40000)
{
if (data.Count > 120)
{
throw new Exception("写多个保持寄存器的数量最多为120个");
}
if (data.Where(u => u.Length != 2).Count() > 0)
{
throw new Exception("写多个保持寄存器的数值中含有不是16位的值");
}
//保持寄存器数值长度,每个数值是2个字节【第13个字节,索引是12】
byte dataByteCount = Convert.ToByte(data.Count * 2);
//命令行
byte[] command = new byte[13 + dataByteCount];
try
{
//MBAP报文头组合,高低字节换位(与主机字节相反)
if (id == -40000)
{
Head = head().Reverse().ToArray();
}
else
{
Head = BitConverter.GetBytes((ushort)id).Reverse().ToArray();
}
MBAPTotalLength = BitConverter.GetBytes((ushort)(command.Length - 6)).Reverse().ToArray();
slaveAddress = Convert.ToByte(SlaveAddress);
//命令行加载报文头【1-7字节】
Array.Copy(MBAP, 0, command, 0, 7);
//功能码
command[7] = 0x10;//功能码【第8字节】
//保持寄存器起始地址
byte[] start = BitConverter.GetBytes((ushort)StartAddress).Reverse().ToArray();
//命令行加载起始地址
Array.Copy(start, 0, command, 8, 2);//【9-10字节】
//保持寄存器数值数量,报文【第11,12字节(索引10,11)】
byte[] dataNumCount = BitConverter.GetBytes((ushort)data.Count).Reverse().ToArray();
//命令行加载保持寄存器数值数量
Array.Copy(dataNumCount, 0, command, 10, 2);//【11-12字节】
//命令行加载保持寄存器数值长度(按字节算)【13字节】
command[12] = dataByteCount;
//data数值加载到命令行
for (int i = 0; i < data.Count; i++)
{
Array.Copy(data[i].Reverse().ToArray(), 0, command, 13 + 2 * i, 2);
}
}
catch (Exception ex)
{
ModbusException?.BeginInvoke("功能码0x10", ex, null, null);
}
return command;
}
#endregion
#region 异常处理程序
public Action<string, Exception> ModbusException { get; set; }
#endregion
}
#endregion //Modbus TCP
5.总结
Modbus协议具体含义非本文重点,可以通过上面所说别的博主文章进行参考学习,也非常感谢上述博主的文章,让我学习到很多,知识是积累而来的,此处代码也算是自己的学习笔记。