ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# .NET 连接西门子S7 PLC通信指南:从S7协议到S7.Net/Sharp7实战

C# .NET 连接西门子S7 PLC通信指南:从S7协议到S7.Net/Sharp7实战 简介面向C#开发者与工控技术人员工控老马出品的实例源码聚焦于如何通过.NET方式与西门子S7系列PLC进行通信。程序采用WinForm界面完整演示了从S7.NET连接、读写寄存器到界面刷新的过程覆盖工业上位机开发中最常用的通信场景既适合零基础入门也为有经验的程序员提供了可复用的代码模板。资源包为zip格式约140KB共32个文件其中包含9个C#源文件作为核心逻辑配合Visual Studio工程文件和配置文件可直接编译运行同时提供exe与dll程序集方便先运行查看效果再对照源码学习资源文件夹和界面预览图则用来支撑WinForm设计整体结构精简且完整。已有322人学习浏览说明此实例在同类资源中具备不错的参考价值。通过学习读者可以掌握S7.NET库的连接用法、数据读写格式转换等关键知识点并能沿窗体入口理清程序执行顺序项目保留完整解决方案和程序集依赖稍作修改即可迁移到实际PLC控制项目中是工业通信入门和项目借鉴的实用资料。1. 用 C# 和 .NET 连西门子 S7 PLC真正的门槛在 PLC 侧参数“C# 通过 .NET 方式实现与西门子 S7 PLC 通信”这个需求在上位机开发里几乎每天都会出现MES 要采集产线数据配料系统要把参数写进 DB 块设备状态要实时上抛。很多人以为这事的难点在协议解析其实今天 .NET 生态里 S7 协议早被封装成现成 NuGet 包几行代码就能把 S7-1200 的 DB 读回来。真正卡住人的是 PLC 侧参数DB 是否开了优化块访问、CPU 是否允许 PUT/GET 通信、Rack 与 Slot 是否填对这三项错任何一个报错方式都不同而它们和 C# 代码本身无关。下面从协议栈讲清 S7 通信的边界给出一份连接 S7-1200/1500 的最小可运行代码再把批量读取、UI 刷新和排错清单一次说透新手能照着跑通老手可以核对参数边界。2. 从 S7 协议到 .NET 库选型S7.Net 与 Sharp7 怎么选2.1 S7 通信的本质TCP 102 端口上的 TPKT、COTP 与 S7Comm西门子 S7 以太网通信并不是普通 TCP 上直接跑业务数据而是三层嵌套最外层是 TPKT负责声明后续数据包长度中间是 COTP负责建立和维持传输连接最里层才是 S7Comm也就是真正承载“读 DB、写 M 区”请求应答的协议。C# 里用 Socket 连上 PLC 的 102 端口只是完成了 TCP 层后面还有两次握手COTP 的 Connection Request/Confirm以及 S7 层的 PDU 长度协商全部走完PLC 才开始接受读写请求。这三层协议每一层都有固定字节结构比如 TPKT 前四个字节是版本号和保留位后两个字节是总长度COTP 的 CR 报文里带有本地和远程 TSAPTSAP 又和 Rack、Slot 强相关。用代码自己实现不是不行但要做完 CRC、分包、重传、长数据拆分这些事工作量足够写一个小型通信组件。这也是为什么主流做法是直接用社区封装好的库把精力留在业务参数上。验证网络层是否通可以先用一段最小 C# 代码探测 102 端口using System.Net.Sockets; using var client new TcpClient(); var connectTask client.ConnectAsync(192.168.0.10, 102); var done await Task.WhenAny(connectTask, Task.Delay(2000)); if (done connectTask client.Connected) { Console.WriteLine(TCP 102 端口已连通); } else { Console.WriteLine(TCP 连接超时先查网络和防火墙); }这段代码的逻辑是同时等待连接完成和 2 秒超时谁先完成看谁。注意 PLC 的 102 端口只对“允许 PUT/GET 通信”的 CPU 开放并且只接受 ISO-on-TCP 握手普通 TCP 探测工具显示端口开放不代表 S7 能通信但端口都不通时一定是网络层问题。参数说明里值得留意的是超时时间PLC 响应通常几十毫秒2 秒已经足够区分网络不通和协议不匹配。2.2 NuGet 里的两个主力库S7.Net 与 Sharp7 的取舍.NET 生态里用来连西门子 S7 的开源库最常用的是 S7.NetNuGet 包名为 S7netplus和 Sharp7。两者封装的都是同一套 S7 协议但风格差异很大选型取决于项目规模和你对数据处理的习惯。对比项S7.Net (S7netplus)Sharp7API 风格强类型直接按地址读对象字节缓冲S7Client.ReadArea 返回 byte[]上手难度低几行代码读写 DB中需要自己解析字节序批量读取提供 ReadMultipleVars提供 ReadMultiVars但也要拼字节适合场景点数少、逻辑简单的上位机点数多、追求吞吐的采集程序Real/Int 转换自动完成手动用 S7.GetRealAt / S7.GetIntAt我一般这么选项目里数据点几十个、以 UI 展示为主就用 S7.Net代码量最少要每秒采上千个点、做高速数据归档用 Sharp7 或直接基于原生 Socket 重写轮询更合适。S7.Net 的 Read 方法返回 object每次读取都有装箱拆箱高频采集时这部分开销会被放大这会在后续循环采集章节专门处理。如果项目还要打通三菱、Modbus、西门子多个品牌可以看跨品牌通信库 HslCommunication它对西门子各系列的支持粒度更细包括 S7-200 SMART 这种协议族有差异的机型。S7-200 SMART 的协议实现和 S7-1200 并不完全一致直接套用常见 CpuType 可能连不上用之前务必确认库是否明确支持该机型。2.3 连接前必须确认的 PLC 侧设置PUT/GET 与优化块访问C# 代码写得再规范PLC 侧不开权限也是白搭。S7-1200/1500 默认情况下不允许外部设备通过 S7 协议读写必须在 TIA Portal 里手动开启进入 CPU 组态找到“防护与安全”下的“连接机制”勾选“允许来自远程对象的 PUT/GET 通信访问”。这一步不做C# 端 Open 可能成功但真正 Read 时会被拒绝而且报错信息不一定直观。比 PUT/GET 更隐蔽的是 DB 块的“优化的块访问”选项。S7-1200/1500 新建的 DB 默认勾选优化块访问这种块没有固定偏移地址S7.Net 用 DB1.DBD0 这类绝对地址去读会失败。解决办法是在 DB 属性里取消“优化的块访问”重新编译下载后才会生成偏移地址。S7-300/400 的 DB 默认非优化基本没有这个问题所以很多老工程师第一次碰 1200 都会在这卡住。S7-1500 对优化块的支持更彻底如果项目不允许取消优化访问那 S7 协议这条路就走不通只能换 OPC UA 或西门子官方 API这个边界在项目选型时就要确认。提示取消优化块访问会改变 DB 内部布局且无法直接恢复原状务必在 PLC 程序编译下载前另存一份备份。3. 最小可运行实例用 S7.Net 读写 S7-1200 的 DB 与 M 区3.1 建立连接CpuType、IP 与 Rack/Slot 的对应关系S7.Net 建立连接只需一行代码构造函数里四个参数全是坑。以 S7-1200 为例标准写法是new Plc(CpuType.S71200, 192.168.0.10, 0, 1)其中第三、四个参数是 Rack 和 Slot。Rack 是机架号Slot 是 CPU 所在插槽号不同系列的默认值不一样最常搞错的是 S7-300 和 S7-400。PLC 系列常用 Rack常用 Slot备注S7-120001绝大多数固件适用S7-150001部分早期组态为 0/0S7-30002导轨式安装CPU 常在 2 号槽S7-40003机架式CPU 在 3 号槽居多Rack/Slot 的作用是生成 COTP 握手时的 TSAP 值填错时 TCP 连接能建立但 COTP 握手会被 PLC 拒绝现象表现为 Open 卡住或直接抛异常。S7.Net 里 CpuType 枚举映射了 CPU 型号S71200、S71500、S7300、S7400 是常用项选错型号会导致 PDU 协商参数不匹配。连接代码和基本读写可以这样写using S7.Net; var plc new Plc(CpuType.S71200, 192.168.0.10, 0, 1); plc.Open(); float temp (float)plc.Read(DB1.DBD0); short speed (short)plc.Read(DB1.DBW4); bool start (bool)plc.Read(DB1.DBX2.0); plc.Write(M0.0, true); plc.Write(DB1.DBD8, 25.5f); plc.Close();代码的逻辑是先通过 Open 完成 TCP 连接和 S7 握手之后 Read 和 Write 都基于同一连接进行最后 Close 释放。地址语法里 DB1.DBD0 表示 DB1 从偏移 0 开始的双字4 字节通常对应 PLC 侧 Real 或 DInt 类型DB1.DBW4 是偏移 4 开始的字2 字节对应 IntDB1.DBX2.0 是 DB1 偏移 2 字节的第 0 位对应 Bool。M0.0 是位存储区地址与 DB 无关。需要特别说明Read 返回的是 object强转类型必须和 PLC 侧声明一致Real 读出来强转 float 没问题但如果 DB 里声明的是 DInt 却按 Real 读数值会完全错乱这是地址对齐最常见的坑。3.2 读写 DB 与 M 区使用 ReadBytes 做更底层的读取Read 方法虽然方便但遇到 PLC 侧是自定义结构体、数组或者字符串时就不够用了。更底层的做法是用 ReadBytes 直接按字节区间读取然后自己在 C# 侧解析。这样做的好处是读写范围和解析逻辑完全可控不再依赖库内置的类型映射。byte[] data plc.ReadBytes(DataType.DataBlock, 1, 0, 20); float temp BitConverter.ToSingle(data, 0); short speed BitConverter.ToInt16(data, 4); uint counter BitConverter.ToUInt32(data, 6);这段代码从 DB1 的偏移 0 开始读 20 个字节然后按位置解析出 4 字节的 float、2 字节的 short 和 4 字节的 uint。参数含义很直接DataType.DataBlock 指定访问数据块第二个参数 1 是 DB 编号第三个参数 0 是起始字节偏移第四个参数 20 是要读取的总字节数。需要强调的是字节序西门子 PLC 使用大端序而 BitConverter 在大多数 Windows 机器上按小端序解析所以解析前要做反转sbyte[] reversed data[..4].Reverse().ToArray(); float temp BitConverter.ToSingle(reversed, 0);这一条几乎每个从零写采集程序的人都会踩一次。如果你用 S7.Net 的 Read 方法库内部已经处理了字节序不会遇到这个问题。但一旦切到 ReadBytes大端转小端就变成你自己的责任。批量读 DB 时建议统一按字节区间读取然后集中做一次字节序转换避免对每个变量都调用一次 Read 导致多次 TCP 往返。3.3 Open 抛异常时的排查顺序网络、握手、权限PLC 通信报错时最忌讳逐个猜。S7.Net 的 Open 会抛出 PlcException异常里带 ErrorCode但错误码信息量有限多数人要查半天。我建议按固定顺序排查先确认 TCP 通不通再确认 Rack/Slot 是否正确然后确认 PUT/GET 权限最后确认 DB 地址。前两个问题主要在 Open 阶段暴露后两个问题通常在第一次 Read 或 Write 时暴露。try { plc.Open(); _ plc.Read(DB1.DBD0); } catch (PlcException ex) { Console.WriteLine($ErrorCode: {ex.ErrorCode}); Console.WriteLine($Message: {ex.Message}); }这个简短逻辑的价值在于把协议层的错误暴露出来。S7 协议的错误码大体分两类0x81 段多位对象不存在或地址类型不对0x80 段多位请求不被支持比如 PUT/GET 未开启。看到 0x81 就先检查 DB 地址和偏移看到 0x80 就先回 PLC 侧看连接机制。值得说明的是Open 成功不代表能读写很多 PUT/GET 问题会延迟到第一次 Read 才暴露所以测试代码至少要包含一次真实读写只测 Open 不算验证通过。4. 循环采集不卡 UIS7.Net 批量读取与后台轮询写法4.1 ReadMultipleVars 一次取回多个变量逐条调用 Read 是最直观的写法但每条 Read 都是一次完整的 S7 请求应答往返读 30 个变量就是 30 次网络往返。S7-1200 单条读写响应在毫秒级30 个变量积攒下来至少几十毫秒再加上 UI 线程渲染循环采集和 UI 刷新卡顿是必然结果。S7.Net 提供的 ReadMultipleVars 把多个读取请求合并到一个 PDU 里一次往返取回全部数据。var items new[] { new DataItem(DataType.DataBlock, 1, 0, VarType.Real, 1), new DataItem(DataType.DataBlock, 1, 4, VarType.Int, 1), new DataItem(DataType.Memory, 0, 0, VarType.Bit, 1), new DataItem(DataType.DataBlock, 1, 40, VarType.S7String, 20) }; var results plc.ReadMultipleVars(items); float temp (float)results[0]; short speed (short)results[1]; bool running (bool)results[2]; string name (string)results[3];DataItem 构造函数的五个参数分别是数据类型、DB 编号、起始地址、变量类型和个数结果按传入顺序返回。注意 VarType.S7String 的第 5 个参数 20 表示最大字符长度PLC 侧字符串要声明成相同长度否则读回来的字符串可能被截断。ReadMultipleVars 适合固定点位的采集点位列表通常在程序启动时构建一次不要每次轮询都 new 一批 DataItem那样会引入额外的 GC 压力。S7 协议单次 PDU 有长度限制一次读取的总字节数不要超过 240 字节左右超过时分组读取这个限制来自 S7 协议自身的 PDU 协商不是库的缺陷。4.2 后台采集与 UI 定时刷新解决循环数据采集和 UI 刷新卡顿UI 卡顿的本质是采集和渲染抢占同一个线程。S7.Net 的 Read 是同步阻塞方法如果直接在按钮事件里 while 循环调用界面从点击到结束完全无响应几秒后系统会提示“程序未响应”。常见做法是把采集放到后台线程用快照对象保存最新数据帧UI 线程用定时器只负责读快照和刷新界面两者通过锁或并发集合隔离。public sealed class PlcPollingService { private readonly Plc _plc; private readonly ConcurrentDictionarystring, object _snapshot new(); private CancellationTokenSource? _cts; public PlcPollingService(Plc plc) _plc plc; public void Start(int intervalMs 200) { _cts new CancellationTokenSource(); Task.Run(() PollLoop(_cts.Token, intervalMs)); } private async Task PollLoop(CancellationToken token, int intervalMs) { var items BuildDataItems(); while (!token.IsCancellationRequested) { try { var values _plc.ReadMultipleVars(items); for (int i 0; i items.Length; i) { _snapshot[items[i].VarName] values[i]; } } catch (PlcException) { // 断线处理记录时间等待下次轮询重连 } await Task.Delay(intervalMs, token); } } public T? GetT(string key) _snapshot.TryGetValue(key, out var v) ? (T)v : default; }这段代码的逻辑核心是采集循环只负责把最新值写入快照不接触任何 UI 控件因此 UI 线程永远不被网络阻塞。UI 侧用 DispatcherTimer 每 100 毫秒读一次快照并刷新 TextBlock最终呈现的效果是界面不掉帧PLC 通信也不中断。参数说明里有几个取舍intervalMs 设为 200 毫秒是最常见的起步值对应 5Hz 采集频率能满足大多数设备监控场景低于 50 毫秒时 S7-1200 的响应能力会逼近极限而且频繁的 PDU 交互会挤占 PLC 的通信资源。ConcurrentDictionary 在这里比普通 Dictionary 合适因为它保证后台线程写入和 UI 线程读取之间不会出现“读到半截数据”的竞态。这个方案的局限是轮询本身有延迟PLC 侧数据变化要在下一个采集周期才能看到对实时性要求特别高的场景可以缩短间隔但不要试图在 UI 线程直接做高频轮询。4.3 字符串与字节解析从 DB 读一段原始数据S7 协议里的字符串不是 .NET 那种长度前缀而是两字节头加内容体的结构第一个字节是字符串最大长度第二个字节是当前有效长度从第三个字节开始才是真正的字符内容。直接用 Read 读字符串有时能成功但如果 PLC 侧声明长度和 C# 侧不一致读回来的字符会多出尾部空格或乱码。最可靠的方式是 ReadBytes 后手动按 S7 格式解析。byte[] raw plc.ReadBytes(DataType.DataBlock, 1, 40, 22); int maxLen raw[0]; int curLen raw[1]; string value Encoding.ASCII.GetString(raw, 2, curLen);这段代码的逻辑是先读 DB1 偏移 40 处的 22 个字节22 对应 2 字节头加 20 字节最大内容然后从 raw[1] 取当前有效长度最后从第 2 字节开始截取定长内容。注意 PLC 字符串是 ASCII 编码如果 PLC 里声明的是 WString宽字符串内容是 UTF-16 编码要改用 Encoding.Unicode 解析字节数也要相应翻倍。这个解析细节在做配方管理、读取报警文本时基本都会遇到。另一个实用场景是读数组比如 PLC 侧有 10 个 Real 组成的数组ReadBytes 按 40 个字节一次读回再用循环从每个 4 字节切片转 float比调用 10 次 Read 快得多。5. S7 通信排错参数清单与一个验证技巧5.1 连不上 PLC 时的核对参数表PLC 通信排错不是看代码而是按参数表逐项核对。下面这张表覆盖了我在现场遇到过的绝大多数场景每一项都对应一个明确操作。现象常见原因处理动作TCP 102 端口不通网线/交换机/防火墙ping 通后用 telnet 验证 102 端口Open 超时Rack/Slot 填错按机型对照 Rack/Slot 表修改Open 成功但 Read 报 0x81地址不存在或类型不符核对 DB 号和偏移地址检查 DB 长度Read 报 0x80PUT/GET 未勾选回 TIA Portal 勾选连接机制能读保护 DB 但读不到值优化块访问未取消取消优化访问后重新编译下载虚拟机里连不上 PLCNAT 网络导致 PLC 无法回连把虚拟机网络改为桥接模式最后一行值得单独说很多人习惯在 VMware 里做 C# 开发虚拟机用 NAT 模式时虚拟机可以访问外网但 PLC 回应虚拟机发出的握手包时会因为 NAT 地址转换找不到目标表现为 TCP 能连上但 S7 握手一直没有响应。改成桥接模式后虚拟机直接占用物理网段地址PLC 回包不再经过宿主机的 NAT 转译问题立刻消失。5.2 用 ReadBytes 缩小问题范围业务代码里叠加了各种类型转换出问题时很难判断是连接问题还是解析问题。一个实用的定位手段是最小化读取只读 4 个字节看能不能拿到原始数据。byte[] probe plc.ReadBytes(DataType.DataBlock, 1, 0, 4); Console.WriteLine(BitConverter.ToString(probe));如果 probe 能正常输出 4 字节十六进制说明连接、协议、DB 访问全通问题出在后续的类型解析或地址换算如果 probe 抛异常问题在连接层或 PLC 侧权限根本还没走到数据类型那一步。这样一拆排查范围直接缩小一半。5.3 用抓包确认握手是否完成当代码链路看不出问题时抓包是最客观的验证方式。Wireshark 里设置过滤条件tcp.port 102连一次 PLC 再断开正常情况能依次看到 TCP 三次握手、COTP 的 Connection Request 和 Connection Confirm以及若干 S7 Read/Write 请求。如果在 TCP 握手后看不到 COTP Confirm说明 Rack/Slot 或 TSAP 配置有问题PLC 已经拒绝应用层握手如果 COTP 正常但看不到 S7 请求说明 PUT/GET 权限受限。把这组过滤条件保存成 Wireshark 常用过滤配置之后每次连不上都能直接打开对照比反复猜测参数高效得多。本文还有配套的精品资源点击获取
返回列表