17 KiB
17 KiB
PLC 模块使用指南 | PLC Module Usage Guide
1. 模块注册 | Module Registration
PLC 模块已在 App.xaml.cs 中注册,通过 Prism 的模块化系统自动加载。
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<PLCModule>();
base.ConfigureModuleCatalog(moduleCatalog);
}
DI 注册清单 | DI Registration Summary
PLCModule 在 RegisterTypes 中注册以下服务:
| 服务 | 生命周期 | 说明 |
|---|---|---|
IPlcClient → S7PlcClient |
瞬态 | 每次 Resolve 创建独立实例 |
PlcService |
单例 | PLC 连接管理 + 批量读取缓存 + 统一信号字典 |
IPlcService → PlcService |
单例(同实例) | 跨模块 PLC 连接状态查询接口 |
PlcWriteQueue |
单例 | 后台单线程顺序写入队列 |
ISignalDataService → SignalDataService |
单例 | 信号数据交互服务(对外接口) |
XmlSignalParser |
瞬态 | XML 信号定义解析器 |
ConfigLoader |
瞬态 | 配置加载器 |
2. 配置文件设置 | Configuration File Setup
在 App.config 中添加 PLC 连接配置:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- PLC 连接配置 | PLC Connection Configuration -->
<add key="Plc:IpAddress" value="192.168.1.100" />
<add key="Plc:Port" value="102" />
<add key="Plc:Rack" value="0" />
<add key="Plc:Slot" value="1" />
<add key="Plc:PlcType" value="S1200" />
<!-- 批量读取 DB 块配置 | Bulk Read DB Block Configuration -->
<!-- ReadDbBlock: 周期性批量读取的 DB 块,地址落在此范围内的信号走缓存读取 -->
<!-- ReadDbBlock: DB block for periodic bulk read; signals within this range use cache -->
<add key="Plc:ReadDbBlock" value="DB1" />
<add key="Plc:ReadStartAddress" value="200" />
<add key="Plc:ReadLength" value="200" />
<!-- PLC 超时配置 | PLC Timeout Configuration -->
<add key="Plc:ConnectTimeoutMs" value="3000" />
<add key="Plc:ReadTimeoutMs" value="3000" />
<add key="Plc:WriteTimeoutMs" value="3000" />
<!-- 自动重连(每 5 秒检测连接状态)| Auto Reconnection (checks every 5 seconds) -->
<add key="Plc:bReConnect" value="true" />
</appSettings>
</configuration>
注意:
WriteDbBlock配置项已移除。写入地址由信号所属 Group 的DBNumber自动决定。
支持的 PLC 类型 | Supported PLC Types
S200Smart- S7-200Smart 系列S300- S7-300 系列S400- S7-400 系列S1200- S7-1200 系列S1500- S7-1500 系列
3. 信号定义文件格式 | Signal Definition File Format
PlcAddrDfn.xml 采用 <Group> 分组结构,每个 Group 对应一个 DB 块:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Config>
<!-- 批量读取 DB(高频缓存信号)-->
<Group ID="SignalList_Read" DBNumber="1">
<Signal Name="PlcLive" Type="byte" StartAddr="200" IndexOrLength="" Remark="PLC 心跳" />
<Signal Name="SystemReady" Type="bool" StartAddr="221" IndexOrLength="1" Remark="系统就绪" />
</Group>
<!-- 写入 DB -->
<Group ID="SignalList_Write" DBNumber="31">
<Signal Name="SoftLive" Type="byte" StartAddr="0" IndexOrLength="" Remark="软件心跳" />
<Signal Name="StartCmd" Type="bool" StartAddr="221" IndexOrLength="0" Remark="启动命令" />
<Signal Name="DeviceName" Type="string" StartAddr="240" IndexOrLength="20" Remark="设备名称" />
</Group>
<!-- 其他 DB(单点读取)-->
<Group ID="Status" DBNumber="100">
<Signal Name="ScanMode" Type="byte" StartAddr="201" IndexOrLength="" Remark="扫描模式" />
</Group>
</Config>
XML 格式说明 | XML Format Notes
| 节点/属性 | 说明 |
|---|---|
<Group ID="..."> |
分组唯一标识,不可重复 |
<Group DBNumber="..."> |
该组信号所属的 PLC DB 块号(整数) |
<Signal Name="..."> |
信号逻辑名称,全局唯一(跨所有 Group) |
Type |
数据类型:bool / byte / short / int / single / double / string |
StartAddr |
起始字节地址(相对于数据块) |
IndexOrLength |
bool 类型为位索引(0-7),string 类型为字符串长度,其他类型留空 |
Remark |
备注说明(可选) |
信号名称全局唯一性约束 | Global Signal Name Uniqueness
所有 Group 中的信号名称必须全局唯一。LoadSignalDefinitions 加载时自动检查,重复时抛出 PlcException,包含重复名称和所属 Group ID。
4. 信号数据交互服务(推荐方式)| Signal Data Service (Recommended)
4.1 概述 | Overview
ISignalDataService 是外部模块与 PLC 交互的推荐接口。外部模块仅需通过信号逻辑名称(如 "PlcLive"、"StartCmd")即可完成数据读写,无需关心物理地址和数据类型转换。
该服务提供三类数据交互方式:
- 自动路由读取(GetValueByName):根据信号 DB 块和地址范围自动选择批量缓存或单点读取
- 队列写入(EnqueueWrite):将写入任务提交到 PlcWriteQueue,后台顺序处理
- 直接写入+回读校验(WriteDirectWithVerify):绕过队列,高优先级写入并回读验证
4.2 注入 ISignalDataService
using XP.Hardware.Plc.Abstractions;
namespace YourNamespace.ViewModels
{
public class YourViewModel : BindableBase
{
private readonly ISignalDataService _signalService;
private readonly ILoggerService _logger;
public YourViewModel(ISignalDataService signalService, ILoggerService logger)
{
_signalService = signalService;
_logger = logger.ForModule<YourViewModel>();
}
}
}
4.3 读取信号(自动路由)| Read Signal (Auto-Routed)
GetValueByName 根据信号的 DBNumber 和 StartAddr 自动选择读取路径:
- 批量缓存路径:
signal.DBNumber == ReadDbBlock 块号且signal.StartAddr在[ReadStartAddress, ReadStartAddress+ReadLength-1]范围内 - 单点读取路径:其他情况(不同 DB 块,或同 DB 块但地址超出缓存范围)
// 非泛型读取,返回 object | Non-generic read, returns object
object value = _signalService.GetValueByName("PlcLive");
// 泛型读取,自动类型转换 | Generic read with auto type conversion
byte plcLive = _signalService.GetValueByName<byte>("PlcLive"); // 批量缓存(在范围内)
bool isReady = _signalService.GetValueByName<bool>("SystemReady"); // 批量缓存(在范围内)
byte scanMode = _signalService.GetValueByName<byte>("ScanMode"); // 单点读取(DB100)
byte softLive = _signalService.GetValueByName<byte>("SoftLive"); // 单点读取(地址 0 不在缓存范围)
注意事项:
- 批量缓存数据来自 100ms 周期读取,存在最大 100ms 延迟
- 单点读取每次发起 PLC 通讯,适合低频或非批量 DB 的信号
- PLC 未连接或缓存未就绪时抛出
PlcException - 单点读取失败时抛出
PlcException(含信号名、DB 块号、错误原因)
4.4 队列写入 | Queue Write
写入地址由信号所属 Group 的 DBNumber 自动拼接,格式为 DB{DBNumber}.{StartAddr}:
// 写入字节值 | Write byte value
bool enqueued = _signalService.EnqueueWrite("SoftLive", (byte)1);
// 写入布尔值 | Write boolean value
bool enqueued2 = _signalService.EnqueueWrite("StartCmd", true);
// 写入整型值 | Write int value
bool enqueued3 = _signalService.EnqueueWrite("TargetPosition", 5000);
// 写入字符串 | Write string
bool enqueued4 = _signalService.EnqueueWrite("DeviceName", "XplorePlane-CT");
返回值说明:
true:成功入队,等待后台处理false:队列未运行或已满
4.5 直接写入+回读校验 | Direct Write with Verify
bool verified = await _signalService.WriteDirectWithVerify("EmergencyStop", true);
if (verified)
{
_logger.Info("紧急停止信号写入成功并已校验 | Emergency stop signal written and verified");
}
else
{
_logger.Error(null, "紧急停止信号写入校验失败 | Emergency stop signal verification failed");
}
5. 初始化流程 | Initialization Flow
public async Task InitializeAsync()
{
// 1. 加载配置 | Load configuration
var config = _configLoader.LoadPlcConfig();
// 2. 连接 PLC(连接成功后自动启动批量读取定时任务)
bool success = await _plcService.InitializeAsync(config);
if (!success) return;
// 3. 加载信号定义文件 | Load signal definition file
// 建立统一信号字典,检查信号名称全局唯一性
_plcService.LoadSignalDefinitions("PlcAddrDfn.xml");
_logger.Info("PLC 初始化完成,信号数据服务就绪 | PLC initialized, signal data service ready");
}
初始化后的自动行为:
- PlcService 以 100ms 周期批量读取
ReadDbBlock指定的 DB 块,更新 Bulk_Read_Cache - 连接监控每 5 秒检查一次,断开时自动重连(需配置
bReConnect=true)
6. 底层直接读写 | Low-Level Direct Read/Write
PlcService 保留底层泛型读写方法,适用于不通过信号定义的场景:
// 泛型读取 | Generic read
bool boolValue = await _plcService.ReadValueAsync<bool>("DB1.0.0");
byte byteValue = await _plcService.ReadValueAsync<byte>("DB1.0");
int intValue = await _plcService.ReadValueAsync<int>("DB1.4");
float floatValue = await _plcService.ReadValueAsync<float>("DB1.8");
// 字符串读写 | String read/write
string strValue = await _plcService.ReadStringAsync("DB1.20", 10);
bool success = await _plcService.WriteStringAsync("DB1.20", "Hello", 10);
// 泛型写入 | Generic write
bool success2 = await _plcService.WriteValueAsync("DB1.4", 12345);
7. PLC 地址格式说明 | PLC Address Format
使用 ISignalDataService 时无需手动拼接地址,地址由 SignalDataService 内部根据信号的 DBNumber、StartAddr、IndexOrLength 自动生成:
| 信号类型 | 生成格式 | 示例 |
|---|---|---|
| bool | DB{DBNumber}.{StartAddr}.{IndexOrLength} |
DB1.221.1 |
| 其他 | DB{DBNumber}.{StartAddr} |
DB31.0 |
底层直接读写时的地址格式:
| 数据类型 | 地址格式 | 示例 |
|---|---|---|
| bool | DB{n}.{byte}.{bit} |
DB1.0.0 |
| byte/short/int/float/double | DB{n}.{byte} |
DB1.4 |
| string | DB{n}.{byte} |
DB1.20 |
8. 异常处理 | Exception Handling
using XP.Hardware.Plc.Exceptions;
try
{
var value = _signalService.GetValueByName<int>("SomeSignal");
}
catch (PlcException ex)
{
_logger.Error(ex, "PLC 操作失败: {Message} | PLC operation failed: {Message}", ex.Message);
}
异常场景汇总 | Exception Scenarios
| 场景 | 方法 | 行为 |
|---|---|---|
| 信号名称不存在 | GetValueByName / EnqueueWrite / WriteDirectWithVerify | 抛出 PlcException |
| 缓存未就绪 | GetValueByName(批量缓存路径) | 抛出 PlcException |
| 单点读取失败 | GetValueByName(单点路径) | 抛出 PlcException(含信号名、DB 块号、错误原因) |
| 类型不兼容 | EnqueueWrite / WriteDirectWithVerify | 抛出 PlcException |
| 信号名称全局重复 | LoadSignalDefinitions | 抛出 PlcException(含重复名称和 Group ID) |
| 写入队列未运行 | EnqueueWrite | 返回 false |
| 直接写入通道未连接 | WriteDirectWithVerify | 返回 false |
| 回读值不一致 | WriteDirectWithVerify | 返回 false |
9. 在应用退出时关闭连接 | Close Connection on Application Exit
protected override void OnExit(ExitEventArgs e)
{
try
{
var plcService = Container.Resolve<PlcService>();
plcService?.Dispose();
}
catch (Exception ex)
{
Log.Error(ex, "PLC 资源释放失败 | Failed to release PLC resources");
}
Log.CloseAndFlush();
base.OnExit(e);
}
10. 自动重连机制 | Auto-Reconnection Mechanism
- 每 5 秒检查一次连接状态
- 检测到断开时自动尝试重连
- 通过配置
bReConnect=true启用 - 重连状态会更新到
StatusText属性(可绑定到 UI)
11. 完整示例 | Complete Example
using Prism.Commands;
using Prism.Mvvm;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XP.Hardware.Plc.Abstractions;
using XP.Hardware.Plc.Exceptions;
using XP.Hardware.Plc.Services;
using XP.Hardware.PLC.Helpers;
namespace YourNamespace.ViewModels
{
public class PlcDemoViewModel : BindableBase
{
private readonly PlcService _plcService;
private readonly ISignalDataService _signalService;
private readonly ConfigLoader _configLoader;
private readonly ILoggerService _logger;
public PlcDemoViewModel(
PlcService plcService,
ISignalDataService signalService,
ConfigLoader configLoader,
ILoggerService logger)
{
_plcService = plcService;
_signalService = signalService;
_configLoader = configLoader;
_logger = logger.ForModule<PlcDemoViewModel>();
}
public async Task InitAsync()
{
var config = _configLoader.LoadPlcConfig();
bool connected = await _plcService.InitializeAsync(config);
if (connected)
{
_plcService.LoadSignalDefinitions("PlcAddrDfn.xml");
}
}
public void ReadSignals()
{
try
{
// PlcLive 在批量读取范围内 → 缓存读取
byte plcLive = _signalService.GetValueByName<byte>("PlcLive");
// ScanMode 在 DB100 → 单点读取
byte scanMode = _signalService.GetValueByName<byte>("ScanMode");
_logger.Info("PlcLive={PlcLive}, ScanMode={ScanMode}", plcLive, scanMode);
}
catch (PlcException ex)
{
_logger.Error(ex, "读取信号失败 | Failed to read signals");
}
}
public void WriteSignal()
{
// 写入地址自动使用 SoftLive 所属 Group 的 DBNumber
bool enqueued = _signalService.EnqueueWrite("SoftLive", (byte)1);
_logger.Debug("SoftLive 入队结果: {Result} | SoftLive enqueue result: {Result}", enqueued);
}
public async Task DirectWriteAsync()
{
bool verified = await _signalService.WriteDirectWithVerify("EmergencyStop", true);
_logger.Info("EmergencyStop 写入校验: {Result} | EmergencyStop write verify: {Result}", verified);
}
}
}
12. 注意事项 | Notes
- 线程安全:PlcService 的 Bulk_Read_Cache 使用 lock 保护,GetCacheSnapshot 返回独立副本
- 异步操作:WriteDirectWithVerify 是异步方法,GetValueByName 和 EnqueueWrite 是同步方法
- 三通道独立:PlcService、PlcWriteQueue、SignalDataService 各持有独立的 IPlcClient 实例
- 信号定义加载:必须在
InitializeAsync成功后调用LoadSignalDefinitions - 缓存延迟:批量缓存路径的数据最大延迟为 100ms;单点读取路径实时但有通讯开销
- WriteDbBlock 已移除:写入地址由信号所属 Group 的 DBNumber 自动决定,无需配置
- 信号名称全局唯一:跨 Group 的信号名称不能重复,否则 LoadSignalDefinitions 抛出异常
- 资源释放:应用退出时调用
PlcService.Dispose()停止定时任务并释放连接 - 跨模块连接状态:其他硬件模块应通过
IPlcService接口查询 PLC 连接状态,而非直接依赖PlcService具体类
13. 故障排查 | Troubleshooting
连接失败
- 检查 IP 地址和端口是否正确
- 确认 PLC 型号配置是否匹配
- 验证 Rack 和 Slot 参数
- 检查网络连接和防火墙设置
GetValueByName 抛出"数据缓存未就绪"
- 确认 PLC 已连接且 InitializeAsync 返回 true
- 等待首次批量读取完成(约 100ms)
- 检查 ReadDbBlock / ReadStartAddress / ReadLength 配置是否正确
GetValueByName 走单点读取但卡住
- 已通过
Task.Run包装解决 UI 线程死锁问题 - 如仍有问题,检查 PLC 连接状态和网络延迟
信号地址超出缓存范围走了单点读取
- 这是正常行为:同 DB 块但地址不在
[ReadStartAddress, ReadStartAddress+ReadLength-1]范围内的信号自动走单点读取 - 如需走缓存,调整 ReadStartAddress 和 ReadLength 使信号地址落在范围内
信号名称未找到
- 确认已调用
LoadSignalDefinitions加载信号定义文件 - 检查 PlcAddrDfn.xml 中是否包含该信号名称(
<Signal Name="...">) - 注意信号名称区分大小写
信号名称重复导致加载失败
- 检查 PlcAddrDfn.xml 中是否有跨 Group 的重名信号
- 信号名称必须在所有 Group 中全局唯一
EnqueueWrite 返回 false
- 检查 PlcWriteQueue 是否已启动(IsRunning)
- 确认队列未满(默认容量 1000)
WriteDirectWithVerify 返回 false
- 检查直接写入通道的连接状态
- 查看日志中的具体错误信息(写入失败 / 回读失败 / 回读值不一致)