西门子 PLC 与 C# 通信_项目实战 一、项目概述该项目是一个基于 WPF 和 .NET Framework 4.7.2 开发的西门子 S7-1200 PLC 数据采集应用程序用于某周期性数据采集场景。核心功能单路实时采集每秒读取 PLC 指定地址的数值实时刷新界面自动周期存盘按配置间隔默认 10 分钟将瞬时值和周期最低值写入 CSV手动单次采集用户可随时触发一次采集并独立存盘秒级高精度采集可选的秒级 CSV 异步写入不影响主采集路径长期稳定运行支持 8 小时以上连续运行异常不导致进程退出技术栈组件版本说明框架.NET Framework 4.7.2WPF 桌面应用PLC 通信S7netplus 0.20.0西门子 S7 协议库UI 框架WPF现代化界面设计二、项目结构S7DataCollector/ ├── Config/ │ └── AppSettings.cs # 集中配置读取App.config 默认值 ├── Models/ │ └── DataRecord.cs # 采集记录模型 ├── Services/ │ ├── CsvLogger.cs # 自动/手动 CSV 写入器 │ ├── ErrorLogger.cs # 错误日志写入器 │ ├── PlcService.cs # PLC 通信服务封装 │ └── SecondCsvWriter.cs # 秒级 CSV 异步写入器 ├── App.xaml / App.xaml.cs # 应用入口与全局异常处理 ├── MainWindow.xaml / MainWindow.xaml.cs # 主窗口与业务逻辑 ├── App.config # 运行时配置 └── S7DataCollector.csproj # 项目配置三、核心架构3.1 单路采集设计程序采用单路采集架构即只有一个定时器负责读取 PLC读取结果同时供给三个消费端UiTimer (1秒) ──→ Read PLC ──→ ① UI 实时显示 ──→ ② 周期最低值累加器 ──→ ③ 秒级 CSV 入队这种设计确保了每秒只读取一次 PLC降低通信负荷所有消费端使用同一数据源避免数据不一致读超时被压缩到刷新周期内避免一次慢读阻塞后续采集3.2 双定时器模型定时器间隔职责_uiTimerUiRefreshSeconds默认 1 秒唯一读 PLC 路径更新界面和周期最低值_logTimerIntervalMinutes默认 10 分钟触发自动存盘写入 CSV 和历史表3.3 三路 CSV 输出程序生成三类独立的 CSV 文件文件类型文件名触发方式特点自动采集DataLog_Auto_yyyyMMdd.csv定时器周期触发含瞬时值 周期最低值手动采集DataLog_Manual_yyyyMMdd.csv用户点击触发独立序号不含周期最低值秒级采集DataLog_Second_yyyyMMdd.csv每秒入队后台批量写异步写入不阻塞主流程四、配置说明配置文件位于App.config开发时或S7DataCollector.exe.config运行时。?xml version1.0 encodingutf-8?!-- 运行时配置PLC 连接与采集周期。 修改后需重新部署/覆盖 exe 同目录的 S7DataCollector.exe.config 才会生效。 业务代码通过 AppSettings 读取键缺失时使用 AppSettings 中的默认常量。 --configurationstartupsupportedRuntimeversionv4.0sku.NETFramework,Versionv4.7.2//startupappSettings!-- PLC 连接参数 --!-- PLC IP 地址 --addkeyPlcIpvalue192.168.0.1/!-- 机架号S7-1200 通常为 0 --addkeyPlcRackvalue0/!-- 插槽号S7-1200 通常为 1 --addkeyPlcSlotvalue1/!-- 读数地址与类型 --!-- 读取地址MD10 M区双字字节偏移10也可改为 DB1.DBD0 等 --addkeyPlcAddressvalueMD10/!-- Float/Real按 IEEE754 浮点解析读到 DInt 位型时会自动转 Float 其它可选DInt / DWord / Int / Word --addkeyPlcValueTypevalueFloat/!-- 采集周期 --!-- 自动写入 CSV 的间隔分钟仅影响存盘不影响界面刷新 --addkeyIntervalMinutesvalue10/!-- 界面「当前 PLC 数值」刷新间隔秒 单路读 PLC 周期与 CSV 存盘无关 --addkeyUiRefreshSecondsvalue1/!-- PLC 通信超时上限毫秒单路采集实际超时还会被压到「刷新周期-100ms」以内 --addkeyReadTimeoutMsvalue5000/!-- 秒级 CSV与 10 分钟 Auto 文件分离采集线程只入队后台批量写 --!-- true/false是否启用 DataLog_Second_yyyyMMdd.csv --addkeyEnableSecondLogvaluetrue/!-- 有界队列容量约等于可缓冲秒数写不过来时丢最旧并计数 --addkeySecondLogQueueCapacityvalue600/!-- 单次最多刷多少行 --addkeySecondLogBatchSizevalue50/!-- 最大刷盘间隔毫秒与 BatchSize 任一触发即写 --addkeySecondLogFlushIntervalMsvalue1000//appSettings/configuration4.1 PLC 连接参数配置项默认值说明PlcIp192.168.0.1PLC IP 地址PlcRack0机架号S7-1200 通常为 0PlcSlot1插槽号S7-1200 通常为 1usingSystem;usingSystem.Globalization;usingS7.Net;usingS7DataCollector.Config;namespaceS7DataCollector.Services{/// summary/// PLC 通信服务封装 S7netplus负责与西门子 S7-1200 的连接、读数、断开。/// para/// 默认读取 M 区双字地址 MD10数值类型由 see crefAppSettings.PlcValueType/ 决定///Float/Real 时按 IEEE754 解释 32 位原始数据。/// /para/// para本类方法均为线程安全内部 c_sync/c 锁调用方仍建议串行化对同一实例的访问。/para/// /summarypublicsealedclassPlcService:IDisposable{/// summary保护 _plc 实例的互斥锁/summaryprivatereadonlyobject_syncnewobject();/// summaryS7netplus 底层 PLC 对象未连接或已断开时为 null/summaryprivatePlc_plc;privatebool_disposed;/// summaryPLC IP 地址/summarypublicstringIpAddress{get;}/// summary机架号S7-1200 通常为 0/summarypublicshortRack{get;}/// summary插槽号S7-1200 通常为 1/summarypublicshortSlot{get;}/// summary读取地址如 MD10、MW10、DB1.DBW0/summarypublicstringAddress{get;}/// summary数值类型名Float / Real / DInt / DWord / Int 等/summarypublicstringValueTypeName{get;}/// summary读写超时毫秒/summarypublicintReadTimeoutMs{get;}/// summary当前是否已与 PLC 建立连接/summarypublicboolIsConnected{get{lock(_sync){return_plc!null_plc.IsConnected;}}}/// summary/// 创建 PLC 服务。参数为 null 时从 see crefAppSettings/ 读取默认配置。/// /summarypublicPlcService(stringipnull,short?racknull,short?slotnull,stringaddressnull,stringvalueTypenull,int?readTimeoutMsnull){IpAddressstring.IsNullOrWhiteSpace(ip)?AppSettings.PlcIp:ip.Trim();Rackrack??AppSettings.PlcRack;Slotslot??AppSettings.PlcSlot;Addressstring.IsNullOrWhiteSpace(address)?AppSettings.PlcAddress:address.Trim();ValueTypeNamestring.IsNullOrWhiteSpace(valueType)?AppSettings.PlcValueType:valueType.Trim();ReadTimeoutMsreadTimeoutMs??AppSettings.ReadTimeoutMs;}/// summary/// 连接到 PLC。若已有连接会先断开再重建。/// /summary/// exception crefInvalidOperationException连接失败时抛出/exceptionpublicvoidConnect(){ThrowIfDisposed();lock(_sync){DisconnectInternal();// CpuType.S71200适配本项目目标 PLCS7-1200_plcnewPlc(CpuType.S71200,IpAddress,Rack,Slot){ReadTimeoutReadTimeoutMs,WriteTimeoutReadTimeoutMs};_plc.Open();if(!_plc.IsConnected){thrownewInvalidOperationException($无法连接到 PLC{IpAddress}(Rack{Rack}, Slot{Slot}));}}}/// summary/// 按配置地址读取数值MD10 默认按 Float。/// 若底层读到 DInt/DWord 位型会按 IEEE754 转成 Float。/// /summary/// returns转换后的 double 数值/returns/// exception crefInvalidOperationException未连接或读空值时抛出/exceptionpublicdoubleReadValue(){ThrowIfDisposed();lock(_sync){if(_plcnull||!_plc.IsConnected){thrownewInvalidOperationException(PLC 未连接无法读取数据。);}varresult_plc.Read(Address);if(resultnull){thrownewInvalidOperationException($读取{Address}返回空值。);}returnConvertByValueType(result);}}/// summary/// 按 see crefValueTypeName/ 将底层读结果转换为 double。/// /summaryprivatedoubleConvertByValueType(objectresult){vartypeName(ValueTypeName??Float).Trim();// Float / Real真空等模拟量常用按 IEEE754 解释 32 位if(typeName.Equals(Real,StringComparison.OrdinalIgnoreCase)||typeName.Equals(Float,StringComparison.OrdinalIgnoreCase)){returnConvertDIntBitsToFloat(result);}if(typeName.Equals(DWord,StringComparison.OrdinalIgnoreCase)||typeName.Equals(UDInt,StringComparison.OrdinalIgnoreCase)){returnConvert.ToUInt32(result,CultureInfo.InvariantCulture);}if(typeName.Equals(Int,StringComparison.OrdinalIgnoreCase)||typeName.Equals(Word,StringComparison.OrdinalIgnoreCase)){returnConvert.ToInt16(result,CultureInfo.InvariantCulture);}if(typeName.Equals(DInt,StringComparison.OrdinalIgnoreCase)){returnConvert.ToInt32(result,CultureInfo.InvariantCulture);}// 未识别类型时按 FloatDInt 位型转浮点处理returnConvertDIntBitsToFloat(result);}/// summary/// 将 DInt/DWord 的 32 位原始数据按 IEEE754 解释为 Float。/// S7netplus 读 MD 区时有时返回 int/uint需用位型重解释而非数值强转。/// /summaryprivatestaticdoubleConvertDIntBitsToFloat(objectresult){if(resultisfloatf)returnf;if(resultisdoubled)returnd;if(resultisuintu)returnBitConverter.ToSingle(BitConverter.GetBytes(u),0);if(resultisinti)returnBitConverter.ToSingle(BitConverter.GetBytes(i),0);if(resultislongl)returnBitConverter.ToSingle(BitConverter.GetBytes(unchecked((int)l)),0);// 其它数值先落到 32 位整型位型再转 Floattry{varbitsConvert.ToInt32(result,CultureInfo.InvariantCulture);returnBitConverter.ToSingle(BitConverter.GetBytes(bits),0);}catch{returnConvert.ToSingle(result,CultureInfo.InvariantCulture);}}/// summary断开与 PLC 的连接可重复调用/summarypublicvoidDisconnect(){lock(_sync){DisconnectInternal();}}/// summary内部断开逻辑调用方须已持有 _sync/summaryprivatevoidDisconnectInternal(){if(_plcnull)return;try{if(_plc.IsConnected){_plc.Close();}}catch{// 断开时忽略异常避免掩盖上层重连逻辑}finally{_plcnull;}}privatevoidThrowIfDisposed(){if(_disposed)thrownewObjectDisposedException(nameof(PlcService));}/// summary释放资源断开连接并标记已释放/summarypublicvoidDispose(){if(_disposed)return;Disconnect();_disposedtrue;}}}4.2 读数参数配置项默认值说明PlcAddressMD10读取地址如 MD10, DB1.DBD0PlcValueTypeFloat值类型Float/Real/DInt/DWord/Int4.3 采集周期配置项默认值说明IntervalMinutes10自动 CSV 存盘间隔分钟UiRefreshSeconds1界面刷新间隔即单路采集周期秒ReadTimeoutMs5000PLC 通信超时上限毫秒4.4 秒级 CSV 参数配置项默认值说明EnableSecondLogtrue是否启用秒级 CSVSecondLogQueueCapacity600有界队列容量约缓冲秒数SecondLogBatchSize50单次最多刷盘行数SecondLogFlushIntervalMs1000最大刷盘间隔毫秒五、数据流程5.1 启动流程App.OnStartup └── StartApplication ├── RegisterGlobalExceptionHandlers() # 注册三类异常钩子 ├── EnsureLogsDirectory() # 确保 Logs 目录存在 ├── WriteSessionStart() # 写入会话启动日志 └── new MainWindow().Show() # 显示主窗口5.2 自动采集流程BtnStart_Click ├── EnsurePlcConnected(forceReconnect: true) ├── StartTimers() # 启动双定时器 ├── ResetIntervalMin() # 重置周期最低值累加器 ├── RefreshRealtimeAsync() # 立即刷新界面写入缓存 └── SaveAutoSampleAsync() # 立即记一条自动 CSV ┌── UiTimer_Elapsed (每秒触发) │ └── RefreshRealtimeAsync() │ ├── Read PLC │ ├── PublishSample() │ │ ├── UpdateLastSample() # 更新缓存 │ │ ├── UpdateIntervalMin() # 更新周期最低值 │ │ └── SecondCsvWriter.TryEnqueue() # 秒级入队 │ └── 更新 UI └── LogTimer_Elapsed (每10分钟触发) └── SaveAutoSampleAsync() ├── TryGetFreshInPeriodSample() # 取新鲜缓存瞬时值 ├── TakeAndClearIntervalMin() # 结算周期最低值 ├── CsvLogger.Append() # 写入 CSV ├── AddHistoryBounded() # 追加历史表 └── BeginNewPeriod() # 推进周期起点5.3 周期最低值机制程序在每个自动采集周期内默认 10 分钟维护一个最低值采样阶段每秒读取的数值与当前周期最低值比较保留较小值结算阶段自动存盘时取出周期最低值清空累加器回滚机制若写盘失败已取出的最低值会合并回当前周期周期边界拒绝上周期遗留的陈旧采样避免边界低压点污染六、并发设计6.1 同步原语锁对象用途说明_plcLockPLC 读写互斥所有 PlcService 调用须在此锁内_logGate存盘互斥使用SemaphoreSlim支持 async/await_intervalMinLock周期最低值保护保护_intervalMinValue,_intervalMinTime,_periodStart_lastSampleLock采样缓存保护保护_lastValue,_lastStamp_uiRefreshBusy界面刷新防重入使用Interlocked.CompareExchange6.2 秒级 CSV 异步写入SecondCsvWriter采用生产者-消费者模式采集线程只调用TryEnqueue()非阻塞入队后台工作线程按批BatchSize或按间隔FlushIntervalMs批量刷盘有界队列队列满时丢弃最旧数据不反压采集线程七、错误处理与容错7.1 全局异常防护程序注册了三类未处理异常钩子全部记录日志并尽量阻止进程退出异常类型处理方式DispatcherUnhandledException记日志标记Handled trueAppDomain.UnhandledException记日志尽力阻止终止TaskScheduler.UnobservedTaskException记日志调用SetObserved()7.2 自动存盘失败处理存盘冲突自动采集标记延后补写_pendingAutoSave不丢周期写盘失败回滚已取出的周期最低值预约重试_autoSaveRetryAfterTicks重试触发由下次秒级刷新路径顺带触发补写7.3 PLC 通信异常处理连接失败进入周期重试模式由秒级定时器持续尝试重连连续失败计数记录连续失败次数用于状态提示安全断开断开时忽略异常避免掩盖上层重连逻辑八、文件输出格式8.1 自动采集 CSVId,Data,Time,Source,MinData,MinTime 1,1.23456,2026-07-16 10:00:00,Auto,1.12345,2026-07-16 10:05:30 2,1.34567,2026-07-16 10:10:00,Auto,1.23456,2026-07-16 10:12:158.2 手动采集 CSVId,Data,Time,Source 1,1.45678,2026-07-16 10:03:00,Manual 2,1.56789,2026-07-16 10:08:00,Manual8.3 秒级采集 CSVId,Data,Time 1,1.23456,2026-07-16 10:00:00 2,1.23457,2026-07-16 10:00:01 3,1.23455,2026-07-16 10:00:02九、界面布局主窗口自上而下分为五个区域区域内容功能通信状态区指示灯 状态文字 配置摘要显示连接状态和当前配置实时数据区大字号数值 采样时间实时显示 PLC 读数历史表格区DataGrid 绑定ObservableCollectionDataRecord显示采集历史记录控制按钮区启动采集 / 停止采集 / 手动采集一次控制采集流程路径状态栏文件路径提示 运行状态消息显示输出路径和运行状态十、运行说明10.1 启动方式方式一直接运行最快双击S7DataCollector\bin\Release\net472\S7DataCollector.exe方式二Visual Studio打开 S7DataCollector.sln → F5调试或 CtrlF5直接运行方式三命令行cdS7DataCollectordotnet run-cRelease10.2 配置修改注意事项修改S7DataCollector.exe.config运行时而非源码中的App.config修改后必须关闭程序再重新打开配置才会生效PLC 需允许 PUT/GET 访问电脑防火墙需放行连不上时查看界面红色状态灯和Logs\ErrorLog.txt10.3 部署要求目标机器需安装 .NET Framework 4.7.2 或更高版本电脑与 PLC 需在同一网段建议先 ping 通 PLC IP 再启动程序十一、设计要点总结单路采集一个定时器读 PLC多路消费降低通信负荷缓存策略使用缓存瞬时值避免边界再读 PLC新鲜度阈值 2 秒周期最低值半开区间累加写盘失败回滚边界不污染异步秒级写入有界队列 后台批量写不阻塞主流程异常防护全局异常钩子 自动重试 状态恢复支持长期无人值守资源管理完整的IDisposable实现窗口关闭时释放所有资源