西门子 PLC 与 C# 通信

西门子 PLC 与 C# 通信

本教程基于项目中的 SiemensPlcConnector 模块,从零讲解如何用 C# 连接西门子 PLC 并读写数据。


目录

  1. 先了解几个概念
  2. 环境准备
  3. 项目结构
  4. [第一步:连接 PLC](#第一步:连接 PLC)
  5. 第二步:读取数据
  6. 第三步:写入数据
  7. [S7 地址格式速查](#S7 地址格式速查)
  8. 核心代码逐段讲解
  9. 完整示例程序
  10. 常见问题排查

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,PLC 192.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-1200
  • throw 抛出异常,让调用者知道参数不合法

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> 是泛型:同一个方法可以返回 boolintfloat 等不同类型
  • 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 项目中引用本模块

参考资源

相关推荐
gugucoding1 小时前
31. 【C语言】堆栈与队列的实现
c语言·开发语言·数据结构·链表
万联WANFLOW2 小时前
多分支企业组网,IP 网段到底该怎么规划
开发语言·php
阿里嘎多学长2 小时前
2026-07-07 GitHub 热点项目精选
开发语言·程序员·github·代码托管
cookies_s_s4 小时前
C++ 字符串动态创建对象 -- 工厂模式、自动注册、模板递归动态调用
服务器·开发语言·c++
盐焗鹌鹑蛋5 小时前
【C++】继承
开发语言·c++
栈溢出了6 小时前
Java Lambda 表达式笔记
java·开发语言·intellij-idea
zhixingheyi_tian6 小时前
一份GC日志的解读
java·开发语言
czhaii6 小时前
串口1中断收发-C语言-MODBUS协议
c语言·开发语言
apihz6 小时前
随机驾考题目(C 照科一 / 科四 2000+ 题)免费API调用教程
android·java·c语言·开发语言·网络协议·tcp/ip