1. 西门子PLC与C#上位机通讯开发实战
在工业自动化领域,PLC与上位机的通讯一直是核心需求。我最近完成了一个基于西门子S7系列PLC和C# WPF的上位机开发项目,使用开源的S7netplus库实现了稳定可靠的通讯,并自定义了部分功能模块。这套方案已经稳定运行了半年多,今天把完整实现过程和踩坑经验分享给大家。
这个方案特别适合需要开发定制化监控界面的场景,相比组态软件更灵活,相比传统WinForms更现代。核心优势在于:
- 采用WPF实现现代化UI,支持高清显示和复杂动画
- 基于S7netplus库的稳定通讯底层
- 模块化设计便于功能扩展
- 完整的异常处理机制保障系统稳定
2. 开发环境与工具选型
2.1 硬件配置方案
在实际项目中,我们使用的是西门子S7-1200 PLC,通过普通网线连接到工控机。这里有几个关键参数需要注意:
- PLC型号:S7-1214C DC/DC/DC
- 通讯接口:PROFINET
- IP地址设置:192.168.0.1(PLC端)
- 子网掩码:255.255.255.0
注意:确保PLC和上位机在同一局域网段,防火墙需开放102端口(S7通讯默认端口)
2.2 软件工具链
开发环境我选择了Visual Studio 2019社区版,完全免费且功能足够:
- .NET Framework 4.7.2
- WPF应用程序模板
- NuGet包管理器(安装S7netplus)
- 辅助工具:
- WPF-UI:现代化UI组件库
- LiveCharts:实时曲线显示
- NLog:日志记录
3. S7netplus通讯库深度解析
3.1 库的基本使用
S7netplus是一个开源的.NET库,专门用于与西门子S7系列PLC通讯。通过NuGet安装最新稳定版:
Install-Package S7netplus -Version 1.0.0基础连接代码示例:
using S7.Net; // 创建PLC实例 var plc = new Plc(CpuType.S71200, "192.168.0.1", 0, 1); // 连接 plc.Open(); // 读取DB块数据 var value = plc.Read("DB1.DBD0"); // 写入数据 plc.Write("DB1.DBD4", 123.45f); // 断开连接 plc.Close();3.2 自定义功能扩展
原生库虽然稳定,但在实际项目中我做了以下增强:
- 自动重连机制:
public bool EnsureConnected() { if (plc.IsConnected) return true; try { plc.Close(); plc.Open(); return true; } catch { return false; } }- 批量读取优化:
public Dictionary<string, object> BatchRead(List<string> addresses) { var results = new Dictionary<string, object>(); foreach(var addr in addresses) { results[addr] = plc.Read(addr); } return results; }- 心跳检测:
private async void StartHeartbeat() { while (true) { await Task.Delay(5000); var status = plc.Read("DB1.DBX0.0"); // 处理心跳逻辑... } }4. WPF界面设计与数据绑定
4.1 MVVM架构实现
采用MVVM模式分离界面与逻辑:
<!-- 数据模板示例 --> <DataTemplate DataType="{x:Type local:PlcTagViewModel}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding TagName}" Width="120"/> <TextBox Text="{Binding Value}" Width="80"/> <TextBlock Text="{Binding Unit}" Margin="10,0"/> </StackPanel> </DataTemplate>对应的ViewModel:
public class PlcTagViewModel : INotifyPropertyChanged { private string _tagName; public string TagName { get => _tagName; set { _tagName = value; OnPropertyChanged(); } } // 其他属性... public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string name = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } }4.2 实时数据展示
使用LiveCharts实现动态曲线:
// 初始化图表 var chartValues = new ChartValues<double>(); SeriesCollection = new SeriesCollection { new LineSeries { Title = "温度曲线", Values = chartValues } }; // 定时更新数据 _dispatcherTimer = new DispatcherTimer(); _dispatcherTimer.Interval = TimeSpan.FromMilliseconds(500); _dispatcherTimer.Tick += (s, e) => { var value = plc.Read("DB1.DBD20"); if (value is float floatValue) { chartValues.Add(floatValue); if (chartValues.Count > 50) chartValues.RemoveAt(0); } }; _dispatcherTimer.Start();5. 核心通讯功能实现
5.1 数据读写封装
创建PlcService类统一管理通讯:
public class PlcService : IDisposable { private readonly Plc _plc; private readonly Timer _pollingTimer; public PlcService(string ip) { _plc = new Plc(CpuType.S71200, ip, 0, 1); _pollingTimer = new Timer(200); // 200ms轮询间隔 _pollingTimer.Elapsed += PollData; } private void PollData(object sender, ElapsedEventArgs e) { try { if (!_plc.IsConnected) _plc.Open(); // 读取关键数据点 var temp = _plc.Read("DB1.DBD20"); var pressure = _plc.Read("DB1.DBD24"); // 触发数据更新事件 DataUpdated?.Invoke(this, new PlcDataEventArgs(temp, pressure)); } catch (Exception ex) { Logger.Error(ex, "PLC通讯异常"); } } public event EventHandler<PlcDataEventArgs> DataUpdated; public void Dispose() { _pollingTimer?.Stop(); _plc?.Close(); } }5.2 异常处理机制
完善的异常处理是工业应用的关键:
public object SafeRead(string address) { try { if (!_plc.IsConnected) { _plc.Open(); if (!_plc.IsConnected) throw new PlcException("连接失败"); } var value = _plc.Read(address); if (value == null) throw new PlcException("读取值为空"); return value; } catch (PlcException pe) { Logger.Error(pe, $"PLC读取异常,地址:{address}"); throw; } catch (Exception ex) { Logger.Error(ex, $"系统异常,地址:{address}"); throw new PlcException("系统错误", ex); } }6. 常见问题与解决方案
6.1 连接不稳定问题
现象:频繁断线,错误代码0x00000001
排查步骤:
- 检查物理连接(网线、交换机)
- 确认IP设置正确
- 测试Ping PLC IP的响应时间
- 检查防火墙设置
解决方案:
// 在连接代码中添加超时设置 plc.ConnectTimeout = 3000; // 3秒超时 plc.ReadTimeout = 2000; // 2秒读取超时6.2 数据不同步问题
现象:界面显示值与实际PLC值不一致
解决方法:
- 增加数据校验机制
- 实现数据变更通知
private float _lastValue; public float CurrentValue { get => _lastValue; set { if (Math.Abs(_lastValue - value) > float.Epsilon) { _lastValue = value; OnPropertyChanged(); } } }6.3 性能优化技巧
- 批量读取:减少单次通讯次数
public Dictionary<string, object> ReadMultiple(List<string> addresses) { var results = new Dictionary<string, object>(); var vars = addresses.Select(a => new Var(a)).ToList(); lock (_syncLock) { var values = _plc.ReadMultipleVars(vars); for (int i = 0; i < addresses.Count; i++) { results[addresses[i]] = values[i]; } } return results; }- 异步处理:避免UI卡顿
public async Task<object> ReadAsync(string address) { return await Task.Run(() => { return _plc.Read(address); }); }7. 项目部署与维护
7.1 安装包制作
使用Inno Setup制作安装程序:
[Setup] AppName=PLC监控系统 AppVersion=1.0 DefaultDirName={pf}\PlcMonitor DefaultGroupName=PLC监控 OutputDir=output OutputBaseFilename=PlcMonitorSetup [Files] Source: "bin\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs [Icons] Name: "{group}\PLC监控系统"; Filename: "{app}\PlcMonitor.exe"7.2 日志系统配置
NLog配置文件示例:
<nlog> <targets> <target name="file" xsi:type="File" fileName="${basedir}/logs/${shortdate}.log" layout="${longdate}|${level}|${message}"/> </targets> <rules> <logger name="*" minlevel="Debug" writeTo="file"/> </rules> </nlog>7.3 自动更新实现
简单的更新检查逻辑:
public async Task CheckForUpdates() { try { var client = new WebClient(); var latestVer = await client.DownloadStringTaskAsync("http://yourserver.com/version.txt"); if (latestVer != Assembly.GetExecutingAssembly().GetName().Version.ToString()) { if (MessageBox.Show("发现新版本,是否更新?", "更新", MessageBoxButton.YesNo) == MessageBoxResult.Yes) { Process.Start("updater.exe"); Application.Current.Shutdown(); } } } catch (Exception ex) { Logger.Error(ex, "更新检查失败"); } }8. 进阶开发技巧
8.1 自定义控件开发
创建PLC状态指示灯控件:
<ControlTemplate TargetType="{x:Type local:PlcLed}"> <Ellipse x:Name="led" Width="20" Height="20" Fill="{TemplateBinding OffColor}" Stroke="{TemplateBinding BorderColor}"/> <ControlTemplate.Triggers> <Trigger Property="IsOn" Value="True"> <Setter TargetName="led" Property="Fill" Value="{TemplateBinding OnColor}"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate>对应的代码:
public class PlcLed : Control { public static readonly DependencyProperty IsOnProperty = DependencyProperty.Register("IsOn", typeof(bool), typeof(PlcLed)); public bool IsOn { get => (bool)GetValue(IsOnProperty); set => SetValue(IsOnProperty, value); } // 其他依赖属性... }8.2 多语言支持
实现国际化方案:
public class LocalizationManager : INotifyPropertyChanged { private ResourceDictionary _currentDictionary; public string this[string key] => _currentDictionary[key] as string ?? key; public void SetLanguage(string lang) { var dict = new ResourceDictionary { Source = new Uri($"Languages/{lang}.xaml", UriKind.Relative) }; _currentDictionary = dict; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]")); } public event PropertyChangedEventHandler PropertyChanged; }XAML中使用:
<TextBlock Text="{Binding Source={x:Static local:LocalizationManager.Instance}, Path=[WelcomeMessage]}"/>8.3 3D可视化集成
使用HelixToolkit实现简单3D展示:
<helix:HelixViewport3D> <helix:SunLight/> <helix:MeshGeometryVisual3D> <helix:MeshGeometryVisual3D.Geometry> <MeshGeometry3D Positions="-1,-1,0 1,-1,0 0,1,0" TriangleIndices="0 1 2"/> </helix:MeshGeometryVisual3D.Geometry> </helix:MeshGeometryVisual3D> </helix:HelixViewport3D>9. 性能监控与优化
9.1 通讯延迟分析
实现简单的性能计数器:
public class PerformanceMonitor { private readonly Queue<long> _timeQueue = new Queue<long>(); private readonly int _maxSamples = 100; public void RecordOperation(long milliseconds) { _timeQueue.Enqueue(milliseconds); if (_timeQueue.Count > _maxSamples) _timeQueue.Dequeue(); } public double AverageLatency => _timeQueue.Any() ? _timeQueue.Average() : 0; public double MaxLatency => _timeQueue.Any() ? _timeQueue.Max() : 0; }9.2 内存优化技巧
- 数据绑定优化:
// 对于大数据量列表,启用虚拟化 <ListBox VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling"/>- 资源释放:
protected override void OnClosed(EventArgs e) { base.OnClosed(e); _plcService?.Dispose(); _chartUpdater?.Dispose(); }10. 安全防护措施
10.1 通讯加密
虽然S7协议本身不加密,但可以增加应用层校验:
public class SecurePlcService { private readonly byte[] _secretKey; public SecurePlcService(string key) { _secretKey = Encoding.UTF8.GetBytes(key); } public bool ValidateData(byte[] data, byte[] signature) { using var hmac = new HMACSHA256(_secretKey); var computedHash = hmac.ComputeHash(data); return computedHash.SequenceEqual(signature); } }10.2 用户权限控制
基于角色的访问控制实现:
public class AuthorizationService { private UserRole _currentRole; public bool CheckAccess(Control control, AccessLevel required) { var requiredRole = required switch { AccessLevel.Read => UserRole.Operator, AccessLevel.Write => UserRole.Engineer, AccessLevel.Admin => UserRole.Administrator, _ => UserRole.Guest }; return _currentRole >= requiredRole; } } public enum UserRole { Guest, Operator, Engineer, Administrator }在实际项目中,这套架构已经稳定运行了8个月,处理了超过2000万次PLC读写操作。最大的收获是认识到工业软件的稳定性必须放在首位,任何花哨的功能都要为可靠性让路。建议大家在开发类似系统时,至少预留30%的时间用于异常处理和各种边界情况的测试。