ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

HslCommunication v7.0.1 实战:构建稳定高效的 PLC 测试工具

HslCommunication v7.0.1 实战:构建稳定高效的 PLC 测试工具

简介:Hslcommunication v7.0.1 是一款面向工业自动化工程师与 PLC 学习者的通讯测试工具,主要用于 PLC 设备的通信调试、数据监控以及程序上传下载。它支持 MODBUS、CAN、Ethernet/IP、Profinet 等多种主流协议,覆盖大部分工业通讯需求,可应对系统调试、故障修复、性能优化与培训教学等场景,对初学者和资深工程师都较为友好。资源包共 6 个文件,以 2 个 dll 动态库、2 个 exe 可执行程序及 2 个 xml 配置文件为主,dll 承担通讯核心功能,exe 提供演示与自动更新入口,xml 用于配置与说明,压缩包约 730KB,体积轻巧便于携带。目前已有 4885 人学习下载,热度较高。借助该工具,读者可完成实时数据监测、远程调试与故障诊断,快速定位 PLC 变量变化与报警信息,并通过程序上传下载实现备份与修改,从而缩短停机时间、提升调试效率,是工业 4.0 时代自动化工程师的实用助手。

1. 从一次产线联调翻车说起:HslCommunication v7.0.1 到底解决什么问题

去年帮一个做非标自动化的朋友排查故障,现场三菱 FX5U 和西门子 S7-1200 混用,上位机用 C# 写的采集程序,读 D 寄存器偶尔返回 0,重启就好,过两小时又犯。他一开始怀疑网线,换了三根;又怀疑交换机,换了两台;最后发现是每次读写都新建连接、没做复用,PLC 连接数被占满后新请求排队超时,超时后代码里 catch 住直接返回默认值 0。这个坑用 HslCommunication 的长连接模式加一个连接状态检测就能绕过去。HslCommunication 是一套 .NET 平台下的工业通信组件库,v7.0.1 是它较新的一个版本分支,封装了西门子、三菱、欧姆龙、Modbus、AB 等主流 PLC 的协议,让上位机不用自己啃 S7 协议的 TPKT/COTP 分层,也不用为每个品牌写一套 socket 收发。拿它做 PLC 测试工具,核心价值就三点:协议统一、连接可管、异常可查。适合谁?做设备数据采集的 .NET 开发者、做产线调试的自动化工程师、以及需要快速验证 PLC 点位通不通的现场人员。下面按“先跑通、再调参、再避坑”的顺序讲。

2. 用 HslCommunication v7.0.1 搭一个最小可用的 PLC 测试工具

2.1 选型理由:为什么不是自己写 Socket,也不是用现成组态软件

自己写 Socket 不是不行,但你要面对的是各品牌协议差异。西门子 S7 通信要处理 ISO-on-TCP 的 TPKT 头、COTP 连接请求、S7 通信的 PDU 协商;三菱 MC 协议分 1E 帧和 3E 帧,二进制和 ASCII 还不一样;欧姆龙 FINS 又有自己的命令码。这些协议细节每个品牌都能写一本书,而实际项目里你只需要“读 D100 这个值”。HslCommunication 把这些差异收进统一的Read/Write接口,你面对的是plc.ReadInt16("D100")这种调用。组态软件也能做测试,但组态软件通常绑定画面和授权,做一个纯后台的批量点位测试工具反而笨重,而且不好集成到自己的 CI 或诊断脚本里。用 HslCommunication 写控制台工具,几十行代码就能批量轮询几百个点位,输出 CSV,这是组态软件不容易做到的。

2.2 环境准备与依赖引入

v7.0.1 支持 .NET Framework 4.6.1 及以上、.NET Standard 2.0、.NET 5/6/7/8。新建一个 .NET 6 控制台项目,通过 NuGet 引入。注意包名和命名空间,v7 之后部分类做了调整,老教程里的HslCommunication.Profinet.Siemens.SiemensS7Net仍然可用,但建议用新命名空间下的类。

# 新建控制台项目,目标框架 .NET 6 dotnet new console -n PlcProbe cd PlcProbe # 引入 HslCommunication 包,版本按 v7.0.1 系列选择 dotnet add package HslCommunication --version 7.0.1

逻辑说明:用dotnet add package而不是手动拷 DLL,是为了让依赖可复现。参数说明:--version 7.0.1锁定版本,避免自动升级到不兼容的大版本;如果 NuGet 源里没有精确的 7.0.1,选 7.0.x 系列最接近的即可,不要跨到 8.x,因为 v8 部分 API 有破坏性变更。

2.3 连接西门子 S7-1200 并读取一个 DB 块

下面是最小可运行代码,连接、读一个 Int16、读一个 Float、断开。

using HslCommunication; using HslCommunication.Profinet.Siemens; // 创建西门子 S7 对象,指定 PLC 型号 var plc = new SiemensS7Net(SiemensPLCS.S1200, "192.168.0.10") { Port = 102, // S7 默认端口 102 ConnectTimeOut = 3000, // 连接超时 3 秒 ReceiveTimeOut = 2000 // 接收超时 2 秒 }; // 建立长连接 OperateResult connectResult = plc.ConnectServer(); if (!connectResult.IsSuccess) { Console.WriteLine($"连接失败:{connectResult.Message}"); return; } // 读取 DB1.DBW0,即 DB1 中偏移 0 的 Int16 OperateResult<short> readInt = plc.ReadInt16("DB1.0"); if (readInt.IsSuccess) Console.WriteLine($"DB1.0 = {readInt.Content}"); else Console.WriteLine($"读取失败:{readInt.Message}"); // 读取 DB1.DBD2,即偏移 2 的 Float OperateResult<float> readFloat = plc.ReadFloat("DB1.2"); if (readFloat.IsSuccess) Console.WriteLine($"DB1.2 = {readFloat.Content}"); // 用完关闭,释放连接 plc.ConnectClose();

逻辑说明:SiemensS7Net构造函数第一个参数是 PLC 型号枚举,第二个是 IP。ConnectServer()建立长连接,后续读写复用这个连接,避免每次新建。ReadInt16("DB1.0")里的地址字符串格式是DB块号.字节偏移,读 Float 时偏移要按 2 递增,因为 Float 占 4 字节,但地址写的是起始字节偏移。参数说明:ConnectTimeOut和ReceiveTimeOut在局域网内可以设 2000~3000 毫秒,跨网段或无线网桥建议放大到 5000 毫秒,否则网络抖动会误报超时。Port一般不用改,S7 固定 102。

2.4 连接三菱 FX5U 并批量读取 D 寄存器

三菱的地址格式和西门子不同,D 寄存器直接写D100,批量读用ReadInt16的数组重载。

using HslCommunication.Profinet.Melsec; // 三菱 MC 协议,FX5U 用二进制 3E 帧 var melsec = new MelsecMcNet("192.168.0.20", 6000) { NetworkNumber = 0x00, // 网络号 NetworkStationNumber = 0x00, // 站号 ConnectTimeOut = 3000 }; OperateResult connect = melsec.ConnectServer(); if (!connect.IsSuccess) { Console.WriteLine($"三菱连接失败:{connect.Message}"); return; } // 从 D100 开始连续读 10 个 Int16 OperateResult<short[]> batch = melsec.ReadInt16("D100", 10); if (batch.IsSuccess) { for (int i = 0; i < batch.Content.Length; i++) Console.WriteLine($"D{100 + i} = {batch.Content[i]}"); } else { Console.WriteLine($"批量读取失败:{batch.Message}"); } melsec.ConnectClose();

逻辑说明:MelsecMcNet默认走二进制 3E 帧,端口 6000 是三菱 MC 协议常用端口,实际以 PLC 设置为准。ReadInt16("D100", 10)一次读 10 个寄存器,比循环单点读快得多,因为一次网络往返就够。参数说明:NetworkNumber和NetworkStationNumber在多级网络或经网关时才需要改,直连一般保持 0。批量读的长度不要超过 PLC 单次通信允许的最大字数,三菱一般单次最多 960 个字,但实际建议控制在 100 以内,方便定位问题。

2.5 把测试结果落成 CSV 方便比对

现场调试经常需要“读一遍存下来,改完 PLC 再读一遍对比”。加一个简单的 CSV 输出。

using System.IO; using System.Text; // 假设已有一个点位列表 var points = new[] { "D100", "D101", "D102", "D103" }; var sb = new StringBuilder(); sb.AppendLine("Address,Value,Timestamp"); foreach (var addr in points) { OperateResult<short> r = melsec.ReadInt16(addr); string val = r.IsSuccess ? r.Content.ToString() : $"ERR:{r.Message}"; sb.AppendLine($"{addr},{val},{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}"); } File.WriteAllText("plc_probe.csv", sb.ToString(), Encoding.UTF8); Console.WriteLine("已写入 plc_probe.csv");

逻辑说明:逐点读虽然慢,但调试阶段能清楚看到哪个点位失败。参数说明:时间戳精确到毫秒,方便和 PLC 侧的时间戳对齐;编码用 UTF-8,避免中文错误信息乱码。如果点位多,改成批量读再写 CSV,速度会快一个数量级。

3. 参数怎么设:超时、重试、PDU 长度与连接模式

3.1 超时参数不是越大越好

很多人被超时坑过:设 500 毫秒,网络稍微抖一下就报错;设 10000 毫秒,真断线时界面卡死十秒。我的经验是分两层设。连接超时ConnectTimeOut设 3000 毫秒,因为 TCP 握手在局域网内通常几十毫秒,3 秒足够覆盖交换机收敛。接收超时ReceiveTimeOut设 2000 毫秒,因为 PLC 扫描周期一般 1~10 毫秒,响应很快,2 秒没回基本就是有问题。如果是无线网桥或 4G 路由,两个都放大到 5000 毫秒,并且要配合重试。

// 带重试的读取封装 OperateResult<short> ReadWithRetry(SiemensS7Net plc, string addr, int maxRetry = 3) { for (int i = 0; i < maxRetry; i++) { var r = plc.ReadInt16(addr); if (r.IsSuccess) return r; // 如果是连接断开,尝试重连 if (r.Message.Contains("连接") || r.Message.Contains("Socket")) { plc.ConnectClose(); System.Threading.Thread.Sleep(200); plc.ConnectServer(); } } return plc.ReadInt16(addr); // 最后一次结果直接返回 }

逻辑说明:重试不是无脑循环,而是区分错误类型。连接类错误才重连,数据类错误(比如地址越界)重试多少次都没用。参数说明:maxRetry设 3 次,间隔 200 毫秒,总耗时可控;如果业务对实时性要求高,重试次数要减少,改为上报错误让上层决策。

3.2 PDU 长度影响批量读的上限

西门子 S7 通信的 PDU(协议数据单元)长度在连接建立时协商,S7-1200 一般 240 字节,S7-1500 可以到 960 字节。HslCommunication 内部会自动按 PDU 拆分批量请求,但如果你一次读 500 个寄存器,库会拆成多次请求,总耗时反而比手动分批更长,因为每次请求都有往返。我的做法是手动分批,每批不超过 100 个寄存器,批间不加延时,让库自己调度。

// 手动分批读,每批 100 个 var allValues = new List<short>(); for (int start = 0; start < 500; start += 100) { var r = plc.ReadInt16($"DB1.{start * 2}", 100); if (r.IsSuccess) allValues.AddRange(r.Content); else { Console.WriteLine($"批 {start} 失败:{r.Message}"); break; } }

逻辑说明:地址偏移按字节算,Int16 占 2 字节,所以start * 2。参数说明:每批 100 是经验值,PDU 240 字节时约能放 120 个 Int16,留点余量避免库内部再拆。如果读 Float,每批减半到 50 个。

3.3 长连接和短连接的选择

短连接是每次读写都ConnectServer然后ConnectClose,优点是简单、不怕连接僵死,缺点是每次都有 TCP 握手开销,高频采集时 PLC 连接数容易被占满。长连接是启动时连一次,之后一直复用,优点是快,缺点是需要处理断线重连。做测试工具建议用长连接加心跳:每隔 30 秒读一个固定寄存器,读失败就重连。

// 心跳线程示例 var heartbeat = new System.Threading.Thread(() => { while (true) { System.Threading.Thread.Sleep(30000); var r = plc.ReadInt16("DB1.0"); if (!r.IsSuccess) { plc.ConnectClose(); System.Threading.Thread.Sleep(500); plc.ConnectServer(); } } }); heartbeat.IsBackground = true; heartbeat.Start();

逻辑说明:心跳读一个已知存在的地址,成功说明连接活着。参数说明:间隔 30 秒是折中,太短增加 PLC 负担,太长断线发现慢。IsBackground = true让线程随主程序退出,避免程序卡住不退。

4. 避坑记录:现场调试最容易翻车的五个点

4.1 读回来全是 0,但连接显示成功

现象:ConnectServer返回成功,ReadInt16也返回成功,但值一直是 0。原因:地址写错了,比如西门子 DB 块没勾选“允许 PUT/GET 通信”,或者 DB 块编号不对,库读到了未初始化的区域。解决:先在 PLC 侧确认 DB 块属性和偏移,用DB1.0这种明确地址,不要用DB1.DBW0混写。另外 S7-1200 要在硬件配置里勾选“允许来自远程对象的 PUT/GET 通信访问”,否则连接能建立但读写被拒。

4.2 批量读报“数组越界”或“长度错误”

现象:ReadInt16("D100", 200)报错。原因:三菱 MC 协议单次读字数有限制,或者地址范围超出了 PLC 实际定义的软元件范围。解决:把批量拆小,先确认 PLC 里 D 寄存器的最大编号。另外注意地址字符串不要带空格,"D100"和"D 100"在部分版本里解析结果不同。

4.3 程序跑几小时后卡死

现象:界面无响应,日志停在某次读操作。原因:长连接没有设接收超时,或者设得太大,PLC 侧因为网络风暴或扫描周期过长没响应,上位机 socket 一直等。解决:ReceiveTimeOut必须设,且不要超过 5000 毫秒;同时加心跳线程,心跳失败就强制重连。如果用了多线程读写同一个 plc 对象,要加锁,HslCommunication 的对象不是线程安全的。

4.4 三菱 FX5U 连不上,但 FX3U 可以

现象:同一段代码连 FX3U 正常,换 FX5U 就超时。原因:FX5U 默认走 SLMP 协议,端口和帧格式与 FX3U 的 MC 协议有差异,FX3U 常用 1E 帧,FX5U 用 3E 帧。解决:确认 PLC 以太网参数里的“通信协议”设置,HslCommunication 里用MelsecMcNet对应 3E 帧,如果 PLC 设的是 1E 帧,要换MelsecMcAsciiNet或调整帧类型。端口也要对,FX5U 默认 5007 或 6000,以实际设置为准。

4.5 写入成功但 PLC 没反应

现象:Write返回成功,但 PLC 输出没变化。原因:写的是输入寄存器或系统保留区,或者写入了但被 PLC 程序下一周期覆盖。解决:确认目标地址是可写软元件,比如三菱的 D 可以写,X 是输入继电器不能写;西门子的 DB 块要确认不是只读优化块。另外写入后加 100 毫秒再读回验证,不要只信写操作的返回值。

5. 进阶技巧:用 HslCommunication 做点位批量验证与差异比对

做 PLC 测试工具,最终要解决的是“改完程序后,几百个点位到底对不对”。我一般会写一个两遍采集加差异比对的流程:第一遍在 PLC 程序修改前采集基准值,存 CSV;第二遍修改后采集,逐点比对,只输出不一致的点。这样现场调试不用盯着几百行数据看。

using System.Collections.Generic; using System.Linq; // 采集函数,返回 地址->值 的字典 Dictionary<string, string> Capture(MelsecMcNet plc, IEnumerable<string> addresses) { var dict = new Dictionary<string, string>(); foreach (var addr in addresses) { var r = plc.ReadInt16(addr); dict[addr] = r.IsSuccess ? r.Content.ToString() : $"ERR:{r.Message}"; } return dict; } // 比对函数 void Diff(Dictionary<string, string> baseline, Dictionary<string, string> current) { foreach (var kv in baseline) { if (!current.ContainsKey(kv.Key)) { Console.WriteLine($"{kv.Key} 缺失"); continue; } if (current[kv.Key] != kv.Value) Console.WriteLine($"{kv.Key} 变化:{kv.Value} -> {current[kv.Key]}"); } }

逻辑说明:Capture逐点读,适合调试阶段;如果点位上千,改成批量读再拆字典。Diff只输出变化点,减少干扰。参数说明:地址列表可以从 CSV 或文本文件读入,方便现场按设备分段测试。基准值文件建议带时间戳命名,避免覆盖。

还有一个技巧是模拟量换算。PLC 里的 Int16 原始值要转成工程值,比如 4~20mA 对应 0~100 摄氏度。这个换算不要在采集循环里做,而是采集完统一做,因为换算公式可能随量程调整,分开做方便改。

// 原始值转工程值:4~20mA 对应 0~100 double RawToEng(short raw, double rawMin = 0, double rawMax = 27648, double engMin = 0, double engMax = 100) { return (raw - rawMin) * (engMax - engMin) / (rawMax - rawMin) + engMin; }

逻辑说明:西门子模拟量模块常用 0~27648 对应 0~100% 量程,三菱常用 0~4000。参数说明:rawMin/rawMax按模块手册填,不要硬编码在采集循环里。如果读回来是负数,检查是不是 Int16 溢出,模拟量原始值可能超过 32767,要用ReadUInt16。

最后说一个我自己的习惯:每次现场调试前,先用 HslCommunication 写一个最小连接测试,只读一个已知点位,确认通了再上批量。这个习惯帮我省过很多次“以为是代码问题,其实是网段不通”的时间。工具本身不复杂,复杂的是现场环境,把连接、超时、重试、比对这四件事做扎实,PLC 测试工具就稳了。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进