西门子 S7-1200/1500 是长三角产线保有量最大的 PLC,上位机直连它最省事的方式不是 OPC、也不是 Modbus,而是走原生的 S7 通信协议(ISO-on-TCP,102 端口):不用在博图里写任何通信程序、不用额外授权,一个 NuGet 包就能读写 DB 块。但"能连上"和"用得对"之间隔着一堆经典坑:优化块访问、PUT/GET 权限、地址偏移、字符串布局、点式读取的性能。这篇用 S7.NetPlus 把这些坑一次填平。
一、三条路线怎么选:S7 直连 / OPC UA / Modbus TCP
| 对比项 | S7 协议直连(S7.NetPlus) | OPC UA | Modbus TCP |
|---|---|---|---|
| PLC 侧配置 | 勾两个选项即可,不写通信程序 | 要配服务器/证书/接口 | 要写 Modbus_Server 程序块、规划保持寄存器 |
| 数据模型 | 按 DB 绝对地址/偏移访问,类型自管 | 自描述节点树,结构体原生支持 | 寄存器拼接,全靠点表约定 |
| 安全性 | 无加密,靠网络隔离 + PUT/GET 白名单 | 证书加密认证,可跨网 | 无安全机制 |
| 适用机型 | S7-300/400/1200/1500(非优化DB) | 1500 及新型号原生 | 全系列(占用程序资源) |
| 典型场景 | 单台/少数几台西门子 PLC 的产线级上位机 | 多品牌混线、厂级平台 | 跨品牌仪表/老设备统一轮询 |
▲ 经验法则:车间内网、PLC 全是西门子、点位集中在几个 DB 块——直接 S7 协议,开发量最小。多品牌、要跨网段或上云——走 OPC UA(见此前 OPC UA 一篇)。
二、报文长什么样:三层封套,理解了排错不慌
抓包时 Wireshark 里看到的 S7 通信是三层套起来的,S7.NetPlus 全部帮你封装,但出问题(连不上、读写报错)时要知道卡在哪层:
▲ 常见错误码对号入座:0x80/0x81 对象不存在(DB 号或偏移越界)、0x84 此类型不支持(优化块没关)、0x85 地址越界。看到 AckData 里的 ErrorClass/ErrorCode,比库抛出的英文异常更直接。
三、PLC 侧两项必改设置(90% 的"连不上"在这)
- 关闭"优化的块访问":在博图里右键要读的 DB 块 → 属性 → 特性 → 取消勾选"优化的块访问",然后重新下载。S7 协议走绝对地址(DB1.DBW0 这种),优化块没有固定偏移,开着优化读出来的就是 0x84 报错或全零。建议给上位机单独建通信 DB,全程关闭优化,工艺块保持优化;
- 允许 PUT/GET:PLC 属性 → 保护与安全 → 连接机制 → 勾选"允许来自远程伙伴(PLC、HMI、OPM…)的 PUT/GET 通信访问"。不勾,TCP 能连上但读写一律被 PLC 拒绝;
- IP 与上位机同网段、关闭或放通 PLC 侧防火墙的 102 端口;S7-1500 还需确认访问级别不是"完全保护(不允许读写)"。
四、地址映射表:DBX/DBB/DBW/DBD 一次记牢
| 地址写法 | 含义 | 长度/字节序 | C# 类型 |
|---|---|---|---|
| DB1.DBX0.0 | DB1 第 0 字节第 0 位(Bool) | 1 bit | bool |
| DB1.DBB1 | DB1 第 1 字节 | 1 字节 | byte |
| DB1.DBW2 | DB1 从第 2 字节起的字 | 2 字节,大端 | short / ushort |
| DB1.DBD4 | DB1 从第 4 字节起的双字 | 4 字节,大端 | int / uint / float |
| DB1.DBD8 | 64 位双字长 | 8 字节 | long / double |
| DB1.DBB12 | S7 字符串(如 String[10]) | 头部2字节:最大长度+实际长度,后跟字符 | [S7String(10)] string |
偏移 0~1
偏移 2~3
偏移 4~7
8=最大 9=实际
▲ 注意偏移不能重叠:DBW0 占 0、1,下一个字至少从 2 开始;Real 占 4 字节建议偶数对齐。博图 DB 块的"偏移量"列是最终裁判,C# 结构体字段顺序必须和它完全一致。
五、C# 连接与基础读写
NuGet 安装 S7.NetPlus(开源免费,支持 .NET Framework 4.6.1+ 与 .NET 6/8)。最小连接:
using S7.Net; using S7.Net.Types; var plc = new Plc(CpuType.S71500, "192.168.1.10", rack: 0, slot: 1); plc.Open(); if (plc.IsConnected) { short output = (short)plc.Read("DB1.DBW0"); // 读Int16 float temp = (float)plc.Read("DB1.DBD4"); // 读Real bool running = (bool)plc.Read("DB1.DBX20.0"); // 读位 plc.Write("DB1.DBW10", (short)1200); // 写Int16 } plc.Close();
生产代码不能这么裸用:Open 可能抛异常、连接随时可能断。包一层带重连和读失败计数的客户端:
public class S7Client : IDisposable { private readonly Plc _plc; private readonly object _gate = new(); private int _failStreak; public S7Client(string ip) => _plc = new Plc(CpuType.S71500, ip, 0, 1) { ReadTimeout = 2000, WriteTimeout = 2000 }; private bool EnsureOpen() { lock (_gate) { if (_plc.IsConnected) return true; try { _plc.Open(); _failStreak = 0; return true; } catch (Exception ex) { if (++_failStreak % 10 == 1) Log.Warning($"PLC连接失败#{_failStreak}:{ex.Message}"); return false; // 不向上抛,采集循环按坏点处理 } } } public bool TryRead<T>(string addr, out T value) { value = default; if (!EnsureOpen()) return false; try { value = (T)_plc.Read(addr); _failStreak = 0; return true; } catch (Exception ex) { Log.Warning($"读{addr}失败:{ex.Message}"); try { _plc.Close(); } catch { } // 强制下次重建连接 return false; } } }
▲ 采集循环对 TryRead 返回 false 的点打"坏质量"标记而不是停循环——和报警、趋势两篇的数据质量约定一致。
六、结构体映射 + 整块读取:一次往返拿全部点
点式 Read 每个地址一个请求包,100 个点就是 100 次网络往返(内网单次约 1~5ms,累计很可观)。正确做法是把同一扫描周期要用的点排进一个通信 DB,一次把整块读上来,再用 ReadClass 映射成结构体:
// C#侧:字段顺序、类型必须与博图非优化DB的偏移列严格一致 public class LineStatus { public short OutputCount; // DBW0 public short NgCount; // DBW2 public float Temperature; // DBD4 public float Pressure; // DBD8 [S7String(16)] public string ProductCode; // DBB12:S7 String[16],共占18字节 } // 一次请求读整块(长度按DB实际大小,PDU不足时库自动分包重组) var status = plc.ReadClass<LineStatus>(db: 1, startByteAddress: 0); Log.Information($"产量={status.OutputCount} 温度={status.Temperature:F1} 型号={status.ProductCode}"); // 下发参数同样整块写:填好结构体一次 WriteClass,避免几十个点写一半的中间态 var param = new RecipeParam { TargetTemp = 85.5f, Speed = 1200, ProductCode = "A12-2609" }; plc.WriteClass(param, db: 2, startByteAddress: 0);
| 读法(120 个点,内网实测量级) | 网络往返 | 耗时量级 |
|---|---|---|
| 逐点 Read | 120 次请求/应答 | 200~600ms,周期很难压进 200ms |
| ReadBytes 整块 + 映射 | 1 次(PDU 内)或少数几次 | 3~15ms |
七、写控制必须握手:永远不要直接写 Q 点
上位机直接写 M 位或 Q 输出去启停设备,丢包、重发、重启都可能造成误动作。规范做法是命令 DB 里做三拍握手,执行权始终在 PLC:
| 拍 | 上位机 | PLC |
|---|---|---|
| 1 请求 | 写命令码 + 参数 + Req 计数 +1 | 主循环检测 Req≠Ack |
| 2 执行 | 等待 | 校验安全联锁条件 → 执行动作 → Ack=Req + 写结果码 |
| 3 确认 | 读到 Ack==Req 与结果码,清命令码 | 命令码被清零后回到空闲 |
public async Task<bool> SendCommandAsync(ushort code, Dictionary<string,object> p, CancellationToken ct) { ushort req = (ushort)((ushort)_plc.Read("DB2.DBW30") + 1); // 读旧Req,+1 WriteParamsBlock(p); _plc.Write("DB2.DBW28", (ushort)code); _plc.Write("DB2.DBW30", req); // Req最后写,PLC看到即新命令 var deadline = DateTime.Now.AddSeconds(5); while (DateTime.Now < deadline) { ushort ack = (ushort)_plc.Read("DB2.DBW32"); if (ack == req) { short result = (short)_plc.Read("DB2.DBW34"); _plc.Write("DB2.DBW28", (ushort)0); // 清命令码 return result == 0; // 0=PLC侧执行成功 } await Task.Delay(100, ct); } return false; // 超时:PLC联锁条件不满足或未执行,绝不能重发写Q点式操作 }
八、现场高频坑清单
- 读出来全是 0 或 0x84:90% 是优化块访问没关、或改了 DB 没重新下载;
- TCP 通但 Open 卡住/被拒:槽位号不对(1200/1500 用 1)、PUT/GET 没勾;
- 数值差 256 倍/正负翻转:字节序或偏移错,对照博图偏移列逐字段核对;
- 字符串乱码或错位:S7 String 头两字节不能跳过,用 [S7String(长度)] 映射;WString(宽字符)和 String 是两回事,别混用;
- 多上位机/HMI 同时连:S7-1200 连接资源有限(个位数),同网段多客户端要走边缘网关收拢成一条连接,别每个程序各连各的;
- 轮询周期不要低于 PLC 循环周期:比 OB1 还快只会拿到重复值;200ms 是多数产线的合理起点;
- 不要在 UI 线程同步读写:网络抖动会让界面假死,采集放后台线程,值通过线程封送更新界面。
九、验收检查
- 博图在线监控与上位机界面对拍:每个字段给已知值,逐一比对数值与字符串,重点验 Int16、Real、String 三种类型;
- 拔网线 10 秒:TryRead 全部返回坏点、界面不乱跳;插回后 2 秒内自动恢复,无异常弹窗;
- 命令握手测试:在 PLC 联锁条件不满足时发启动命令,验证 5 秒超时返回失败且 PLC 不动作;连续发 20 条命令验证 Req/Ack 不丢不重;
- 压测:120 点整块读,200ms 周期连续跑 24 小时,CPU 占用、失败计数、连接数稳定;
- 断电演练:上位机与 PLC 分别重启,验证命令计数能重新对齐、无复位后误动作。
S7 直连方案的哲学是让 PLC 干控制,让上位机干数据:非优化通信 DB 做契约、整块读写做效率、计数握手做安全、坏质量标记做鲁棒。这套组合在单品牌西门子产线上,比 OPC 配置轻、比 Modbus 不用写 PLC 程序,是投入产出比最高的接法——等将来多品牌混线或要上云时,再在边缘把同一批数据翻译成 OPC UA,业务层一行都不用改。
