简介:C#与西门子S7系列PLC通讯示例源码,面向工业自动化领域开发者,帮助实现远程监控、数据采集与设备控制。包内共76个文件,包含C#源码、可执行程序、项目配置、文本说明及资源文件等,压缩包仅859KB,目录结构清晰,便于对照学习。目前已有388人学习。示例覆盖SIMATIC.NET库与Libnodave两种通信方案,详细演示TCP/IP连接(默认端口102)、数据类型转换、读写PLC变量、异常处理等关键环节;提供MainForm界面、类图及编译好的exe,读者可快速运行验证并在此基础上扩展维护。适合希望上手S7 PLC通信的C#开发人员。
1. C# 对接西门子 S7 系列 PLC,动手前先分清能做什么
一台没人看守的 S7-1200 站在车间里,WinCC 组态界面只能看、不能改,产线上的临时配方还得让技术员跑上去用触摸屏点。真正要解决的往往不是“能不能连”,而是“怎么在自己的 C# 上位机里稳定地把数据拿下来、再写回去”。这个标题的落点就在这里:用 C# 写一个能跑在工控机或普通 PC 上的上位机程序,通过以太网与西门子 S7 系列 PLC(S7-200 SMART、S7-300/400、S7-1200/1500)交换 M 区、I/Q 区、DB 块里的数据,替代触摸屏或 WinCC 做定制化界面、数据采集和配方下发。
适合读这篇文章的人有两类:一类是 C# 工程师,熟悉语法但不清楚 S7 协议的地址体系、字节序和连接限制,容易在 PLC 那头栽跟头;另一类是 PLC 工程师,梯形图写得顺手,但想把数据交给自己写的 C# 工具,需要先把“S7 协议里地址怎么写”这个隔阂补上。这篇文章会把选型、连接、读写、地址映射和采集调度一次性串起来,示例代码按可直接修改的方向给,读完能明确知道自己的场景该用哪套方案。
2. S7 协议选型与 C# 通信库的边界
2.1 S7 协议和 Modbus TCP 在数据访问上的差异
S7 协议是西门子私有协议,TCP 固定走 102 端口,和 Modbus TCP 的 502 端口是两套体系。Modbus TCP 的优势在于它把 PLC 内部的数据区抽象成 0x/1x/3x/4x 这样的寄存器空间,任何语言只要拼对 MBAP 头和功能码就能读写;而 S7 协议直接面向 CPU 的存储区,访问时要给出明确的“区域 + 地址 + 数据类型”,比如M10.0、DB1.DBX0.0、DB1.DBD4。这也解释了一个常见现象:用 Modbus 连 S7-1200 需要先在 PLC 侧组态一个 Modbus TCP 从站,把 DB 块映射到保持寄存器上,而用 S7 协议连接时基本不需要额外组态,只要 CPU 的 PUT/GET 通信功能是开启的。
对 C# 开发者来说,差异直接影响代码组织。用 S7 协议时,你写的代码像在直接“点名”PLC 的内存地址,例如读取 DB1 的第 0 个字节,指令读的是DB1.DBB0;而用 Modbus TCP 时,你得先关心“DB1 被映射到了 4x 区的哪个偏移”。如果你的设备多、品牌杂,Modbus TCP 确实更通用;但如果你面对的是一整条西门子产线,设备型号从 S7-200 SMART 到 S7-1500 都有,直接用 S7 协议省略了 PLC 侧组态映射这一步,交付和维护都少一个环节。
2.2 三个 C# 通信库的适用边界
市面上的 C# 库不会只有一个,选型时主要看三件事:授权方式、支持的 PLC 型号范围、以及地址语法是否贴近西门子原生表达。下表是我常用的选型依据。
| 库 | 支持的 PLC 范围 | 授权与获取 | 地址语法特点 | 适用场景 |
|---|---|---|---|---|
| S7-200 SMART 官方库(S7_200_SMART.dll) | S7-200 SMART | 免费,从西门子官网下载 DLL | M0.0、V100.0,与 SMART 编程软件一致 | 老设备维护、最小化读写 |
| HslCommunication | S7-200 SMART / S7-1200 / S7-1500 等 | 免费,NuGet 直接安装 | M100、DB1.DBD0,同一套 API 兼容多种 PLC | 新项目首选,跨品牌统一 |
| Sharp7 | S7-200 / S7-300 / S7-400 / S7-1200 / S7-1500 | 开源,GitHub 获取 | DB1,DBD0风格,底层封装薄 | 老 S7-300/400 备件少、需离线包 |
选库不只是看功能列表,还要看地址体系是否和你手里的 PLC 一致。比如 S7-200 SMART 官方库把 V 区作为数据区入口,而 HslCommunication 在连 S7-1200/1500 时更习惯直接写DB1.DBD0。如果你拿 S7-200 SMART 的示例代码去连 S7-1500,大概率会卡在“地址找不到”这一层。
2.3 动手前的 PLC 侧检查清单
代码写得再正确,PLC 侧设置不对也连不上。常见的准备工作有三项,顺序不能乱:
- CPU 的 IP 地址固定,且和上位机在同一个网段。很多现场失败是因为 PLC 用的是 DHCP,重启后 IP 变了。
- S7-1200/1500 需要在 TIA Portal 的“保护与安全”里勾选“允许从远程伙伴(PUT/GET 通信访问)”。S7-300/400 则要在硬件组态里勾选 “Enable clock” 旁边的通信选项,不同 GSD 版本菜单位置略有差异。
- 关闭电脑防火墙,或放行 TCP 102 端口。Windows 防火墙默认会拦掉工控软件的自建监听,这一条排查效率最高。
提示:先用
ping PLC的IP确认链路通,再用Test-NetConnection PLC的IP -Port 102确认 102 端口能访问,这两步过了再谈代码。
3. S7-200 SMART 的最小连接与读取示例源码
3.1 引入 S7-200 SMART 库与客户端实例
S7-200 SMART 是维修现场最常见的型号,西门子官方提供了S7_200_SMART.dll,它不依赖 TIA Portal 环境,直接在 Visual Studio 里引用即可。这个库没有上 NuGet,需要从西门子官网下载压缩包,解压后把 DLL 拖进项目引用。引用时注意目标框架,老 DLL 在 .NET 6 以上版本可能不兼容,建议用 .NET Framework 4.8 或 .NET Core 3.1 承载采集服务。
用这个库连 PLC 的套路非常固定:先 new 一个 PLC 对象,设置 IP,调 Connect,然后 Read。示例代码如下:
using System; using S7_200_SMART; class S7Demo { static void Main(string[] args) { PLC plc = new PLC(); plc.IP = "192.168.2.1"; // PLC 的固定 IP int ret = plc.Connect(); // 返回 0 表示连接成功 if (ret == 0) { Console.WriteLine("连接成功"); // 读取 M0.0 开始的 1 个字节,结果放入 buffer byte[] buffer = new byte[1]; int readRet = plc.Read("M0.0", 1, out buffer); if (readRet == 0) { // buffer[0] 的位 0 对应 M0.0,位 1 对应 M0.1 bool m00 = (buffer[0] & 0x01) == 0x01; Console.WriteLine("M0.0 = " + m00); } plc.Close(); } else { Console.WriteLine("连接失败,错误码:" + ret); } } }这段代码的核心逻辑只有三步:设置plc.IP指向 PLC 的 IP 地址;调用Connect()建立 TCP 连接,返回值非 0 时按错误码查表;用Read("M0.0", 1, out buffer)读取以 M0.0 为起点的一个字节。注意官方库的Read是按“字节数”读的,size参数是 1 时只取 1 个字节,如果你要读连续的 10 个字节,就要把 size 改成 10,同时把 buffer 容量也改成 10。很多初学者在这里只把 buffer 长度改了、size 忘了改,结果读回来的数据永远是第一个字节。
3.2 连接 PLC 并读取 M 区的最小代码
S7-200 SMART 的地址体系里,M 区是位存储区,它的每个位既可以用M0.0这种位地址表示,也可以按字节用MB0、按字用MW0、按双字用MD0表示。用官方库读位时,读回来的是一个字节,要自己用位运算取出目标位,这在实际项目中反而是好事,因为一次读一个字节就能拿到 8 个位的状态,比逐位读取少占用通信时间。
下面这段代码演示了读取 MW0 和 MD2 的值,也就是直接读出 16 位整数和 32 位浮点数:
byte[] bufWord = new byte[2]; plc.Read("MW0", 2, out bufWord); short mw0 = (short)((bufWord[0] << 8) | bufWord[1]); byte[] bufDWord = new byte[4]; plc.Read("MD2", 4, out bufDWord); float md2 = BitConverter.ToSingle( new byte[] { bufDWord[3], bufDWord[2], bufDWord[1], bufDWord[0] }, 0);这里必须解释两个关键点。第一,西门子 S7 协议里多字节数据是“高字节在前”的,MW0的bufWord[0]是高 8 位,和 C# 里常见的BitConverter.ToInt16低字节序正好相反,所以读回后要手动拼一次。第二,浮点数MD2的字节序处理同理,BitConverter.ToSingle要求传入的字节数组是低字节序,所以要先把从 PLC 读回来的四个字节反转再转换,否则数值会变成一个完全不相关的浮点数。这个字节序问题在后续用 HslCommunication 时也会遇到,但那个库内部已经处理好了。
3.3 断线重连与超时参数怎么调
S7-200 SMART 官方库的Connect()没有显式超时参数,连接超时时间由 Windows Socket 的默认值决定,一般在 20 秒左右。现场设备重启时,这个等待时间会让上位机界面像死了一样。解决办法是不要在 UI 线程里直接调 Connect,而是放到后台线程,或者用Task.Run包一层,配合CancellationTokenSource做手动取消。
断线重连是另一个常见需求。PLC 断电、网线松动、CPU 停机都会导致连接断开,判断方式有两种:调用plc.Connected属性看连接状态,或者每次读写返回非 0 错误码时执行一次重连。我一般会维护一个简单的重连状态机:
if (ret != 0 || !plc.Connected) { plc.Close(); Thread.Sleep(2000); ret = plc.Connect(); if (ret == 0) { // 重连成功,可以重新订阅数据 } }重连间隔不能太短,1 秒以下的重试会让 PLC 的通信负载居高不下,尤其当多台上位机同时连一个 CPU 时,频繁的 Connect/Close 会拉高 CPU 的通信中断率。2000 毫秒是比较稳妥的起点,连续失败 5 次后可以指数退避,最长到 10 秒。
4. S7 地址映射与数据类型转换的关键细节
4.1 西门子 S7 存储区地址在 C# 中的写法
S7 协议把数据区分为若干独立的存储区域,不同型号的 PLC 支持的区域略有差异。下面是 S7-1200/1500 与 S7-200 SMART 在地址表达上的对照表,这也是 C# 程序员最需要背下来的一张表。
| 存储区 | S7-1200/1500 地址示例 | S7-200 SMART 地址示例 | 功能 |
|---|---|---|---|
| 输入映像区 | I0.0、IB0、IW0 | I0.0、IB0 | 读取输入端信号 |
| 输出映像区 | Q0.0、QB0、QW0 | Q0.0、QB0 | 写输出端信号 |
| 位存储区 | M0.0、MB0、MW0 | M0.0、MB0、MW0 | 存放中间变量 |
| 数据块 | DB1.DBX0.0、DB1.DBB0 | V 区(VB0等) | 存放全局数据 |
S7-1200/1500 的 DB 块是核心数据区,地址写法固定为DB块号.数据类型+偏移,比如DB1.DBX0.0表示 DB1 的第 0 字节的第 0 个位,DB1.DBD4表示 DB1 偏移 4 字节开始的 32 位浮点数。而 S7-200 SMART 没有命名 DB 块,数据统一放在 V 存储区,官方库把 V 区地址直接映射为VB0、VW10等。
这里有一个容易踩的坑:S7-300/400 的 DB 块是“背景 DB”和“全局 DB”分开管理的,地址写DB10.DBX0.0时,10 是 DB 块号;而 S7-1200/1500 里既有全局 DB 也有与 FB 关联的背景 DB,背景 DB 如果被优化访问了,S7 协议默认无法访问,必须在 DB 属性里取消“优化的块访问”,或者用符号寻址的方式生成 S7-1200 的访问路径。上位机开发时如果发现读 DB 块返回“对象不存在”,先检查这个属性。
4.2 Byte、Word、DWord 的字节序处理
S7 协议的多字节数据统一采用大端序(Big-Endian),即高位字节存在低地址。C# 原生类型默认是小端序,直接转换前必须做字节反转。下面是常见数据类型的换算规则。
| PLC 数据类型 | 地址示例 | 数据宽度 | C# 端转换方式 |
|---|---|---|---|
| Bool | M0.0 | 1 位 | 取字节后做位掩码 |
| Byte | MB0 | 8 位 | 直接取 byte |
| Int / Word | MW10 | 16 位 | (short)((b0 << 8) | b1) |
| DInt / DWord | MD20 | 32 位 | 反转后BitConverter.ToInt32 |
| Real | MD24 | 32 位 | 反转后BitConverter.ToSingle |
| String | DB1.DBB10 | 可变 | 读字节数组后按 ASCII/UTF8 解码 |
字节反转别写死在业务代码里,封装成工具函数更方便。下面这个辅助类覆盖了最常见的几种转换:
public static class S7Convert { public static short ToInt16(byte[] data, int start) { return (short)((data[start] << 8) | data[start + 1]); } public static float ToFloat(byte[] data, int start) { byte[] bytes = new byte[4]; Array.Copy(data, start, bytes, 0, 4); Array.Reverse(bytes); return BitConverter.ToSingle(bytes, 0); } public static byte[] FromInt16(short value) { byte[] bytes = BitConverter.GetBytes(value); Array.Reverse(bytes); return bytes; } public static byte[] FromFloat(float value) { byte[] bytes = BitConverter.GetBytes(value); Array.Reverse(bytes); return bytes; } }写操作时同样要反转,FromInt16和FromFloat就是为写回 PLC 准备的。每次读写都走这个工具类,能避免因为字节序问题在调试上浪费时间。还有一个细节:S7 的 String 类型第一个字节存储最大长度、第二个字节存储实际长度,从第三个字节开始才是字符内容,直接取DB1.DBB10偏移后的字节数组时,要跳过前两个字节再转字符串。
4.3 用 ReadBytes 一次取大块数据
上位机做数据采集时,最常见的性能瓶颈是“一条一条读”。比如要读 DB1 里连续的 50 个浮点数,如果用ReadFloat("DB1.DBD0")读 50 次,每次都要发起一次请求-响应往返,累计延迟可能到几百毫秒。正确做法是一次性把整块数据读回来,然后在内存里解析。
HslCommunication 的Read方法支持直接按字节数读取,写起来很简洁:
using HslCommunication; using HslCommunication.Profinet.Siemens; SiemensS7Net s7 = new SiemensS7Net(SiemensPLCS.S1500, "192.168.0.1"); s7.SetLogin("admin", "123456"); // 部分固件需要登录,没有授权可省略 OperateResult<byte[]> read = s7.Read("DB1.DBB0", 200); if (read.IsSuccess) { // 从 read.Content 中按偏移解析数据 float temp = S7Convert.ToFloat(read.Content, 0); float pressure = S7Convert.ToFloat(read.Content, 4); }Read("DB1.DBB0", 200)中的第二个参数 200 表示要读 200 个字节,一次请求就能覆盖 DB1 从偏移 0 到 199 的区域。读取时要保证 PLC 侧这 200 个字节里没有超过 DB1 的实际长度,否则会报越界错误。更稳妥的做法是在 DB1 里单独规划一个连续的“采集区”,把要上传的数据集中在里面,上位机只读这个区域。
5. 进阶:循环采集与 UI 解耦,以及 S7-1200/1500 的兼容写法
5.1 用 async/await 解决 C# 循环数据采集和 UI 刷新卡顿
C# 上位机做循环采集时最典型的问题就是界面卡顿。原因几乎都一样:采集循环和 UI 刷新跑在同一个线程里,Read是同步阻塞方法,一次读超时 3 秒,界面就冻结 3 秒。常见的做法是把循环放到Task.Run里,采集结果通过Progress<T>报告给 UI 线程。下面是一个最小结构:
private async Task StartPollingAsync(SiemensS7Net s7, CancellationToken ct) { var progress = new Progress<(float temp, float pressure)>(value => { labelTemp.Text = value.temp.ToString("F2"); labelPressure.Text = value.pressure.ToString("F2"); }); await Task.Run(async () => { while (!ct.IsCancellationRequested) { OperateResult<float> r1 = s7.ReadFloat("DB1.DBD0"); OperateResult<float> r2 = s7.ReadFloat("DB1.DBD4"); if (r1.IsSuccess && r2.IsSuccess) { ((IProgress<(float, float)>)progress).Report((r1.Content, r2.Content)); } await Task.Delay(100, ct); // 100ms 采集周期 } }, ct); }采集周期用Task.Delay(100, ct)而不是Thread.Sleep,这样才能在点击“停止”按钮时通过CancellationToken立刻退出循环。UI 更新频率建议低于采集频率,比如采集 100ms、刷新界面 300ms,否则界面刷新开销反而会拖累采集节奏。可以把上次刷新时间记下来,和当前时间做差值判断。
5.2 用 HslCommunication 连 S7-1200/1500 的读写写法
如果你的现场是 S7-1200/1500,直接用 HslCommunication 的SiemensS7Net类比官方库更省事,因为它解决了地址语法和字节序问题。构造时第一个参数是 PLC 型号,第二个是 IP 地址:
SiemensS7Net s7 = new SiemensS7Net(SiemensPLCS.S1200, "192.168.0.1"); s7.ConnectTimeOut = 2000; // 连接超时 2 秒 OperateResult<bool> rBit = s7.ReadBool("M0.0"); // 读位 OperateResult<float> rReal = s7.ReadFloat("DB1.DBD0"); // 读浮点 OperateResult<short> rInt = s7.ReadInt16("DB1.DBW2"); // 读整数 s7.Write("M0.0", true); // 写位 s7.Write("DB1.DBD0", 25.5f); // 写浮点和 S7-200 SMART 官方库最大的区别是地址里带了类型后缀:DBW表示 DB 块里的字,DBD表示双字,DBB表示字节。写DB1.DBD0时,库内部把它解析为“DB1 块偏移 0 字节开始的 32 位浮点”。这个地址表达方式和 TIA Portal 里的绝对寻址完全一致,调试时对着 PLC 程序看偏移即可,不用再做二次映射。
5.3 连接失败时先查这三个位置
最后给出一个排错顺序,遇到连接失败按这个顺序往下查比盲试快得多。第一,确认 PLC 的 IP 没有冲突,很多现场一台编程电脑用了静态 IP,和 PLC 冲突后上位机就间歇性连不上;第二,查 102 端口通不通,Test-NetConnection命令能直接给出结果,如果不通,问题九成在防火墙或交换机 VLAN 配置上;第三,确认 S7-1200/1500 的 PUT/GET 通信确实勾选了,并且 CPU 处于 RUN 或 STOP 状态都能被访问,有些固件版本默认禁止从外部读写优化 DB。这三个位置排查完,绝大多数连接问题都能暴露出来。
本文还有配套的精品资源,点击获取