西门子 PLC 与 C# 通信
本教程基于项目中的
SiemensPlcConnector模块,从零讲解如何用 C# 连接西门子 PLC 并读写数据。
目录
- 先了解几个概念
- 环境准备
- 项目结构
- [第一步:连接 PLC](#第一步:连接 PLC)
- 第二步:读取数据
- 第三步:写入数据
- [S7 地址格式速查](#S7 地址格式速查)
- 核心代码逐段讲解
- 完整示例程序
- 常见问题排查
1. 先了解几个概念
什么是 PLC?
PLC(可编程逻辑控制器)是工业现场常用的控制设备,负责采集传感器信号、控制电机阀门等。
什么是 S7 协议?
西门子 S7 系列 PLC(如 S7-1200、S7-1500)通过 S7 通信协议 与上位机(你的 C# 程序)交换数据,底层走 TCP/IP 网络。
整体通信流程
┌─────────────┐ TCP/IP (端口 102) ┌─────────────┐
│ C# 程序 │ ◄──────────────────────► │ 西门子 PLC │
│ (上位机) │ S7 协议读写 DB/M/I/Q │ (控制器) │
└─────────────┘ └─────────────┘
本模块做了什么?
| 层级 | 说明 |
|---|---|
| S7.Net 库 | 开源第三方库,负责底层 S7 协议报文 |
| SiemensPlcConnector | 我们封装的通用类,提供连接、读写、重试、释放 |
| PlcAddressHelper | 地址拼接工具,减少手写地址出错 |
| Program.cs | 你的业务代码,调用上述类完成具体功能 |
2. 环境准备
2.1 软件要求
- .NET 8 SDK(或更高版本)
- Visual Studio 2022 / VS Code / Rider 任一 IDE
- (可选)TIA Portal ------ 用于查看 PLC 程序与 DB 块地址
2.2 PLC 侧设置(TIA Portal)
在 PLC 设备属性中,必须勾选:
保护与安全 → 连接机制 → 允许来自远程对象的 PUT/GET 通信访问
否则 C# 程序无法读写数据。
2.3 网络要求
- PC 与 PLC 在同一网段(例如 PC
192.168.0.10,PLC192.168.0.1) - 能
ping通 PLC 的 IP 地址 - 防火墙允许 TCP 102 端口
2.4 安装依赖
项目已通过 NuGet 引用 S7netplus,首次编译时自动下载:
powershell
cd SiemensPlcConnector
dotnet restore
dotnet build
3. 项目结构
SiemensPlcConnector/
├── SiemensPlcConnector.csproj # 项目配置与 NuGet 依赖
├── SiemensPlcConnector.cs # 核心连接类(重点学习)
├── PlcAddressHelper.cs # 地址拼接辅助类
├── Program.cs # 运行示例
└── README.md # 本教程
4. 第一步:连接 PLC
4.1 最简连接代码
csharp
using S7.Net;
using Siemens.S7Connector;
// using var:程序结束时自动断开连接并释放资源
using var plc = new SiemensPlcConnector(
ipAddress: "192.168.0.1", // PLC 的 IP 地址,改成你的实际 IP
cpuType: CpuType.S71200, // CPU 型号,必须与 PLC 一致
rack: 0, // 机架号,S7-1200/1500 固定为 0
slot: 1); // 槽号,S7-1200/1500 固定为 1
// 发起连接
if (plc.Connect())
{
Console.WriteLine("连接成功!");
}
else
{
Console.WriteLine("连接失败,请检查 IP 和网络。");
}
4.2 逐行说明
| 代码 | 含义 |
|---|---|
using S7.Net |
引用第三方 S7 协议库 |
using Siemens.S7Connector |
引用本项目封装的连接器 |
using var plc = ... |
创建连接器;离开作用域时自动调用 Dispose() 断开连接 |
ipAddress |
PLC 在局域网中的 IP |
CpuType.S71200 |
告诉库这是 S7-1200 CPU,不同型号握手参数不同 |
rack: 0, slot: 1 |
S7-1200/1500 的标准参数;S7-300/400 通常 slot: 2 |
plc.Connect() |
建立 TCP 连接并完成 S7 握手,成功返回 true |
4.3 不同 CPU 的参数对照
| PLC 型号 | CpuType | Rack | Slot |
|---|---|---|---|
| S7-1200 | CpuType.S71200 |
0 | 1 |
| S7-1500 | CpuType.S71500 |
0 | 1 |
| S7-300 | CpuType.S7300 |
0 | 2 |
| S7-400 | CpuType.S7400 |
0 | 2 |
5. 第二步:读取数据
连接成功后,用地址字符串指定要读的位置。
5.1 常用读取方法
csharp
// 读取 BOOL(开关量),地址格式:DB块.字节.位
bool isRunning = plc.ReadBool("DB1.DBX0.0");
// 读取 INT(16 位整数),占 2 字节
short speed = plc.ReadInt("DB1.DBW2");
// 读取 REAL(32 位浮点数),占 4 字节
float temperature = plc.ReadReal("DB1.DBD4");
// 读取 DINT(32 位整数)
int counter = plc.ReadDInt("DB1.DBD8");
// 通用泛型读取(上面方法的底层实现)
int value = plc.Read<int>("DB1.DBD8");
拓展------代码示例
csharp
5.2 地址与数据类型对应关系
假设 PLC 中 DB1 的布局如下:
DB1 偏移量 变量名 类型 C# 读取方法
─────────────────────────────────────────────────
0.0 Running BOOL ReadBool("DB1.DBX0.0")
0.1 Alarm BOOL ReadBool("DB1.DBX0.1")
2 Speed INT ReadInt("DB1.DBW2")
4 Temperature REAL ReadReal("DB1.DBD4")
8 Counter DINT ReadDInt("DB1.DBD8")
重要:地址必须与 TIA Portal 中 DB 块的实际偏移一致,否则读到的是错误数据。
5.3 用辅助类拼接地址(推荐)
手写地址容易出错,可以用 PlcAddressHelper:
csharp
// 等价于 "DB1.DBX0.0"
string addr = PlcAddressHelper.DbBool(dbNumber: 1, byteOffset: 0, bitOffset: 0);
bool running = plc.ReadBool(addr);
// 等价于 "DB1.DBW2"
string speedAddr = PlcAddressHelper.DbInt(1, 2);
short speed = plc.ReadInt(speedAddr);
6. 第三步:写入数据
写入语法与读取类似,第二个参数是要写入的值。
csharp
// 写入 BOOL:把 DB1 第 0 字节的第 1 位设为 true
plc.Write("DB1.DBX0.1", true);
// 写入 INT:注意 C# 中 INT 对应 short 类型,需要强制转换
plc.Write("DB1.DBW2", (short)1500);
// 写入 REAL(float)
plc.Write("DB1.DBD4", 36.5f);
// 写入 DINT(int)
plc.Write("DB1.DBD8", 99999);
写入时的类型注意
| PLC 类型 | C# 写入类型 | 示例 |
|---|---|---|
| BOOL | bool |
true / false |
| INT | short |
(short)1500 |
| DINT | int |
99999 |
| REAL | float |
36.5f |
7. S7 地址格式速查
7.1 地址组成规则
DB1.DBX0.0
│ │ │ │
│ │ │ └── 位号(0~7),仅 BOOL 使用
│ │ └──── 字节偏移
│ └─────── 寻址方式:X=位, B=字节, W=字(2字节), D=双字(4字节)
└─────────── DB 块编号
7.2 常用寻址前缀
| 前缀 | 含义 | 示例 | 数据大小 |
|---|---|---|---|
DBX |
位(Bool) | DB1.DBX0.0 |
1 bit |
DBB |
字节 | DB1.DBB10 |
1 byte |
DBW |
字(Int) | DB1.DBW0 |
2 bytes |
DBD |
双字(DInt/Real) | DB1.DBD4 |
4 bytes |
M |
标志位 | M0.0 |
1 bit |
I |
输入 | I0.0 |
1 bit |
Q |
输出 | Q0.0 |
1 bit |
7.3 字节对齐提醒
偏移 0: BOOL (占 1 字节内的若干位)
偏移 2: INT (必须从偶数偏移开始,占 2 字节) → 偏移 0~1 是 BOOL,INT 从 2 开始
偏移 4: REAL (占 4 字节)
BOOL 虽然只占 1 个 bit,但在 DB 块中通常按字节规划,后面的 INT/REAL 仍需遵守对齐规则。
8. 核心代码逐段讲解
以下摘自 SiemensPlcConnector.cs,按功能块讲解,帮助理解封装思路。
8.1 类的基本结构
csharp
public sealed class SiemensPlcConnector : IDisposable
{
private Plc? _plc; // S7.Net 底层连接对象
private bool _isConnected; // 连接状态标志
private bool _disposed; // 是否已释放资源
public string IpAddress { get; } // 只读属性:IP 地址
public short Rack { get; } // 只读属性:机架号
public short Slot { get; } // 只读属性:槽号
public CpuType CpuType { get; } // 只读属性:CPU 型号
}
要点:
IDisposable:实现"用完即释放"模式,using var语法依赖它private字段:外部不能直接操作底层_plc,必须通过公开方法{ get; }只读属性:连接参数创建后不允许修改
8.2 构造函数 ------ 保存连接参数
csharp
public SiemensPlcConnector(string ipAddress, CpuType cpuType = CpuType.S71200,
short rack = 0, short slot = 1)
{
// 防御性编程:拒绝空 IP
if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentException("IP 地址不能为空。", nameof(ipAddress));
IpAddress = ipAddress.Trim(); // Trim() 去掉首尾空格
CpuType = cpuType;
Rack = rack;
Slot = slot;
}
要点:
- 构造函数只保存参数,不立即连接(延迟连接,用时再连)
cpuType = CpuType.S71200是默认参数,不传时默认 S7-1200throw抛出异常,让调用者知道参数不合法
8.3 Connect() ------ 建立连接
csharp
public bool Connect()
{
if (IsConnected) return true; // 已连接则跳过
try
{
_plc = new Plc(CpuType, IpAddress, Rack, Slot); // 创建底层对象
_plc.Open(); // 发起 TCP + S7 握手
_isConnected = _plc.IsConnected;
return _isConnected;
}
catch (Exception)
{
_isConnected = false; // 失败时清理状态
return false; // 返回 false 而不是抛异常,便于调用者判断
}
}
初学者要点:
- 返回
bool而不是抛异常:连接失败是常见情况,调用者可以用if判断 try/catch:网络问题不会导致程序崩溃
8.4 Read<T>() ------ 泛型读取
csharp
public T Read<T>(string address)
{
EnsureConnected(); // 读之前先确保已连接
return ExecuteWithRetry(() => // 包一层重试逻辑
{
var value = _plc!.Read(address); // S7.Net 读取原始值
if (value is T typedValue) // 类型已匹配则直接返回
return typedValue;
return (T)Convert.ChangeType(value, typeof(T))!; // 否则强制转换
});
}
初学者要点:
<T>是泛型:同一个方法可以返回bool、int、float等不同类型EnsureConnected():若未连接会自动尝试Connect()ExecuteWithRetry:网络抖动时自动重连并重试,提高稳定性_plc!中的!是 null forgiving 运算符,告诉编译器"此处不为 null"
8.5 ExecuteWithRetry ------ 失败重试机制
csharp
private T ExecuteWithRetry<T>(Func<T> action)
{
Exception? lastException = null;
for (int attempt = 0; attempt <= MaxRetryCount; attempt++)
{
try
{
return action(); // 执行传入的读写操作
}
catch (Exception ex)
{
lastException = ex;
_isConnected = false;
if (attempt >= MaxRetryCount) break;
Disconnect(); // 断开旧连接
Connect(); // 重新连接
}
}
throw lastException ?? new InvalidOperationException("PLC 操作失败。");
}
初学者要点:
Func<T>:传入一个"无参、返回 T"的委托(lambda 表达式)- 默认最多重试 3 次(
MaxRetryCount = 3) - 每次失败后先断开再重连,避免 stale 连接
8.6 Dispose() ------ 释放资源
csharp
public void Dispose()
{
if (_disposed) return; // 防止重复释放
Disconnect(); // 断开 PLC 连接
_plc = null; // 释放引用
_disposed = true;
GC.SuppressFinalize(this);
}
初学者要点:
- 配合
using var plc = ...使用:代码块结束时自动调用Dispose() - 工业程序必须正确释放连接,否则 PLC 连接数耗尽
9. 完整示例程序
以下代码可直接替换 Program.cs 后运行(修改 IP 和地址):
csharp
using S7.Net;
using Siemens.S7Connector;
// ── 1. 创建连接器 ──
using var plc = new SiemensPlcConnector(
ipAddress: "192.168.0.1", // ← 改成你的 PLC IP
cpuType: CpuType.S71200,
rack: 0,
slot: 1);
// ── 2. 连接 ──
if (!plc.Connect())
{
Console.WriteLine("❌ 连接失败!请检查:");
Console.WriteLine(" 1. IP 地址是否正确");
Console.WriteLine(" 2. PC 与 PLC 是否同网段");
Console.WriteLine(" 3. PLC 是否开启 PUT/GET 访问");
return;
}
Console.WriteLine("✅ PLC 连接成功");
try
{
// ── 3. 读取 ──
bool running = plc.ReadBool("DB1.DBX0.0");
short speed = plc.ReadInt("DB1.DBW2");
float temp = plc.ReadReal("DB1.DBD4");
Console.WriteLine($"运行状态: {running}");
Console.WriteLine($"速度: {speed}");
Console.WriteLine($"温度: {temp:F1} ℃");
// ── 4. 写入 ──
plc.Write("DB1.DBX0.1", true); // 置位报警确认
plc.Write("DB1.DBW6", (short)1200); // 写入目标速度
Console.WriteLine("✅ 写入完成");
}
catch (Exception ex)
{
Console.WriteLine($"❌ 读写异常: {ex.Message}");
}
// ── 5. using 结束,自动 Disconnect + Dispose ──
运行命令:
powershell
dotnet run --project SiemensPlcConnector
10. 常见问题排查
Q1:Connect() 返回 false
| 检查项 | 操作 |
|---|---|
| IP 是否正确 | 在 TIA Portal 或 PLC 面板查看实际 IP |
| 网络是否通 | ping 192.168.0.1 |
| CPU 型号 | CpuType 必须与 PLC 一致 |
| PUT/GET 权限 | TIA Portal → 设备属性 → 连接机制 → 勾选允许 |
| 防火墙 | 临时关闭防火墙测试 |
Q2:连接成功但读取报错
| 检查项 | 操作 |
|---|---|
| DB 块是否存在 | TIA Portal 中确认 DB1 已创建且未优化(或知道优化后的偏移) |
| 地址偏移 | 与 TIA Portal 中变量偏移完全一致 |
| 数据类型 | DBW 读 INT,DBD 读 REAL/DINT,不要混用 |
| DB 块权限 | DB 块属性中允许 HMI/上位机访问 |
Q3:读到的数值不对
- 检查字节序:S7 为大端序,S7.Net 已自动处理,一般无需关心
- 检查 BOOL 位号 :
DBX0.1是第 0 字节第 1 位,不是第 1 字节 - 检查 DB 优化:TIA Portal V13+ 默认"优化的块访问",地址不再从 0 连续排列,需在 DB 块属性中关闭优化,或查符号表偏移
Q4:程序退出后 PLC 连接未释放
确保使用 using var 或手动调用 plc.Dispose():
csharp
// ✅ 推荐:自动释放
using var plc = new SiemensPlcConnector("192.168.0.1");
plc.Connect();
// ... 业务代码 ...
// 离开作用域时自动 Disconnect
// ✅ 也可以手动释放
var plc = new SiemensPlcConnector("192.168.0.1");
try
{
plc.Connect();
// ... 业务代码 ...
}
finally
{
plc.Dispose();
}
学习路线建议
第 1 步:跑通 Program.cs,确认能连接 PLC
↓
第 2 步:在 TIA Portal 建一个 DB1,添加 BOOL/INT/REAL 变量
↓
第 3 步:用 ReadBool / ReadInt / ReadReal 逐个读取,核对数值
↓
第 4 步:用 Write 写入,在 TIA Portal 在线监控中观察变化
↓
第 5 步:阅读 SiemensPlcConnector.cs 源码,理解封装思路
↓
第 6 步:在自己的 WinForms/WPF 项目中引用本模块
参考资源
- S7netplus GitHub ------ 底层 S7 协议库文档
- 西门子 S7 地址寻址手册 ------ 官方地址格式说明