当前位置:网站首页>【.NET6+Modbus】Modbus TCP协议解析、仿真环境以及基于.NET实现基础通信
【.NET6+Modbus】Modbus TCP协议解析、仿真环境以及基于.NET实现基础通信
2022-08-09 16:09:00 【dotNET跨平台】
接下来的内容,我会以从头开发一个简单的基于modbus tcp通信的案例,来实现一个基础的通信功能。
有关环境:
开发环境:VS 2022企业版
运行环境:Win 10 专业版
.NET 环境版本:.NET 6
【备注】 源码在文末
1、新建一个基于.NET 6带控制器的webapi项目,以及一个类库项目。如下图所示,新建以后的项目目录结构。
2、由于modbus tcp通信实际上就是一个socket通信,所以在类库项目下,先创建了一个Modbus服务类,并且提供一个基于socket通信连接的方法。socket连接以后,需要返回socket实例拿来使用。
3、为了方便一点,再新增一个通用的返回信息类,用于存储一些返回信息使用。
4、基于以上的返回信息类,咱对连接方法进行稍微改造一下,让它看起来更方便一点。这样可以用来验证连接是否正常,以及返回对应的异常信息,好做进一步处理。
5、Modbus TCP请求的报文规则,一些解析信息如下:
站地址:默认0x01, 除非PLC告诉我们其他站地址。
功能码:代表读写数据时候指定的读写方法等。例如读取线圈的功能码是0x01。
地址和读取长度:地址目前个人在施耐德物理的PLC环境上,不能超过30000。同时,单次读写长度不能超过248个byte,否则PLC可能会飘。当然,也可能将来一些PLC可以支持更长的批量数据读写,目前在施耐德PLC环境下不支持(具体型号忘记了,有点久了,当前身边没得PLC了,等下会使用仿真工具来做环境)。
头部校验(消息唯一识别码):0~65535,用于PLC服务端进行区分不同的客户端而使用的一组数据标识,不同的客户端必须保证标识码不重合。例如多个客户端同时存在时候,发起的通信请求,必须保持不一样的识别码,否则Modbus服务端有可能会因为不知是哪个客户端发起的请求而导致信息乱了。
无(协议标识):默认0,代表是Modbus协议。
数据长度:发送的报文的长度,刚好是6位,所以可以写成固定值0x06。(写入的规则不一样,此处固定值只当作读取时候使用)
6、根据协议的一些具体内容,写一个存储功能码和异常返回码的数据类,用于后期做通信时候传参和通信数据验证使用。有关协议具体内容,如下代码所示。
7、由于异常码是byte数据,直接验证可能会麻烦一点,为了可以直观一些,此处再新增一个用于解析Modbus返回的异常信息的方法,用于备用。
8、根据协议规则,提供一些参数,并先搭建一个简单的方法框架,用来可以进行读取线圈的功能。包含简单的报文数据拆分以及报文发送和接收。由于发送报文长度不能超过248byte(1 bool大小 == 1 byte,如果是其他类型,需要做其他长度换算),所以当长度超过时候,做个简单的算法进行拆分再发送,防止发生不必要的异常。以下做一个读取线圈(Bool类型数据)的简单方法。
9、根据上方提供的协议报文组装规则,进行开发一个通用的报文组织方法。有高低位之分,所以对于占用2byte的数据,需要进行"倒装"。
10、发送报文以后,返回的报文含有校验信息:发送的数据报文的第7位的数据,加上 0x80 以后,跟返回的报文的第7位byte数据如果一致,则代表当前通信上可能有异常。异常码在接收的响应报文的第8位。
所以可以继续写一个验证是否成功的校验方法:
11、由于返回的数据也都是byte数据,以上读取的线圈值(布尔值),就需要提供一个数据类型转换的功能用于把byte数组转换为bool数组。
12、对读取线圈的最开始的方法,进行一些完善以后的代码如下。响应报文长度是 发送数据长度*2+9 。
13、接下来做一个简单的测试。准备一下仿真环境,进行本地的测试,看看是否可以连通。先准备两个工具,一个是 modbus poll,另一个是modbus slave。一个用来模拟服务端环境,另一个可用来模拟数据收发验证。
14、两边都设置为读写单个线圈的功能,用于测试以上线圈读取的代码的功能。
15、两边都设置为modbus tcp连接方式。Slave站点启动以后,默认为本地,poll工具上的IP地址选择本地即可。如果是真实PLC环境,则填写真实PLC地址。
16、测试两边是否通信上。给任意一个地址写入一个true,可以看到另一边也同步更新,说明通信是通的了。
【注意】modbus工具,poll和slave工具默认占用了消息唯一标识码,大概是1~5左右的固定值,所以使用该工具期间,建议程序上的唯一消息识别码设置为5以上,以防止通信干扰。
17、接下来就可以继续完善代码进行验证了。先新增ModbusService的接口IModbusService,用于实现依赖注入。然后在program.cs文件里面进行服务注册。
18、新建一个控制器,用来进行模拟实验。有关代码和注释如图所示。
19、进行读取一个长度试试效果。结果是数据不支持,说明报文有问题。
20、通过断点,找到问题所在,上面的代码里面,length经过简单算法计算以后等于0,此处需要用的应该是newLength变量的值。
21、再次测试,地址从1开始,读取两个地址,结果符合预期。
22、再测试一下,从0开始读取30个,并随即设置若干个是True的值。
23、其他的写入、以及其他类型读写,基本类似。由于篇幅有限,就不继续进行一步一步操作的截图了。读取的,选好类型,报文格式都是一样的,唯一有差别的是写入的报文。下面是写入单个线圈值的报文。线圈当前仅支持一个一个写入。
24、写入寄存器的规则会有些偏差,协议规则如下图。
【备注】以上图的标题,我写错了,应该是 “写入寄存器”报文协议,懒得换图了,大佬们看的时候自己辨别哈~
读取线圈当作引导,其他类型也都异曲同工,大佬们可以自行尝试。
另外说点,如果是生产环境下使用,建议把客户端连接做成【长连接】,不然重复创建连接比较耗费资源,耗时也会因为新建连接而占用一大半。同时,如果是多线程访问,使用同一个客户端连接,必须加锁,否则会干扰数据;如果是多线程,不同客户端,就要保证每个消息识别码必须不同,如果存在同一个识别码,很容易发生数据异常等情况。
有关源码:
ModbusService源码:
public class ModbusService: IModbusService
{ public ResultInformation<Socket> ConnectModbusTcpService( IPAddress ip, int port)
{
ResultInformation<Socket> client = new(); try
{
client.Result = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
client.Result.Connect(new IPEndPoint(ip, port));
client.IsSucceed = true;
} catch (Exception ex)
{
client.IsSucceed=false;
client.Message = ex.Message;
} return client;
} /// <summary>
/// 读取线圈值(Bool) /// </summary>
/// <param name="client">客户端</param>
/// <param name="headCode">头部标识</param>
/// <param name="station">站地址</param>
/// <param name="address">地址</param>
/// <param name="length">长度</param>
/// <returns></returns>
public ResultInformation<bool[]> ReadCoils(Socket client,ushort headCode,byte station, ushort address, ushort length)
{
ResultInformation<bool[]> result = new(); int resultIndex = 0; ushort newLength = 0; ushort realLength = length; // 存储实际长度
try
{
List<byte> byteResult = new List<byte>(); // 存储实际读取到的所有有效的byte数据
while (length > 0)
{ if (length > 248) // 长度限制,不能超过248 {
length = (ushort)(length - 248);
newLength = 248;
} else
{
newLength = length;
length = 0;
}
resultIndex += newLength; byte[] sendBuffers = BindByteData(headCode,station,FunctionCode.ReadCoil,address,newLength); // 组装报文 client.Send(sendBuffers); byte[] receiveBuffers = new byte[newLength * 2 + 9];
int count = client.Receive(receiveBuffers); // 等待接收报文
var checkResult = CheckReceiveBuffer(sendBuffers, receiveBuffers); // 验证消息发送成功与否
if (checkResult.IsSucceed)
{ // 成功,如果长度超出单次读取长度,进行继续读取,然后对数据进行拼接
List<byte> byteList = new List<byte>(receiveBuffers);
byteList.RemoveRange(0, 9); // 去除前面9个非数据位
byteResult.AddRange(byteList); // 读取到的数据进行添加进集合
address += newLength; // 下一个起始地址 } else
{ throw new Exception(checkResult.Message);
}
}
result.IsSucceed = true;
result.Result = ByteToBoolean(byteResult.ToArray(), realLength);
} catch (Exception ex)
{
result.IsSucceed = false;
result.Result = new bool[0];
result.Message = ex.Message;
} return result;
} private bool[] ByteToBoolean(byte[] data,int length)
{ if (data == null)
{ return new bool[0];
} if (length > data.Length * 8) length = data.Length * 8; bool[] result = new bool[length]; for (int i = 0; i < length; i++)
{ int index = i / 8; int offect = i % 8; byte temp = 0; switch (offect)
{ case 0: temp = 0x01; break; case 1: temp = 0x02; break; case 2: temp = 0x04; break; case 3: temp = 0x08; break; case 4: temp = 0x10; break; case 5: temp = 0x20; break; case 6: temp = 0x40; break; case 7: temp = 0x80; break; default: break;
} if ((data[index] & temp) == temp)
{
result[i] = true;
}
} return result;
} private byte[] BindByteData(ushort headCode,byte station,byte functionCode,ushort address, ushort length)
{ byte[] head = new byte[6];
head[0] = station; // 站地址
head[1] = functionCode; // 功能码
head[2] = BitConverter.GetBytes(address)[1]; // 起始地址
head[3] = BitConverter.GetBytes(address)[0];
head[4] = BitConverter.GetBytes(length)[1]; // 长度
head[5] = BitConverter.GetBytes(length)[0]; return GetSocketBytes(headCode,head);
} private byte[] GetSocketBytes(ushort headCode,byte[] head)
{ byte[] buffers = new byte[head.Length+6];
buffers[0] = BitConverter.GetBytes(headCode)[1];
buffers[1] = BitConverter.GetBytes(headCode)[0]; // 2 和 3位置默认,所以不需要赋值
buffers[4] = BitConverter.GetBytes(head.Length)[1];
buffers[5] = BitConverter.GetBytes(head.Length)[0];
head.CopyTo(buffers, 6); return buffers;
} private ResultInformation<string> CheckReceiveBuffer(byte[] send,byte[] receive)
{
ResultInformation<string> result = new(); if ((send[7] + 0x80) == receive[7])
{ var str = FunctionCode.GetDescriptionByErrorCode(receive[8]);
result.IsSucceed = false;
result.Message = str;
} else
{
result.IsSucceed = true;
} return result;
}
}
控制器源码:
[Route("api/[controller]/[action]")]
[ApiController] public class TestModbusController : ControllerBase
{
IModbusService _service; public TestModbusController(IModbusService modbusService)
{
_service = modbusService;
}
[HttpPost] public IActionResult ReadCoil(ushort address, ushort length)
{ var ip = IPAddress.Parse("127.0.0.1"); // ip地址
int port = 502; // modbus tcp通信,默认端口
byte station = (byte)((short)1); // 站地址为1
var connectResult = _service.ConnectModbusTcpService(ip,port); if (connectResult.IsSucceed)
{ // socket连接创建成功
var readResult = _service.ReadCoils(connectResult.Result,6,station,address,length); // 唯一消息码设为6(大于5,且不重复即可)
if (readResult.IsSucceed)
{ if (readResult.Result.Any())
{
StringBuilder sb = new StringBuilder(); for(int i = 0; i < readResult.Result.Length; i++)
{
sb.AppendLine($"[{i}]:{readResult.Result[i]}");
} return Ok(sb.ToString());
}
} else
{ return Ok(readResult.Message);
}
} else
{ return Ok(connectResult.Message);
} return Ok();
}
}
功能码和异常码:
public class FunctionCode
{ #region 功能码 public const byte ReadCoil = 0x01; // 读取线圈状态 寄存器PLC地址 00001 - 09999
public const byte ReadInputDiscrete = 0x02; // 读取 可输入的离散量 寄存器PLC地址 10001 - 19999
public const byte ReadRegister = 0x03; // 读取 保持寄存器 40001 - 49999
public const byte ReadInputRegister = 0x04; // 读取 可输入寄存器 30001 - 39999
public const byte WriteSingleCoil = 0x05; // 写单个 线圈 00001 - 09999
public const byte WriteSingleRegister = 0x06; // 写单个 保持寄存器 40001 - 49999
public const byte WriteMultiCoil = 0x0F; // 写多个 线圈 00001 - 09999
public const byte WriteMultiRegister = 0x10; // 写多个 保持寄存器 40001 - 49999
public const byte SelectSlave = 0x11; // 查询从站状态信息 (串口通信使用)
#endregion
#region 异常码 public const byte FunctionCodeNotSupport = 0x01;// 非法功能码
public const byte DataAddressNotSupport = 0x02;// 非法数据地址
public const byte DataValueNotSupport = 0x03;// 非法数据值
public const byte DeviceNotWork = 0x04;// 从站设备异常
public const byte LongTimeResponse = 0x05;// 请求需要更长时间才能进行处理请求
public const byte DeviceBusy = 0x06;// 设备繁忙
public const byte OddEvenError = 0x08;// 奇偶性错误
public const byte GatewayNotSupport = 0x0A;// 网关错误
public const byte GatewayDeviceResponseTimeout = 0x0B;// 网关设备响应失败
#endregion
public static string GetDescriptionByErrorCode(byte code)
{ switch (code)
{ case FunctionCodeNotSupport: return "FunctionCodeNotSupport"; case DataAddressNotSupport: return "DataAddressNotSupport"; case DataValueNotSupport: return "DataValueNotSupport"; case DeviceNotWork: return "DeviceNotWork"; case LongTimeResponse: return "LongTimeResponse"; case DeviceBusy: return "DeviceBusy"; case OddEvenError: return "OddEvenError"; case GatewayNotSupport: return "GatewayNotSupport"; case GatewayDeviceResponseTimeout: return "GatewayDeviceResponseTimeout"; default: return "UnknownError";
}
}
}
好了,以上就是该文章的全部内容。如果觉得有帮助,欢迎转发、在看和点赞。也欢迎关注我的公众号:Dotnet Dancer
谢谢大家~
边栏推荐
- Collection of DP Optimization Methods
- ceph部署
- Sigrity PowerSI 特征阻抗和耦合度仿真
- MySQL的索引你了解吗
- Problems Existing in Hardware Development of Electronic Products
- Jenkins使用pipeline部署服务到远程服务器
- Leetcode 算法面试冲刺 热题 HOT 100 刷题(406 416 437 438 448)(六十九)
- 2.1, pay attention to the network based on parallel context scenario text image super-resolution
- [1413. Stepwise summation to get the minimum value of positive numbers]
- OpenCV 图像变换之 —— 直方图均衡化
猜你喜欢
融云 x N 世界:构建无限用户实时交互的「元宇宙会场」
CryoEM粒子(Particle)类型预测的数据集构建
QuickSort(快速排序)&&MergeSort(归并排序)的效率比较[搭配LeetCode例题]
What is hardware integrated development?What are the cores of hardware integrated development?
基于CAP组件实现补偿事务与幂等性保障
智能家居控制系统的功能和特点
云服务的分类和应用
日志定期压缩、清除
kafka 通过 jdbc 从oracle抓取数据
1.1、VIFB: A Visible and Infrared Image Fusion Benchmark(一个可见光与红外图像融合Benchmark)文章阅读
随机推荐
电子产品硬件开发中存在的问题
如何让button中的内容分两行显示
聊聊基于docker部署的mysql如何进行数据恢复
2019强网杯高明的黑客
The article details of the qiucode.cn website realize the code block can be copied by clicking the button
shopee引流方式有哪些,商家如何为自己店铺做引流?
CocosCreator accesses WeChat mini-games
QT工程编译过程学习
价值10亿美元 美国向乌克兰提供单次最大规模安全援助
110+ public professional datasets summarized
What is control board custom development?
云服务的分类和应用
原油等特殊期货开户要求和豁免
Installation and use of Lombok plugin in IDEA
Jenkins使用pipeline部署服务到远程服务器
零基础爬虫regex练习「音乐抓取」
B43 - 基于STM32单片机的自动视力检测仪
【开源教程4】疯壳·开源编队无人机-OPENMV 脚本烧写
央企施工企业数字化转型的灵魂是什么
Smart Tool Management System