尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#使用Snap7 DLL实现西门子S7-200 SMART以太网通信读写

C#使用Snap7 DLL实现西门子S7-200 SMART以太网通信读写 做工控这些年西门子S7-200 SMART的以太网通信是我被问得最多的题目之一。不少朋友一上来就找西门子官方通信库结果要么被OPC方式绕晕要么在协议文档里一通翻折腾一天连一次握手都没完成。后来我干脆转到开源库 Snap7——它自带原生 DLL 和 C# 封装直接走 S7 协议200 SMART 的 V 区在底层恰恰映射成 DB1只要调 DBRead/DBWrite 就能把数据读出来、写进去。这篇文章就把我平时怎么用 Snap7 DLL 跟 S7-200 SMART 做数据读写的过程完整写出来附带能直接跑的 C# 代码很适合第一次接触 PLC 通信、或者被官方库劝退过的朋友。1. 先搞清楚为什么我推荐 Snap7 而不是官方库1.1 200 SMART 做上位机通信你其实没几个选择S7-200 SMART 的以太网口是标准配置但上位机要跟它通信路子并没有想象中多。我实际用过的方案主要有三条PC Access SMART 走 OPC、Modbus TCP、直接用 Snap7 走 S7 协议。PC Access SMART 是西门子官方的 OPC 服务器配置起来要先建变量、再映射地址上位机还要装 OPC 客户端调试时最怕遇到“变量明明是绿色的数据却一直为 0”这种玄学问题。而且 OPC 多了一层进程转发做数据采集还能凑合一旦要做快速联动控制延迟和吞吐量都让人着急。Modbus TCP 倒是很直接但 200 SMART 那边你得在程序里调用 MBUS_SERVER 之类的指令块维护一张 Modbus 寄存器映射表上位机侧也要自己组帧、解析工程量不小。Snap7 走了另一条路它直接把 S7 协议封装成一套跨平台的 C/C 动态库C#、Python、Java、LabVIEW 都能调。对 S7-200 SMART 来说PLC 侧完全不用加任何通信块只要网口能用上位机就能用 Snap7 的 DBRead/DBWrite 去访问数据区省掉了中间那一大堆配置环节。1.2 Snap7 的 DLL 到底是怎么回事很多人看到“DLL”就先慌其实 Snap7 这个项目把东西分得很清爽一个snap7.dll是原生动态库负责和 PLC 实际通信里面是 C 写的另一个Snap7.Net.dll是 C# 的托管封装把 native 接口包装成你熟悉的 S7Client 类。你写 C# 程序时只需要引用Snap7.Net.dll但运行的时候snap7.dll必须跟着 exe 一起待在输出目录否则就会报DllNotFoundException。它在 Windows、Linux、macOS 上都有编译好的二进制包甚至还有树莓派 ARM 版本这一点比很多厂商私有库强太多。许可证方面用的是 GPLv3如果是闭源商用产品需要提前评估一下授权风险社区里常见做法是把 Snap7 放在独立进程里以服务方式运行用 RPC 和主程序交互这样至少能实现一定的隔离。个人学习和内部工具则完全不用担心这个问题。1.3 三种通信方式怎么选对比项PC Access SMARTModbus TCPSnap7配置复杂度高中低PLC 侧改动需要配置 OPC 变量需要写 Modbus 指令块无需额外通信块通信性能一般中高跨语言支持OPC 客户端各语言 Modbus 库C# / C / Python 等适合场景监控大屏、数据中心采集中小规模数据交换快速上位机、边缘计算、频繁读写联动从我自己的项目经验看只要你是用 C# 做 WinForm/WPF 上位机并且想快速做出一个能读写 200 SMART 的界面Snap7 是最省心的。它把最麻烦的协议细节都藏了起来但又保留了足够的可控性比如超时、连接类型、PDU 长度这些参数都可以调。2. 通信前必须弄明白的 3 个底层细节2.1 200 SMART 的内存区在 Snap7 里怎么寻址S7-200 SMART 的编程软件里你会看到 I、Q、M、V、SM 这些存储区其中 V 区是给用户自由使用的主要数据区我们上位机读写的绝大多数变量都放在 V 区。但 Snap7 默认的接口是围绕“DB 数据块”设计的比如 S7-300/1200/1500 会用到 DB1、DB2那 200 SMART 没有 DB 块怎么办答案很巧妙在 S7 协议的底层映射里200 SMART 的 V 区就是 DB1。也就是说DBRead(1, 0, 1, buffer)读的其实是 VB0DBWrite(1, 100, 2, buffer)写的其实是 VW100。这一点我第一次用的时候也绕了半天因为按照 300/1200 的习惯去建 DB 编号结果发现根本不需要在 PLC 里建任何 DB 块。非 V 区的访问则是这样映射的I 区对应S7AreaPEQ 区对应S7AreaPAM 区对应S7AreaMK。如果你要读 M0.0可以走ReadArea指定 Area 为 S7AreaMK、偏移为 0如果只是读写 V 区直接用 DBRead/DBWrite 把 DBNumber 固定为 1 就行。2.2 字节序为什么用 BitConverter 读出来是乱的S7 协议走的是大端字节序就是说一个 16 位整数高字节排在前面低字节排在后面。C# 的BitConverter在 x86/x64 平台上是小端序低字节在数组前面。所以你从 PLC 读到 4 个字节直接BitConverter.ToSingle(buffer, 0)得到的 float 大概率是一个天文数字。正确做法是先把byte[]反转一下再交给 BitConverter 解析。读 16 位值也可以不依赖 BitConverter手动组合short value (short)((buffer[0] 8) | buffer[1]);对于 32 位浮点或双字就先把 byte 数组反转再转换。我见过太多新人在这一步翻车读出来一个 3.14 变成 1078523331 之类的一脸懵。这不是 Snap7 的问题是字节序的锅。2.3 Rack、Slot 和连接参数真的不能乱填ConnectTo(ip, rack, slot)这个函数里rack 和 slot 是两个让无数新手抓狂的参数。S7-300/400 需要按硬件机架和插槽去填但 S7-200 SMART 的固定组合一般是ConnectTo(192.168.2.1, 0, 1)也就是说 Rack0Slot1。我见到有人拿 S7-1200 的习惯填 Slot1 是没问题的但也有人把 Slot 填 0结果一直连不上。如果默认参数连不上可以试试 Slot0 或者 Slot2但我实际测试下来 200 SMART 绝大多数情况就是 Slot1。连接类型方面Snap7 的默认 Pg 连接类型就够了相当于以编程器身份访问不必额外设置。另外要注意你电脑的 IP 必须和 PLC 在同一个网段比如 PLC 是 192.168.2.1那电脑就设成 192.168.2.10子网掩码 255.255.255.0。这是连不上时最容易忽略的基础问题。3. 手把手实操C# Snap7 DLL 读写 S7-200 SMART3.1 开发环境与工程配置我用的是 Visual Studio 2022创建 WinForm 项目目标框架选 .NET Framework 4.7.2。Snap7.Net.dll 这个封装文件是 .NET Standard 兼容的在 .NET Core/.NET 5 里也能引用但我实际项目里还是习惯跑在 .NET Framework 上稳定。先把 Snap7 官方发布包里的snap7.dll和Snap7.Net.dll拷贝到工程目录下然后在解决方案资源管理器里右键“引用”添加Snap7.Net.dll。注意一个小细节snap7.dll是原生非托管库不能被“添加引用”你只需要把它放到 exe 的输出目录或者把它加进项目把“复制到输出目录”设为“如果较新则复制”否则程序一运行就会报找不到 DLL。还有一个很容易踩的坑是“位数”。如果你的程序在“任何 CPU”下编译在 64 位操作系统上会以 64 位进程运行此时如果只放了 32 位的snap7.dll会直接抛BadImageFormatException。我现在的习惯是直接在“解决方案管理器”里把平台目标固定成 X64同时使用 64 位版本的snap7.dll。如果客户机器是 32 位系统那就反过来固定 X86。尽量别用 AnyCPU省得给自己挖坑。3.2 连接、断开和错误处理先建立连接。S7Client 是 Snap7 在 C# 里最核心的类ConnectTo返回 0 表示成功非 0 就是错误码可以用ErrorText转成可读的描述using Snap7; S7Client client new S7Client(); int result client.ConnectTo(192.168.2.1, 0, 1); if (result 0) { Console.WriteLine(连接成功); } else { Console.WriteLine($连接失败错误码{result}信息{client.ErrorText(result)}); }断开时调用Disconnect我在封装类里都会先判断Connected()再断开避免重复断开导致异常。还有一个重要习惯通信异常后不要立刻重连稍微等一下或者做几次重试。有些现场网络不太稳定在重连逻辑里加一个循环如最多尝试 3 次每次间隔 1 秒能有效提高恢复成功率。3.3 读取 V 区Bool、Int、Real 的封装读取 V 区最核心的方法就是DBRead(1, start, size, buffer)其中1表示 DB1也就是 200 SMART 的 V 区。我们在应用层经常需要读取不同类型的数据所以我会封装成一个 Helper 类。比如读一个 Bool本质是读 V 区的一个字节再取出对应位读一个 Int需要读连续的 2 个字节并做大小端转换读一个 Real需要读 4 个字节再反转。下面这段代码是我在项目里反复使用的一个辅助类封装了连接、断开、读位、写字、读整数、读浮点、写浮点直接拷贝就能用using System; using Snap7; namespace Snap7S7_200Demo { public class S7PlcHelper { private S7Client _client new S7Client(); public bool IsConnected _client.Connected(); public void Connect(string ip, int rack 0, int slot 1) { int err _client.ConnectTo(ip, rack, slot); if (err ! 0) { throw new Exception($连接失败错误码{err}{_client.ErrorText(err)}); } } public void Disconnect() { if (_client.Connected()) { _client.Disconnect(); } } public bool ReadBit(int byteOffset, int bitIndex) { byte[] buffer new byte[1]; int err _client.DBRead(1, byteOffset, 1, buffer); if (err ! 0) throw new Exception(_client.ErrorText(err)); return (buffer[0] (1 bitIndex)) ! 0; } public void WriteBit(int byteOffset, int bitIndex, bool value) { byte[] buffer new byte[1]; int err _client.DBRead(1, byteOffset, 1, buffer); if (err ! 0) throw new Exception(_client.ErrorText(err)); if (value) buffer[0] | (byte)(1 bitIndex); else buffer[0] (byte)~(1 bitIndex); err _client.DBWrite(1, byteOffset, 1, buffer); if (err ! 0) throw new Exception(_client.ErrorText(err)); } public short ReadShort(int byteOffset) { byte[] buffer new byte[2]; int err _client.DBRead(1, byteOffset, 2, buffer); if (err ! 0) throw new Exception(_client.ErrorText(err)); return (short)((buffer[0] 8) | buffer[1]); } public void WriteShort(int byteOffset, short value) { byte[] buffer new byte[] { (byte)(value 8), (byte)(value 0xFF) }; int err _client.DBWrite(1, byteOffset, 2, buffer); if (err ! 0) throw new Exception(_client.ErrorText(err)); } public int ReadDInt(int byteOffset) { byte[] buffer new byte[4]; int err _client.DBRead(1, byteOffset, 4, buffer); if (err ! 0) throw new Exception(_client.ErrorText(err)); uint a (uint)buffer[3] | ((uint)buffer[2] 8) | ((uint)buffer[1] 16) | ((uint)buffer[0] 24); return (int)a; } public float ReadReal(int byteOffset) { byte[] buffer new byte[4]; int err _client.DBRead(1, byteOffset, 4, buffer); if (err ! 0) throw new Exception(_client.ErrorText(err)); Array.Reverse(buffer); return BitConverter.ToSingle(buffer, 0); } public void WriteReal(int byteOffset, float value) { byte[] buffer BitConverter.GetBytes(value); Array.Reverse(buffer); int err _client.DBWrite(1, byteOffset, 4, buffer); if (err ! 0) throw new Exception(_client.ErrorText(err)); } } }3.4 在 WinForm 里调用一个能直接跑的示例光有 Helper 类还不够直观我写一个简单窗口例子。界面上放一个文本框输入 IP连接按钮断开按钮然后一个定时器或者按钮触发读取。下面的片段展示了在按钮事件里怎么用private S7PlcHelper _plc new S7PlcHelper(); private void btnConnect_Click(object sender, EventArgs e) { try { _plc.Connect(txtIp.Text.Trim(), 0, 1); lblStatus.Text 已连接; } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void btnRead_Click(object sender, EventArgs e) { try { bool startSignal _plc.ReadBit(0, 0); // 读 V0.0 short speed _plc.ReadShort(100); // 读 VW100 float temperature _plc.ReadReal(200); // 读 VD200 lblStart.Text startSignal ? 运行 : 停止; lblSpeed.Text speed.ToString(); lblTemp.Text temperature.ToString(F2); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void btnWrite_Click(object sender, EventArgs e) { try { _plc.WriteShort(100, short.Parse(txtSetSpeed.Text)); // 写 VW100 _plc.WriteBit(0, 1, true); // 置位 V0.1 } catch (Exception ex) { MessageBox.Show(ex.Message); } }这里有个容易被混淆的细节PLC 编程软件里屏显地址 VD200对应到DBRead的字节偏移就是 200不需要再加 0 或减 1。地址命名里的 VW、VD 只是告诉你说这个地址是几字节类型偏移量本身按字节计算。比如 VW100 占 100 和 101 两个字节VD100 占 100 到 103 四个字节。3.5 批量读取别一个地址读一次很多新人一上来就按变量逐个去读一次读一个 Int、一个 Real这样不仅慢还会给 PLC 带来不必要的通信压力。S7 通信是按 PDU 分包传输的连续地址的数据在一个包内读取最高效。比如你要读 VW100、VD102、VW106实际上可以从偏移 100 开始连续读 8 个字节然后自己在内存里拆分byte[] buffer new byte[8]; int err _client.DBRead(1, 100, 8, buffer); if (err ! 0) return; short vw100 (short)((buffer[0] 8) | buffer[1]); float vd102 ReadFloatFromBuffer(buffer, 2); short vw106 (short)((buffer[6] 8) | buffer[7]);ReadFloatFromBuffer就是对 buffer 里从 index 开始的 4 个字节做反转再从 BitConverter 转换本质和前面的 ReadReal 一样只是不再触发新的通信。实际项目中我通常把要采集的 PLC 地址排好序尽量合并成几段连续区间一段一次 DBRead一个 50 变量的界面轮询下来也就几次通信就完成。4. 连接不上、数据错乱、DLL 加载失败这份排查记录直接收藏4.1 连不上IP、防火墙、Slot 和安全软件最开始遇到连接失败先别急着怀疑代码。先用ping 192.168.2.1确认电脑和 PLC 在同一个二层网络ping 不通就检查网线、IP 地址、子网掩码。我曾经在客户现场碰到一台电脑是自动获取 IP 的结果把网线从交换机拔下来直接插到 PLC 上电脑网段变成 10.x 开头PLC 是 192.168.2.1折腾了半天才发现是网段不对。第二件事是 Windows 防火墙Snap7 通信默认走 TCP 102 端口开发阶段干脆把防火墙里的“专用网络”临时关掉测试如果关了能连上那就是防火墙规则没放行 102。还有安全软件某些杀毒软件会拦截 PLC 扫描和 S7 通信行为如果电脑装了企业版 EDR注意看日志有没有阻断记录。再就是 Slot 参数默认 0,1 如果不行把 slot 改成 0 或 2 都试一下有些老固件的 200 SMART 比较挑参数。4.2 能连上但读出来的数据完全不对连上了数据不对九成是字节序和地址换算的问题。读 16 位整数时如果直接BitConverter.ToInt16(buffer, 0)而不是手动(buffer[0] 8) | buffer[1]读出来的数大概率是反的。读 32 位浮点也一样先把数组反转再转换。然后是地址偏移算错。PLC 里写的是 VD100你在代码里读DBRead(1, 100, 4, buffer)是对的但如果写的是 DB100.DBD0 这类表达方式那又不一样200 SMART 里基本只认 V 区别把西门子 300 的 DB 习惯带过来。还有一种情况是 PLC 程序里你访问的 V 区根本没有数据比如程序往 VD100 里写但你读的是 VW100那么读出来的可能只是 VD100 的高 16 位导致看起来是乱码。Bool 位也常出错。V10.3 表示第 10 个字节的第 3 位byteOffset 是 10bitIndex 是 3。我见过有人把 bitIndex 填成 10直接越位。Snap7 不会报错只会给出一个错误的结果。4.3 通信频繁掉线、卡顿甚至把 PLC 拖死S7-200 SMART 的 CPU 性能不像 S7-1200/1500 那么强上位机如果每 10ms 就疯狂读八个地址PLC 通信负载会明显升高极端情况下会影响 PLC 扫描周期甚至导致看门狗报警。我实测下来50ms 的轮询间隔对大多数 200 SMART 项目都足够如果是纯数据显示100ms 都行。做设备联动控制时也尽量把重要的 Bool 和关键浮点放在同一个连续区间一次读取。另外Snap7 的 C# 封装不是线程安全的。如果你开了多个线程同时读写同一个 S7Client 实例会出现奇奇怪怪的报错和超时。解决办法很简单要么所有通信操作串行化放同一个线程或加锁要么每个线程各自建一个连接但连接数不要太多一般 2 到 3 个就能满足需求。Snap7 的 PDU 长度也默认受 PLC 限制不用刻意调大出现TLRequest或ISO相关报错时先检查是不是一次读太大的长度。4.4 DLL 加载失败这类问题怎么解决DllNotFoundException是最常见的运行时报错八成是snap7.dll不在 exe 目录下或者你引用了Snap7.Net.dll但忘了把snap7.dll放到输出目录。在 Visual Studio 里把原生 DLL 加入项目然后设置“复制到输出目录”为“如果较新则复制”基本能解决。“试图加载格式不正确的程序”也是高频问题这通常是位数不匹配。项目平台是 AnyCPU 会变成 64 位进程但snap7.dll是 32 位的直接报这个错。把解决方案平台固定成 X64配 64 位 DLL或 X86配 32 位 DLL。还有一个不太容易察觉的问题是缺 VC 运行库。snap7.dll是 C 写的在干净的系统上可能缺少 Visual C Runtime导致加载失败。去微软官网装一个 vcredist 2015-2022 合集就能解决。遇到 DLL 相关报错时不要第一时间去找那些所谓的系统修复工具绝大多数情况下就是位数、路径、运行库三个原因。现象原因解决DllNotFoundExceptionsnap7.dll 不在输出目录把 DLL 放到 exe 目录并设置复制到输出BadImageFormatException项目位数与 DLL 位数不匹配平台目标固定 X64/X86别用 AnyCPU连接超时网段不对、防火墙拦截、Slot 不对ping 测试、放行 102 端口、检查参数读数据乱码字节序、地址偏移、类型长度错误手动大端拼接核对地址和长度通信掉线轮询太频繁、多线程并发加长轮询周期、加锁或独立连接如果你是在网上找某个“dll 缺失修复工具”我更建议先按上面的列表自己排查通常几分钟就能定位比装乱七八糟的修复软件靠谱得多。4.5 一个被忽视的细节Micro/WIN SMART 在线监控会占连接S7-200 SMART 的以太网连接数有限如果你一边在 STEP 7 Micro/WIN SMART 里在线监控程序一边又在跑自己的上位机偶尔会出现上位机连接不上的问题。这不是 Snap7 的 bug而是 PLC 侧通信资源被编程软件占住了。调试时尽量把 Micro/WIN SMART 的在线监控停止再让上位机去连。这一点在产线上很常见因为电气工程师调完程序后忘了退出在线监视操作员那边上位机就起不来。5. 最后分享几个我在项目里一直沿用的习惯把这些功能封装成一个独立的类之后我换过好几个项目从上位机软件到边缘数据采集小工具基本都是同一套逻辑直接复用。现在我还会把 PLC 里的数据地址集中放在一个静态类里做常量管理比如PLCAddress.WaterTemp 200界面上按名字引用这样以后 PLC 程序改地址只要维护一个常量文件不用满代码去搜硬编码的 200、201 之类的数字。另一个建议是不要在 UI 线程里直接同步读 PLC。哪怕通信再快也架不住网络抖动一下界面就卡住。我现在一般用BackgroundWorker或者Task做轮询拿到数据后通过事件或者Invoke更新界面界面流畅性会好很多。如果你要把这套东西接进 MES、数据库或者云平台把 Helper 类再包一层对外暴露“读温度”“写速度”这类业务方法就行底层通信完全不用动。PLC 通信说到底没有多么玄乎把地址映射、字节序、连接参数这三个基本功吃透再配合一个趁手的开源库你也能在半天内写出一个能稳定读写 S7-200 SMART 的上位机。希望这篇实战记录能帮你少走一点我当年走过的弯路。
返回列表