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

资讯详情

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

C#调用Snap7实现西门子S7-200 SMART直接通信与数据读写

C#调用Snap7实现西门子S7-200 SMART直接通信与数据读写 做上位机开发这些年我身边不少同事一提到跟西门子 S7-200 SMART 通信第一反应就是“走 OPC 吧”“用 Modbus TCP 吧”好像这样最省事。但你真到现场就会发现OPC 服务器要授权费、要装中间件Modbus 又要对着 PLC 程序一轮一轮改映射表。如果你的系统只是要读几十个点位、写几个启停命令用开源库 Snap7 的 DLL 直连反而最灵活、代码量最少。Snap7 是一套用 C 写的 S7 协议通信库提供 Windows/Linux 下的 DLL 和源码调用方不用理解 S7 协议的底层报文拿到句柄后直接按“区域、DB 号、起始地址、长度、数据类型”去读写即可。这篇实战是从我实际做过的“MES 数据采集网关”项目里整理出来的读取对象就是现场的 S7-200 SMART开发环境是 C# Snap7 DLL后面也给了 Python 调用示例。不管你是做产线数据采集、设备远程运维还是想把 PLC 数据对接到自己的业务系统这篇文章都适合你对照着做。1. 为什么选 Snap7 去啃 S7-200 SMART通信原理要先说透1.1 三条路里为什么我选了 Snap7S7-200 SMART 在网口上能走的通信方式大概有三种OPC、Modbus TCP、以及原生 S7 协议。我个人的结论很直接项目周期短、预算有限、需要快速改点位的时候优先选 Snap7。方案优点缺点OPC Server点位管理方便HMI 和上位机都能用需要授权费配置工程量大现场换一台电脑要重新配置Modbus TCP协议简单很多网关支持PLC 端必须调用 Modbus 服务器指令占用了程序块还要手动做寄存器映射Snap7 直连 S7免费不改 PLC 程序按地址直接读写需要自己理解区域和地址模型出问题只能自己排查我做过一次对比用 OPC 配置 30 个点位加上调试、换机验证大约花了一天用 Snap7 写代码一上午就打通了。前提是你会一点 C#、Python 或者 C而且愿意阅读 Snap7 的几个核心 API。1.2 S7 协议的本质以及 S7-200 SMART 的边界S7 协议是西门子 PLC 的以太网通信协议底层走的是 ISO-on-TCP也就是在 TCP 之上再包一层 COTP 和 TPKT默认端口是 102。你可以直接用 Socket 发报文但重复造轮子没必要Snap7 就是把这些东西封装好了。这里要提醒一句S7-200 SMART 支持 S7 通信但它不是 S7-300/400也不是 S7-1200/1500协议能力有边界。常用的 I、Q、M、V 区读写完全没问题但定时器、计数器的 S7 读取不是所有固件版本都稳我的习惯是直接把定时器/计数器的值在 PLC 程序里搬到 V 区再读既不折腾协议又能保证数据一致性。S7-200 SMART 的 V 区在 Snap7 里对应的是 DB1这个映射关系后面会重点讲。1.3 TSAP 参数为什么会卡住很多人TSAP 是 Transport Service Access Point简单说就是 TCP 连接之上的“二次寻址”通信双方必须约定好这两个数否则连接建不起来。S7-300 常用的本地 TSAP 是 0x0100、远程是 0x0200但对 S7-200 SMART我测试下来最稳的配置是本地 0x0200、远程 0x0200也就是 Snap7 的默认参数。如果你用 Snap7 的 ConnectTo 方法直接连接失败先别改其他东西把 TSAP 显式设成 0x0200/0x0200 试试。这个细节在许多网上文章里被一笔带过实际排障时却是第一个要怀疑的点。2. 环境搭建DLL 版本、工程引用和 PLC 端设置2.1 下载与确认 DLL 位数Snap7 官方提供预编译的 DLL也在 GitHub 上有源码。下载后你一般会得到一个 release 目录里面有Win32和x64两个子目录。很多新手上来就把 64 位 DLL 复制到 32 位程序目录结果抛DllNotFoundException在工控机上折腾半小时才发现是位数不匹配。我的建议是先确认你的开发环境是 x86 还是 x64再把对应目录下的 snap7.dll 复制到 exe 的输出目录。如果你用的是 NuGet 上的 Snap7 包也要留意包默认带的是哪个位数。Python 调用时同样要匹配你在 64 位 Python 里加载 32 位 DLL一样会报 “%1 不是有效的 Win32 应用程序”。2.2 C# 里引用 DLL 的标准姿势C# 调用 C DLL需要声明 DllImport。最稳的方式是把 DLL 放到运行目录然后用[DllImport(snap7.dll, CallingConvention CallingConvention.Cdecl)]声明。Snap7 的函数基本是 C 风格导出用 Cdecl 是对的不要手滑写成 StdCall否则调用栈会乱掉。先创建客户端对象、再连接、再读写、最后释放生命周期要记清楚。我见过有人在循环里反复创建和释放客户端导致端口资源来不及释放后面就连接超时。2.3 PLC 端的 IP 和 DB 区准备在 Micro/WIN SMART 里给 PLC 设好固定 IP比如 192.168.1.10电脑网卡改成同网段 192.168.1.100先ping通再写程序。很多连接超时问题根本原因就是 PC 和 PLC 不在一个网段或者 Windows 防火墙把 102 端口挡了。关于 DB 区再强调一次S7-200 SMART 没有像 S7-300 那样传统意义上的符号 DB它把 V 存储区抽象成 DB1。也就是说你写的VW100、VD200在 Snap7 里访问时对应的是 DB 号 1起始地址就是 V 区的字节偏移量。这个映射一旦搞错读出来的数据要么是错的要么直接返回错误码 0x00000F地址不存在。2.4 一个容易忽略的问题以太网通信连接数S7-200 SMART 的 S7 通信连接数并不是无限的官方默认可能只支持少量并发连接。如果你同时开着 Micro/WIN SMART 监控、触摸屏通信上位机再连就可能出现“时而通、时而不通”的情况。调试阶段建议把编程软件和 HMI 的在线监控断开给上位机留出干净的连接通道。3. 必须吃透的 API 和参数地址映射才是数据读写的关键3.1 三个核心 API 就够用了Snap7 的完整 API 很多但实际读写 S7-200 SMART你只需要掌握这几个Cli_Create创建客户端句柄。Cli_ConnectTo通过 IP、机架号、槽号连接 PLC。Cli_ReadArea读取指定区域的数据。Cli_WriteArea写入指定区域的数据。Cli_Disconnect断开连接。Cli_Destroy释放客户端对象。其中Cli_ReadArea和Cli_WriteArea的参数结构很清晰但很多人被里面的WordLen和Amount搞糊涂。我个人的理解方式是Start是起始地址WordLen是你访问的数据粒度Amount是你要访问的“元素个数”而不是字节数。比如你要连续读 VB100 到 VB103 共 4 个字节WordLen 选S7WLByte0x02Amount 填 4如果你读的是 VW100 起的 4 个字WordLen 选S7WLWord0x04Amount 也填 4。3.2 S7-200 SMART 地址到 Snap7 参数的换算这是整个项目里最容易出错的地方我放在表格里方便你直接查。PLC 地址AreaDB 号Start 偏移WordLenI0.00x81 (PE)00S7WLBitQ0.20x82 (PA)02S7WLBitM0.00x83 (MK)00S7WLBitMB100x83 (MK)010S7WLByteVB1000x84 (DB)1100S7WLByteVW1000x84 (DB)1100S7WLWordVD2000x84 (DB)1200S7WLDWordV100.0 位0x84 (DB)1800S7WLBit位地址的 Start 不是字节地址而是“全局位数偏移”。计算方法是字节地址 × 8 位编号。所以 V100.0 就是100 × 8 0 800。如果你写成 100那读到的其实是 V12.4 附近的位数据对不上很正常。3.3 字节序和浮点数问题西门子无论 200 SMART 还是 300/400存储多字节数据时都是大端模式。高字节在前低字节在后。C# 的BitConverter在小端机器上会把VD200里的 4 个字节按高低顺序弄反导致浮点数读出来是一个巨大的异常值或者 NaN。解决办法是写两个转换辅助函数读出来之后把字节数组倒序再转float写入之前把float转成字节数组后再倒序交给 Snap7。这个坑几乎每个人都会踩一次我在下面代码里直接给你封装好。4. 完整代码示例C# 调用 Snap7 DLL 读写 S7-200 SMART4.1 声明 DllImport 的实用类这里我给一个精简但可直接运行的 C# 封装。你需要提前把 snap7.dll 放到bin\Debug或bin\Release目录。using System; using System.Runtime.InteropServices; public class Snap7Client : IDisposable { // 句柄 private IntPtr _client; // 区域常量 public const int S7AreaPE 0x81; // I 区 public const int S7AreaPA 0x82; // Q 区 public const int S7AreaMK 0x83; // M 区 public const int S7AreaDB 0x84; // DB 区对 S7-200 SMART 是 V 区 // WordLen 常量 public const int S7WLBit 0x01; public const int S7WLByte 0x02; public const int S7WLWord 0x04; public const int S7WLDWord 0x06; public const int S7WLReal 0x08; [DllImport(snap7.dll, CallingConvention CallingConvention.Cdecl)] private static extern IntPtr Cli_Create(); [DllImport(snap7.dll, CallingConvention CallingConvention.Cdecl)] private static extern void Cli_Destroy(IntPtr client); [DllImport(snap7.dll, CallingConvention CallingConvention.Cdecl)] private static extern int Cli_ConnectTo(IntPtr client, string ip, int rack, int slot); [DllImport(snap7.dll, CallingConvention CallingConvention.Cdecl)] private static extern int Cli_Disconnect(IntPtr client); [DllImport(snap7.dll, CallingConvention CallingConvention.Cdecl)] private static extern int Cli_SetConnectionParams(IntPtr client, string localTsap, string remoteTsap, int localTSAP, int remoteTSAP); [DllImport(snap7.dll, CallingConvention CallingConvention.Cdecl)] private static extern int Cli_ReadArea(IntPtr client, int area, int dbNumber, int start, int amount, int wordLen, byte[] buffer); [DllImport(snap7.dll, CallingConvention CallingConvention.Cdecl)] private static extern int Cli_WriteArea(IntPtr client, int area, int dbNumber, int start, int amount, int wordLen, byte[] buffer); public Snap7Client() { _client Cli_Create(); if (_client IntPtr.Zero) throw new Exception(Snap7 客户端创建失败); } public void Connect(string ip, int rack 0, int slot 0) { int result Cli_ConnectTo(_client, ip, rack, slot); if (result ! 0) throw new Exception(连接失败错误码 result); } public void Disconnect() { Cli_Disconnect(_client); } public void Dispose() { if (_client ! IntPtr.Zero) { Cli_Disconnect(_client); Cli_Destroy(_client); _client IntPtr.Zero; } } // 读取一个字节 public byte ReadByte(int area, int db, int start) { byte[] buffer new byte[1]; int result Cli_ReadArea(_client, area, db, start, 1, S7WLByte, buffer); if (result ! 0) throw new Exception(ReadByte 失败错误码 result); return buffer[0]; } // 读取一个 16 位有符号整数 public short ReadInt16(int area, int db, int start) { byte[] buffer new byte[2]; int result Cli_ReadArea(_client, area, db, start, 1, S7WLWord, buffer); if (result ! 0) throw new Exception(ReadInt16 失败错误码 result); return (short)((buffer[0] 8) | buffer[1]); } // 读取一个 32 位浮点数 public float ReadReal(int area, int db, int start) { byte[] buffer new byte[4]; int result Cli_ReadArea(_client, area, db, start, 1, S7WLReal, buffer); if (result ! 0) throw new Exception(ReadReal 失败错误码 result); Array.Reverse(buffer); // 大端转小端 return BitConverter.ToSingle(buffer, 0); } // 写入一个字节 public void WriteByte(int area, int db, int start, byte value) { byte[] buffer new byte[] { value }; int result Cli_WriteArea(_client, area, db, start, 1, S7WLByte, buffer); if (result ! 0) throw new Exception(WriteByte 失败错误码 result); } // 写入一个 16 位有符号整数 public void WriteInt16(int area, int db, int start, short value) { byte[] buffer new byte[2]; buffer[0] (byte)(value 8); buffer[1] (byte)(value 0xFF); int result Cli_WriteArea(_client, area, db, start, 1, S7WLWord, buffer); if (result ! 0) throw new Exception(WriteInt16 失败错误码 result); } // 写入一个 32 位浮点数 public void WriteReal(int area, int db, int start, float value) { byte[] buffer BitConverter.GetBytes(value); Array.Reverse(buffer); // 小端转大端 int result Cli_WriteArea(_client, area, db, start, 1, S7WLReal, buffer); if (result ! 0) throw new Exception(WriteReal 失败错误码 result); } }4.2 连接、读写 V 区和 M 区的完整流程下面这段代码演示了最典型的场景连接 PLC读 MB0、VW100、VD200再往 VW300 写一个整数往 VD304 写一个浮点数。using System; class Program { static void Main(string[] args) { using (var client new Snap7Client()) { // 连接 PLC机架和槽位对于 S7-200 SMART 通常填 0 client.Connect(192.168.1.10, 0, 0); Console.WriteLine(连接成功); // 读 MB0 byte mb0 client.ReadByte(Snap7Client.S7AreaMK, 0, 0); Console.WriteLine(MB0 mb0); // 读 VW100V 区对应 DB1 short vw100 client.ReadInt16(Snap7Client.S7AreaDB, 1, 100); Console.WriteLine(VW100 vw100); // 读 VD200 float vd200 client.ReadReal(Snap7Client.S7AreaDB, 1, 200); Console.WriteLine(VD200 vd200.ToString(F3)); // 写 VW300 12345 client.WriteInt16(Snap7Client.S7AreaDB, 1, 300, 12345); Console.WriteLine(VW300 写入完成); // 写 VD304 3.14 client.WriteReal(Snap7Client.S7AreaDB, 1, 304, 3.14f); Console.WriteLine(VD304 写入完成); client.Disconnect(); } } }运行后你应该能在控制台看到 MB0 的值以及 VW100、VD200 的实时数据然后去 Micro/WIN SMART 的状态图表里看 VW300 和 VD304会发现已经被改成 12345 和 3.14。4.3 批量读取要一次性规划好如果你要读连续 20 个字节不要循环调用 20 次ReadByte那样通信效率很低。正确做法是一次ReadArea把 20 个字节读进数组然后自己在内存里按偏移解析。比如要读 VB100 到 VB119直接amount 20, start 100, wordLen S7WLByte一次拿回来。这样不仅快而且减少了对 PLC 通信资源的占用在轮询场景下尤其明显。4.4 Python ctypes 也可以直接调同一个 DLL很多小工具的研发是用 Python 写的ctypes 也能直接调 Snap7 DLL。里面最需要注意的是数组缓冲区、字节序处理和 32/64 位匹配。import ctypes import struct snap7 ctypes.CDLL(./snap7.dll) # 声明函数原型 snap7.Cli_Create.restype ctypes.c_void_p snap7.Cli_ConnectTo.argtypes [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int, ctypes.c_int] snap7.Cli_ConnectTo.restype ctypes.c_int snap7.Cli_ReadArea.argtypes [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_char_p] snap7.Cli_ReadArea.restype ctypes.c_int snap7.Cli_WriteArea.argtypes [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_char_p] snap7.Cli_WriteArea.restype ctypes.c_int client snap7.Cli_Create() ip b192.168.1.10 result snap7.Cli_ConnectTo(client, ip, 0, 0) if result ! 0: raise Exception(连接失败错误码 str(result)) S7AreaDB 0x84 S7WLReal 0x08 # 读 VD200 浮点数 buffer ctypes.create_string_buffer(4) result snap7.Cli_ReadArea(client, S7AreaDB, 1, 200, 1, S7WLReal, buffer) if result ! 0: raise Exception(读取失败错误码 str(result)) data buffer.raw data data[::-1] # 大端转小端 value struct.unpack(f, data)[0] print(VD200 , value) snap7.Cli_Disconnect(client) snap7.Cli_Destroy(client)Python 示例和 C# 的思路完全一致Region 对应区域DB 号固定为 1Start 是 V 区字节偏移WordLen 选 Real然后自己处理字节序。5. 实战踩坑连接失败、数据错乱和多线程并发5.1 连接失败先按这个顺序查我自己在项目里遇到过至少五种“连接失败”表现每种对应的原因都不同后来总结了一套排查顺序ping 不通IP 不在同一网段是最常见原因。S7-200 SMART 固定 IP 后要用网线直连或者交换机连接确认电脑网卡 IP 前三段一致。能 ping 通但 Snap7 连不上大概率是 TSAP 不对。显式用Cli_SetConnectionParams设置本地和远程 TSAP 都是 0x0200不要依赖自动默认值。第一次能连断开后立刻重连失败Snap7 客户端没有正确释放。确保每次创建后Dispose不要在高频重试里反复 new。Windows 防火墙拦截把 102 端口放行或者临时关闭防火墙验证。工控机上的杀毒软件也会拦截需要加白名单。PLC 通信资源占满把 Micro/WIN SMART 的在线监控、HMI 触摸屏的通信暂时停掉给上位机留出连接资源。5.2 读出来全是 255 或者 0先查区域和地址如果数据能读出来但明显不对比如 M 区读成了 V 区或者把位地址写成了字节地址读回来的内容自然就是乱的。M0.0 的 Start 是 0不是 1I0.3 的 Start 是 3不是 0.3。这种错误不会报异常但数据会让你排查很久。我习惯在代码里把区域、DB 号、起始地址做成配置项不要硬编码在业务逻辑里。原因是现场点位表经常会变配置化之后改一个 Excel 导入就行了不用重新编译。5.3 浮点数永远不对先想字节序一个 32 位浮点数 VD200PLC 里存的是大端字节序。如果你直接用BitConverter.ToSingle得到的结果通常是一个天文数字或者 NaN。我见过有人在这个坑里反复折腾了一天最后发现只是忘记Array.Reverse字节数组。5.4 多线程读写必须串行化上位机里有多个界面、多个任务时很容易做成“每个线程都创建自己的 Snap7 客户端”。但工厂现场你往往会发现多客户端同时连一个 S7-200 SMART会出现其中一个连接间歇性失败。更稳的做法是全局只维护一个客户端实例所有读写任务通过锁 queue 串行执行。这样既避免连接数问题也避免多个线程同时读一个共享缓冲区造成的数据错乱。private static object _plcLock new object(); public short SafeReadInt16(Snap7Client client, int area, int db, int start) { lock (_plcLock) { return client.ReadInt16(area, db, start); } }如果你确实需要给多个模块提供并行访问那就把“串行访问”放在队列服务里而不是让各个模块直接抢锁。否则某一个模块卡住整个 PLC 通信都会堵住。6. 从 Demo 到产品轮询策略、自动重连和性能优化6.1 轮询间隔别拍脑袋很多初学者把读取循环写成while(true)里Thread.Sleep(100)然后跑一天后连接就断了。S7-200 SMART 的通信能力有限100ms 的轮询压力太大而且数据显示出来的意义也不大。我一般把基础轮询间隔设在 300ms 到 1000ms 之间具体取决于数据刷新要求。如果有些报警信号要求 100ms 以内响应这不应该靠上位机轮询实现应该在 PLC 里做判断把状态结果发给上位机而不是让上位机每秒读 10 次。6.2 自动重连机制现场环境不是实验室交换机重启、网线松动、PLC 断电都可能导致连接断开。产品化的程序必须处理自动重连但重连的频率要控制好。我常用的策略是“指数退避”连续失败时按 1 秒、2 秒、4 秒、8 秒递增最大间隔 30 秒成功连接后恢复为 1 秒。这样既能在恢复后快速连上又不会在 PLC 停机时疯狂刷错误日志。6.3 用多包读取减少请求次数如果你要读 V 区 100 个连续的位别一个个读哪怕这 100 个位的业务意义不同。你可以一次性读 13 个字节100 位取整再在内存里按位偏移解析。Snap7 对连续数据的批量读取效率远高于单点读取这个优化对通信稳定性的提升非常明显。我曾经在一个采集项目里把原来的 200 个单点读取改成 5 个批量读取每秒请求数从 200 降到 5PLC 通信负载直接降了一个量级现场的偶发断连也再没出现过。6.4 最后分享一个调试小技巧调试阶段不要直接写成灰色界面先做一个简单的 WinForm 或控制台工具把连接状态、读取值、错误码都打印出来。很多问题的定位速度其实取决于你能不能快速看到“连接成功”还是“错误码 33554445”。Snap7 的错误码是 int 类型直接用搜索工具查一下对应含义比盲猜要快得多。另外PLC 程序里如果对某个 V 区要做 32 位浮点数运算一定要保证上位机和 PLC 的地址对齐。VW100 和 VW102 是两个相邻的字但如果你把 VD100 读出来它包含了 VW100 和 VW102 的内容你再去读 VW102 做别的用途两边就会打架。建议在 PLC 程序里给上位机划一块独立的 V 区比如 VD200 到 VD500 专门供通信使用不跟工艺逻辑的临时变量混在一起这样上位机和 PLC 双方都不容易出错。Snap7 这套方案我已经在多个 S7-200 SMART 项目里跑过从几十个点位的设备数据采集到几百个点位的产线看板稳定性都经得住现场考验。只要把地址模型、字节序、TSAP 这三个关键点吃透你很快也能自己接出第一套可用的数据读写程序。
返回列表