C#编写ModbusTcp类库,模拟plc进行本地通信测试
Modbus是一个应用层协议,常用于工业自动化设备之间的通信,主要有两种传输方式:RTU和TCP。
常见的功能码包括读取线圈(01)、读取离散输入(02)、读保持寄存器(03)、读输入寄存器(04)、写单个线圈(05)、写单个寄存器(06)、写多个线圈(15)、写多个寄存器(16)等。类库需要支持这些基本操作。
一、协议基础:
-
Modbus TCP 使用 TCP/IP 协议,默认端口 502。
-
数据帧格式:事务标识符(2字节) + 协议标识符(2字节) + 长度(2字节) + 单元标识符(1字节) + Modbus PDU(功能码 + 数据)。
二、常用功能码:
- 03 功能码:读取保持寄存器(Read Holding Registers)。
- 06 功能码:写单个寄存器(Write Single Register)。
三、工具准备:
- Modbus Slave 模拟器:如 Modbus Slave ,用于模拟从站设备。下载地址: 模拟器下载地址
- 配置从站的 IP(如 127.0.0.1)、端口(502)、寄存器地址和初始值。
四、C# 实现步骤:
- 使用 TcpClient 建立 TCP 连接。
- 构造 Modbus 请求报文并发送。
- 接收响应报文并解析数据。
- 处理异常和超时。
五、代码实现:
- 建立数据连接:
/// <summary>
/// 连接
/// </summary>
/// <returns></returns>
protected override Result Connect()
{
var result = new Result();
socket?.SafeClose();
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
//超时时间设置
socket.ReceiveTimeout = timeout;
socket.SendTimeout = timeout;
//连接
IAsyncResult connectResult = socket.BeginConnect(ipEndPoint, null, null);
//阻塞当前线程
if (!connectResult.AsyncWaitHandle.WaitOne(timeout))
throw new TimeoutException("连接超时");
socket.EndConnect(connectResult);
}
catch (Exception ex)
{
socket?.SafeClose();
result.IsSucceed = false;
result.Err = ex.Message;
result.ErrCode = 408;
result.Exception = ex;
}
return result.EndTime();
}
2.断开连接
public static void SafeClose(this Socket socket)
{
try
{
if (socket?.Connected ?? false) socket?.Shutdown(SocketShutdown.Both);//正常关闭连接
}
catch {
}
try
{
socket?.Close();
}
catch {
}
}
3.读取数据
/// <summary>
/// 读取数据
/// </summary>
/// <param name="address">寄存器起始地址</param>
/// <param name="stationNumber">站号</param>
/// <param name="functionCode">功能码</param>
/// <param name="readLength">读取长度</param>
/// <param name="byteFormatting">大小端转换</param>
/// <returns></returns>
public Result<byte[]> Read(string address, byte stationNumber = 1, byte functionCode = 3, ushort readLength = 1, bool byteFormatting = true)
{
var result = new Result<byte[]>();
if (!socket?.Connected ?? true)
{
var conentResult = Connect();
if (!conentResult.IsSucceed)
{
conentResult.Err = $"读取 地址:{
address} 站号:{
stationNumber} 功能码:{
functionCode} 失败。{
conentResult.Err}";
return result.SetErrInfo(conentResult);
}
}
try
{
var chenkHead = GetCheckHead(functionCode);
//1 获取命令(组装报文)
byte[] command = GetReadCommand(address, stationNumber, functionCode, readLength, chenkHead);
result.Requst = string.Join(" ", command.Select(t => t.ToString("X2")));
//获取响应报文
var sendResult = SendPackageReliable(command);
if (!sendResult.IsSucceed)
{
sendResult.Err = $"读取 地址:{
address} 站号:{
stationNumber} 功能码:{
functionCode} 失败。{
sendResult.Err}";
return result.SetErrInfo(sendResult).EndTime();
}
var dataPackage = sendResult.Value;
byte[] resultBuffer = new byte[dataPackage.Length - 9];
Array.Copy(dataPackage, 9, resultBuffer, 0, resultBuffer.Length);
result.Response = string.Join(" ", dataPackage.Select(t => t.ToString("X2")));
//4 获取响应报文数据(字节数组形式)
if (byteFormatting)
result.Value = resultBuffer.Reverse().ToArray().ByteFormatting(format);
else
result.Value = resultBuffer.Reverse().ToArray();
if (chenkHead[0] != dataPackage[0] || chenkHead[1] != dataPackage[1])
{
result.IsSucceed = false;
result.Err = $"读取 地址:{
address} 站号:{
stationNumber} 功能码:{
functionCode} 失败。响应结果校验失败";
socket?.SafeClose();
}
else if (ModbusHelper.VerifyFunctionCode(functionCode, dataPackage[7]))
{
result.IsSucceed = false;
result.Err = ModbusHelper.ErrMsg(dataPackage[8]);
}
}
catch (SocketException ex)
{
result.IsSucceed = false;
if (ex.SocketErrorCode == SocketError.TimedOut)
{
result.Err = $"读取 地址:{
address} 站号:{
stationNumber} 功能码:{
functionCode} 失败。连接超时";
socket?.SafeClose();
}
else
{
result.Err = $"读取 地址:{
address} 站号:{
stationNumber} 功能码:{
functionCode} 失败。{
ex.Message}";
}
}
finally
{
if (isAutoOpen) Dispose();
}
return result.EndTime();
}
/// <summary>
/// 获取随机校验头
/// </summary>
/// <returns></returns>
private byte[] GetCheckHead(int seed)
{
var random = new Random(DateTime.Now.Millisecond + seed);
return new byte[] {
(byte)random.Next(255), (byte)random.Next(255) };
}
/// <summary>
/// 获取读取命令
/// </summary>
/// <param name="address">寄存器起始地址</param>
/// <param name="stationNumber">站号</param>
/// <param name="functionCode">功能码</param>
/// <param name="length">读取长度</param>
/// <returns></returns>
public byte[] GetReadCommand(string address, byte stationNumber, byte functionCode, ushort length, byte[] check = null)
{
var readAddress = ushort.Parse(address?.Trim());
if (plcAddresses) readAddress = (ushort)(Convert.ToUInt16(address?.Trim().Substring(1)) - 1);
byte[] buffer = new byte[12];
buffer[0] = check?[0] ?? 0x19;
buffer[1] = check?[1] ?? 0xB2;//Client发出的检验信息
buffer[2] = 0x00;
buffer[3] = 0x00;//表示tcp/ip 的协议的Modbus的协议
buffer[4] = 0x00;
buffer[5] = 0x06;//表示的是该字节以后的字节长度
buffer[6] = stationNumber; //站号
buffer[7] = functionCode; //功能码
buffer[8] = BitConverter.GetBytes(readAddress)[1];
buffer[9] = BitConverter.GetBytes(readAddress)[0];//寄存器地址
buffer[10] = BitConverter.GetBytes(length)[1];
buffer[11] = BitConverter.GetBytes(length)[0];//表示request 寄存器的长度(寄存器个数)
return buffer;
}
/// <summary>
/// 发送报文,并获取响应报文(如果网络异常,会自动进行一次重试)
/// TODO 重试机制应改成用户主动设置
/// </summary>
/// <param name="command"></param>
/// <returns></returns>
public Result<byte[]> SendPackageReliable(byte[] command)
{
try
{
var result = SendPackageSingle(command);
if (!result.IsSucceed)
{
WarningLog?.Invoke(result.Err, result.Exception);
//如果出现异常,则进行一次重试
var conentResult = Connect();
if (!conentResult.IsSucceed)
return new Result<byte[]>(conentResult);
return SendPackageSingle(command);
}
else
return result;
}
catch (Exception ex)
{
try
{
WarningLog?.Invoke(ex.Message, ex);
//如果出现异常,则进行一次重试
var conentResult = Connect();
if (!conentResult.IsSucceed)
return new Result<byte[]>(conentResult);
return SendPackageSingle(command);
}
catch (Exception ex2)
{
Result<byte[]> result = new Result<byte[]>();
result.IsSucceed = false;
result.Err = ex2.Message;
result.AddErr2List();
return result.EndTime();
}
}
}
4.其他类型数据读取
/// <summary>
/// 读取Int16类型数据
/// </summary>
/// <param name="address">寄存器起始地址</param>
/// <param name="stationNumber">站号</param>
/// <param name="functionCode">功能码</param>
/// <returns></returns>
public Result<short> ReadInt16(string address, byte stationNumber = 1, byte functionCode = 3)
{
var readResut = Read(address, stationNumber, functionCode);
var result = new Result<short>(readResut);
if (result.IsSucceed)
result.Value = BitConverter.ToInt16(readResut.Value, 0);
return result.EndTime();
}
/// <summary>
/// 按位的方式