1. 项目概述:C#与西门子PLC以太网通信解决方案
在工业自动化领域,上位机与PLC的可靠通信是系统稳定运行的基础。经过多个实际项目的验证,我开发了一套基于C#和S7.Net库的西门子全系列PLC通信方案。这套方案最大的特点是:
- 支持西门子S7-200/200Smart/300/1200/1500全系列PLC
- 采用纯以太网通信,摆脱了对OPC和专用通信卡的依赖
- 数据传输稳定高效,实测千兆网络下单次读写仅需1-2ms
- 提供完整的连接状态监控和自动重连机制
这套系统已成功应用于多条生产线,包括汽车零部件装配线和食品包装产线,最长连续运行时间超过30天无故障。
2. 技术架构解析
2.1 通信协议栈
西门子S7协议栈分为三层:
- TPKT层(RFC1006):处理报文分帧和重组
- COTP层(ISO 8073):建立和维持连接
- S7层:实际的数据读写服务
S7.Net库完美实现了这三层协议,我们在此基础上进行了二次封装,主要解决了以下问题:
- 字节序转换(西门子使用大端序,x86平台使用小端序)
- 数据类型映射(特别是浮点数和字符串)
- 连接状态管理
2.2 核心类设计
csharp复制public class S7NetPLC
{
private Plc _plc; // S7.Net核心对象
private string _ip;
private int _rack;
private int _slot;
private CpuType _cpuType;
// 连接状态机
private enum ConnectionState { Disconnected, Connecting, Connected }
private ConnectionState _state = ConnectionState.Disconnected;
// 自动重连线程
private Thread _reconnectThread;
// 公开属性
public bool IsConnected => _state == ConnectionState.Connected;
public int ReconnectCount { get; private set; }
}
3. 通信实现细节
3.1 连接管理
可靠的连接是通信的基础,我们实现了以下机制:
-
双保险检测:
- ICMP Ping检测网络层可达性
- 端口102检测ISO-on-TCP服务可用性
-
指数退避重连:
csharp复制private void ReconnectLoop()
{
while(true)
{
if(!IsConnected)
{
try
{
// 首次立即重连,之后按200ms,400ms,800ms...递增
int delay = Math.Min(200 * (int)Math.Pow(2, ReconnectCount), 5000);
Thread.Sleep(delay);
Connect();
ReconnectCount++;
}
catch { /* 记录日志 */ }
}
Thread.Sleep(100);
}
}
3.2 数据读写实现
3.2.1 基本数据类型支持
我们封装了所有PLC支持的数据类型:
| C#类型 | PLC类型 | 字节数 | 备注 |
|---|---|---|---|
| bool | Bit | 1 | 支持I/Q/M/DB区 |
| byte | Byte | 1 | 无符号 |
| short | Int | 2 | 有符号 |
| ushort | Word | 2 | 无符号 |
| int | DInt | 4 | 有符号 |
| uint | DWord | 4 | 无符号 |
| float | Real | 4 | IEEE754 |
| double | LReal | 8 | IEEE754 |
| string | String | 1-254 | 带长度前缀 |
3.2.2 结构体处理
对于复杂数据结构,我们使用结构体映射:
csharp复制[StructLayout(LayoutKind.Sequential, Pack=1)]
public struct MotorData
{
public float Current; // DB块偏移0-3
public float Speed; // DB块偏移4-7
public ushort Status; // DB块偏移8-9
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=20)]
public string Name; // DB块偏移10-31
}
// 读取示例
public MotorData ReadMotorData(int dbNumber, int startOffset)
{
byte[] buffer = _plc.ReadBytes(DataType.DataBlock, dbNumber, startOffset, 32);
return ByteArrayToStructure<MotorData>(buffer);
}
注意:结构体字段顺序必须与PLC中定义完全一致,且禁用自动内存对齐(Pack=1)
4. 多线程与UI集成
4.1 线程模型设计
工业应用要求UI流畅的同时保证通信实时性,我们采用三层线程模型:
- UI线程:仅负责界面更新,通过Invoke安全更新控件
- 通信线程:定时批量读写数据(默认200ms周期)
- 监控线程:持续检查连接状态,触发重连
csharp复制// UI安全更新示例
delegate void SetTextDelegate(TextBox box, string text);
void SafeSetText(TextBox box, string text)
{
if(box.InvokeRequired)
box.Invoke(new SetTextDelegate(SafeSetText), box, text);
else
box.Text = text;
}
4.2 批量读写优化
为提高效率,我们实现了三种读写策略:
- 单变量读写:适合偶尔操作
- 批量读写:一次性读取多个连续变量
- 全块读取+内存解析:最高效的结构体处理方式
实测性能对比(100个DInt变量):
| 方式 | 耗时(ms) | CPU占用 |
|---|---|---|
| 单变量循环 | 120-150 | 5-8% |
| 批量读取 | 15-20 | <1% |
| 全块读取 | 8-12 | <1% |
5. 实际应用案例
5.1 汽车焊接生产线监控
在该项目中,我们需要监控:
- 200个焊接点的实时温度(Float)
- 50个气缸的状态(Bit)
- 10台伺服电机参数(Struct)
实现方案:
- 创建三个通信线程分别处理不同类型数据
- 使用全块读取方式获取电机参数
- 每500ms更新一次UI
关键代码:
csharp复制// 电机数据结构
public struct ServoMotor
{
public float Position;
public float Speed;
public ushort ErrorCode;
public byte Status;
}
// 批量读取
public List<ServoMotor> ReadMotors(int dbNumber, int startAddr, int count)
{
int structSize = Marshal.SizeOf(typeof(ServoMotor));
byte[] buffer = _plc.ReadBytes(DataType.DataBlock, dbNumber, startAddr, structSize * count);
List<ServoMotor> motors = new List<ServoMotor>();
for(int i=0; i<count; i++)
{
byte[] temp = new byte[structSize];
Array.Copy(buffer, i*structSize, temp, 0, structSize);
motors.Add(ByteArrayToStructure<ServoMotor>(temp));
}
return motors;
}
5.2 常见问题解决方案
5.2.1 连接不稳定
现象:网络波动导致频繁断开
解决:
- 增加心跳包机制(每5秒读取一个固定地址)
- 优化重连策略:首次立即重连,之后指数退避
- 网络异常时暂停数据读写
5.2.2 数据不同步
现象:UI显示值与实际PLC值不一致
解决:
- 实现数据版本号机制
- 只有值变化时才更新UI
- 添加手动刷新按钮
5.2.3 性能瓶颈
现象:数据量大时通信延迟
解决:
- 将数据分组,不同组使用不同通信周期
- 关键数据实时读取,次要数据周期读取
- 使用后台线程预处理数据
6. 部署与优化建议
6.1 环境配置
-
网络要求:
- 建议使用专用工业交换机
- 禁用网络适配器的节能模式
- 设置静态IP避免DHCP问题
-
软件配置:
xml复制<appSettings> <add key="PlcIp" value="192.168.0.1"/> <add key="Rack" value="0"/> <add key="Slot" value="1"/> <add key="ReadInterval" value="200"/> <add key="MaxReconnectAttempts" value="10"/> </appSettings>
6.2 性能调优
-
通信参数优化:
- 调整读取块大小(通常480字节最佳)
- 优化读取周期(关键数据100ms,普通数据500ms)
-
内存管理:
- 重用byte[]缓冲区避免频繁分配
- 使用对象池管理通信对象
-
日志记录:
- 记录通信异常和重连事件
- 定期分析日志优化参数
7. 扩展功能实现
7.1 数据记录
添加SQLite本地存储:
csharp复制public void LogData(string tag, object value)
{
string sql = @"INSERT INTO DataLog(TagName,Value,Timestamp)
VALUES(@tag,@value,datetime('now'))";
using(var conn = new SQLiteConnection("Data Source=log.db"))
{
conn.Open();
var cmd = new SQLiteCommand(sql, conn);
cmd.Parameters.AddWithValue("@tag", tag);
cmd.Parameters.AddWithValue("@value", value.ToString());
cmd.ExecuteNonQuery();
}
}
7.2 报警管理
实现简单的报警规则:
csharp复制public class AlarmMonitor
{
private Dictionary<string, AlarmRule> _rules;
public void CheckAlarms(Dictionary<string, object> currentValues)
{
foreach(var rule in _rules)
{
if(rule.Value.Check(currentValues[rule.Key]))
{
// 触发报警
}
}
}
}
public class AlarmRule
{
public string Tag { get; set; }
public Func<object, bool> Condition { get; set; }
public bool Check(object value)
{
return Condition(value);
}
}
8. 开发注意事项
-
PLC侧配置:
- 确保PLC IP地址固定
- 对于S7-1200/1500,关闭"优化块访问"
- 设置足够的通信资源
-
字符串处理:
- 西门子字符串有长度前缀(1200/1500为2字节,200Smart为1字节)
- 字符串最大长度254字节
- 建议使用ASCII编码节省空间
-
数据类型转换:
- 浮点数要特别注意字节序
- 布尔值建议单独处理,避免结构体打包问题
- 日期时间需要特殊转换
-
多线程安全:
- 避免多个线程同时访问同一个Plc实例
- 使用lock保护共享资源
- 考虑使用读写锁优化性能
这套系统经过多个版本的迭代,目前已经形成了一套稳定的工业通信框架。在实际应用中,最重要的经验是:通信模块要尽可能简单可靠,业务逻辑应该与通信层分离。这样当需要更换通信方式(比如从S7协议转OPC UA)时,只需要替换通信层即可。
