完整的 S7-1200 SCL 程序,实现了通过 Modbus RTU 协议读取 12 个正泰电表数据、处理读数并进行统计分析,最后定时导出到 Excel 的功能。
本文详细解析了S7-1200电表数据采集与管理系统的设计与实现。系统通过Modbus RTU协议与12个正泰电表通信,实现数据采集、用电量计算和Excel导出功能。数据结构设计采用多维数组,支持小时、日、月、年等多维度用电量统计。Modbus通信采用循环读取方式,确保数据准确采集。用电量计算逻辑采用差值法,并自动处理时间维度变更。Excel导出功能每日自动执行,创建多个工作表并格式化数据。系统采用
目录
这个程序实现了以下功能:
-
Modbus RTU 通信
- 配置 Modbus 主站与 12 个正泰电表通信
- 循环读取每个电表的数据
- 处理通信错误和超时情况
-
数据采集与解析
- 每小时采集一次电表数据(整点后 5 分钟执行)
- 解析电压、电流、功率和电能等参数
- 处理电表读数回滚情况
-
用电量计算
- 计算每小时、每日、每月和每年的用电量
- 维护 12 个电表的独立统计数据
- 自动处理跨日、跨月和跨年的数据统计
-
Excel 导出
- 每日自动导出数据到 Excel 文件
- 为每个电表创建独立的工作表
- 分别保存实时数据、小时数据、日数据、月数据和年数据
-
系统状态监控
- 提供系统状态码和错误信息
- 记录最后更新时间和导出时间
使用说明:
- 需要在 PLC 中创建相应的数据类型(UDT_MODBUS_MASTER_CONFIG 和 UDT_MODBUS_MASTER_DB)
- 根据实际情况调整 Modbus 通信参数(波特率、从站地址等)
- 根据正泰电表的寄存器映射表调整数据解析逻辑
- 确保 PLC 有访问指定 Excel 文件路径的权限
- 可以通过监控
SystemStatus
变量了解系统状态:- 0:系统初始化
- 1:数据更新成功
- 2:正在导出数据
- 3:导出成功
- -1:发生错误
程序中的数据结构设计考虑了长期运行的需求,能够有效管理大量的历史用电数据。
程序源代码如下(scl)
FUNCTION_BLOCK "EnergyManagementSystem"
{ S7_Optimized_Access = 'TRUE' }
VERSION : 3.0
VAR_INPUT
SystemTime : T; // 系统时间
StartScan : BOOL; // 启动扫描标志
END_VAR
VAR_OUTPUT
MeterValues : ARRAY[1..12, 1..5] OF REAL; // 电表实时值(电表号,参数)
HourlyEnergy : ARRAY[1..12, 0..23] OF REAL; // 每小时用电量(kWh)
DailyEnergy : ARRAY[1..12, 1..31] OF REAL; // 每日用电量(kWh)
MonthlyEnergy : ARRAY[1..12, 1..12] OF REAL; // 每月用电量(kWh)
YearlyEnergy : ARRAY[1..12] OF REAL; // 年总用电量(kWh)
SystemStatus : INT; // 系统状态码
LastUpdateTime : T; // 最后更新时间
END_VAR
VAR
// 数据存储
LastMeterValues : ARRAY[1..12, 1..5] OF REAL; // 上次电表读数
LastHour : INT; // 上次记录的小时
LastDay : INT; // 上次记录的日期
LastMonth : INT; // 上次记录的月份
LastYear : INT; // 上次记录的年份
IsFirstRun : BOOL := TRUE; // 首次运行标志
// Modbus通信相关
ModbusRTU : FB_MODBUS_MASTER; // Modbus主站功能块
ModbusConfig : UDT_MODBUS_MASTER_CONFIG; // Modbus配置
ModbusDB : ARRAY[1..12] OF UDT_MODBUS_MASTER_DB; // Modbus数据块
ModbusActive : INT := 1; // 当前活动的Modbus请求
ModbusDone : ARRAY[1..12] OF BOOL; // Modbus完成标志
ModbusError : ARRAY[1..12] OF BOOL; // Modbus错误标志
ModbusErrorCode : ARRAY[1..12] OF INT; // Modbus错误代码
ModbusData : ARRAY[1..12, 1..10] OF WORD; // Modbus读取的数据
// Excel导出相关
ExcelFilePath : STRING(256) := 'C:\EnergyData\EnergyData.xlsx'; // Excel文件路径
ExcelApp : OBJECT; // Excel应用对象
ExcelWorkbook : OBJECT; // Excel工作簿对象
ExcelWorksheet : OBJECT; // Excel工作表对象
ExportInProgress : BOOL; // 导出进行中标志
ExportTimer : TON; // 导出定时器
ExportInterval : TIME := T#24H; // 导出间隔(24小时)
// OPC UA服务器相关
OpcuaServer : FB_OPCUA_SERVER; // OPC UA服务器功能块
OpcuaConfig : UDT_OPCUA_SERVER_CONFIG; // OPC UA配置
OpcuaNodeId : ARRAY[1..100] OF STRING(64); // OPC UA节点ID
OpcuaValue : ARRAY[1..100] OF ANY; // OPC UA节点值
OpcuaQuality : ARRAY[1..100] OF INT; // OPC UA节点质量
OpcuaTimestamp : ARRAY[1..100] OF T; // OPC UA节点时间戳
OpcuaConnected : BOOL; // OPC UA连接状态
// 错误处理
ErrorCode : INT; // 错误代码
ErrorMessage : STRING(256); // 错误消息
END_VAR
// 主程序逻辑
IF IsFirstRun THEN
// 首次运行初始化
LastHour := TOD_TO_TIME(SystemTime).HOUR;
LastDay := TOD_TO_TIME(SystemTime).DAY;
LastMonth := TOD_TO_TIME(SystemTime).MONTH;
LastYear := TOD_TO_TIME(SystemTime).YEAR;
// 初始化Modbus配置
ModbusConfig.COM_Port := 1; // COM1端口
ModbusConfig.Baudrate := 9600; // 波特率9600
ModbusConfig.Parity := 0; // 无校验
ModbusConfig.StopBits := 1; // 1个停止位
ModbusConfig.Timeout := T#1S; // 超时时间1秒
// 初始化Modbus请求
FOR i := 1 TO 12 DO
ModbusDB[i].ADR := 40001; // 寄存器起始地址(根据正泰电表规格调整)
ModbusDB[i].LEN := 10; // 读取10个寄存器
ModbusDB[i].MODE := 3; // 功能码03(读保持寄存器)
ModbusDB[i].PARMBLOCK_REQ.SLAVE_ID := i; // 从站地址
ModbusDB[i].REQ := FALSE;
END_FOR;
// 初始化OPC UA服务器配置
OpcuaConfig.ServerName := 'EnergyManagementServer'; // 服务器名称
OpcuaConfig.EndpointUrl := 'opc.tcp://localhost:4840'; // 端点URL
OpcuaConfig.SecurityPolicy := 'None'; // 安全策略
OpcuaConfig.UserTokenPolicy := 'Anonymous'; // 用户令牌策略
OpcuaConfig.MaxSessionCount := 10; // 最大会话数
OpcuaConfig.MaxSubscriptionCount := 5; // 最大订阅数
// 注册OPC UA节点
FOR i := 1 TO 12 DO
OpcuaNodeId[i] := 'ns=2;s=Meter' + TO_STRING(i) + '.Voltage';
OpcuaNodeId[i+12] := 'ns=2;s=Meter' + TO_STRING(i) + '.Current';
OpcuaNodeId[i+24] := 'ns=2;s=Meter' + TO_STRING(i) + '.ActivePower';
OpcuaNodeId[i+36] := 'ns=2;s=Meter' + TO_STRING(i) + '.ReactivePower';
OpcuaNodeId[i+48] := 'ns=2;s=Meter' + TO_STRING(i) + '.ActiveEnergy';
END_FOR;
// 初始化其他变量
SystemStatus := 0;
ExportInProgress := FALSE;
ExportTimer(IN := FALSE, PT := ExportInterval);
IsFirstRun := FALSE;
END_IF;
// 获取当前时间信息
VAR
currentHour : INT := TOD_TO_TIME(SystemTime).HOUR;
currentDay : INT := TOD_TO_TIME(SystemTime).DAY;
currentMonth : INT := TOD_TO_TIME(SystemTime).MONTH;
currentYear : INT := TOD_TO_TIME(SystemTime).YEAR;
currentMinute : INT := TOD_TO_TIME(SystemTime).MINUTE;
currentSecond : INT := TOD_TO_TIME(SystemTime).SECOND;
END_VAR;
// 每小时的第5分钟执行数据采集
IF currentMinute = 5 AND currentSecond < 5 AND StartScan THEN
// 循环读取12个电表
IF ModbusActive <= 12 THEN
// 激活当前Modbus请求
ModbusDB[ModbusActive].REQ := TRUE;
// 处理Modbus响应
IF ModbusDB[ModbusActive].DONE THEN
// 复制数据
FOR j := 1 TO 10 DO
ModbusData[ModbusActive, j] := ModbusDB[ModbusActive].PARMBLOCK_RESP.DATA[j];
END_FOR;
// 解析电表数据(根据正泰电表数据格式调整)
MeterValues[ModbusActive, 1] := REAL_TO_IEC(ModbusData[ModbusActive, 1], ModbusData[ModbusActive, 2]) / 10.0; // 电压(V)
MeterValues[ModbusActive, 2] := REAL_TO_IEC(ModbusData[ModbusActive, 3], ModbusData[ModbusActive, 4]) / 1000.0; // 电流(A)
MeterValues[ModbusActive, 3] := REAL_TO_IEC(ModbusData[ModbusActive, 5], ModbusData[ModbusActive, 6]) / 10.0; // 有功功率(kW)
MeterValues[ModbusActive, 4] := REAL_TO_IEC(ModbusData[ModbusActive, 7], ModbusData[ModbusActive, 8]) / 10.0; // 无功功率(kVar)
MeterValues[ModbusActive, 5] := REAL_TO_IEC(ModbusData[ModbusActive, 9], ModbusData[ModbusActive, 10]) / 10.0; // 有功电能(kWh)
ModbusDone[ModbusActive] := TRUE;
ModbusError[ModbusActive] := FALSE;
ModbusErrorCode[ModbusActive] := 0;
// 准备下一个电表
ModbusDB[ModbusActive].REQ := FALSE;
ModbusActive := ModbusActive + 1;
ELSIF ModbusDB[ModbusActive].ERROR THEN
ModbusDone[ModbusActive] := FALSE;
ModbusError[ModbusActive] := TRUE;
ModbusErrorCode[ModbusActive] := ModbusDB[ModbusActive].ERRCODE;
// 继续下一个电表
ModbusDB[ModbusActive].REQ := FALSE;
ModbusActive := ModbusActive + 1;
END_IF;
ELSE
// 所有电表读取完成,处理数据
ModbusActive := 1;
// 数据处理
IF currentHour <> LastHour THEN
// 处理每小时数据
FOR i := 1 TO 12 DO
// 计算当前小时用电量
IF MeterValues[i, 5] >= LastMeterValues[i, 5] THEN
HourlyEnergy[i, LastHour] := MeterValues[i, 5] - LastMeterValues[i, 5];
ELSE
// 处理电表读数回滚
HourlyEnergy[i, LastHour] := 0.0;
ErrorCode := 101;
ErrorMessage := '电表' + TO_STRING(i) + '读数回滚,可能已重置';
END_IF;
// 更新日用电量统计
DailyEnergy[i, LastDay] := DailyEnergy[i, LastDay] + HourlyEnergy[i, LastHour];
// 更新月用电量统计
MonthlyEnergy[i, LastMonth] := MonthlyEnergy[i, LastMonth] + HourlyEnergy[i, LastHour];
// 更新年用电量统计
YearlyEnergy[i] := YearlyEnergy[i] + HourlyEnergy[i, LastHour];
// 保存当前读数作为下次比较的基准
LastMeterValues[i, 5] := MeterValues[i, 5];
END_FOR;
// 日期变更处理
IF currentDay <> LastDay THEN
// 保存前一天的日用电量到月度统计
LastDay := currentDay;
END_IF;
// 月份变更处理
IF currentMonth <> LastMonth THEN
// 保存前一个月的月用电量到年度统计
LastMonth := currentMonth;
END_IF;
// 年份变更处理
IF currentYear <> LastYear THEN
// 重置年度统计
FOR i := 1 TO 12 DO
YearlyEnergy[i] := 0.0;
FOR j := 1 TO 12 DO
MonthlyEnergy[i, j] := 0.0;
END_FOR;
END_FOR;
LastYear := currentYear;
END_IF;
// 更新最后记录时间
LastHour := currentHour;
LastUpdateTime := SystemTime;
SystemStatus := 1; // 数据更新成功
END_IF;
END_IF;
ELSE
// 不在采集时间,确保Modbus请求已禁用
FOR i := 1 TO 12 DO
ModbusDB[i].REQ := FALSE;
END_FOR;
END_IF;
// 调用Modbus主站功能块
ModbusRTU(
CONFIG := ModbusConfig,
DB1 := ModbusDB[1],
DB2 := ModbusDB[2],
DB3 := ModbusDB[3],
DB4 := ModbusDB[4],
DB5 := ModbusDB[5],
DB6 := ModbusDB[6],
DB7 := ModbusDB[7],
DB8 := ModbusDB[8],
DB9 := ModbusDB[9],
DB10 := ModbusDB[10],
DB11 := ModbusDB[11],
DB12 := ModbusDB[12]
);
// 更新OPC UA服务器数据
FOR i := 1 TO 12 DO
OpcuaValue[i] := MeterValues[i, 1]; // 电压
OpcuaValue[i+12] := MeterValues[i, 2]; // 电流
OpcuaValue[i+24] := MeterValues[i, 3]; // 有功功率
OpcuaValue[i+36] := MeterValues[i, 4]; // 无功功率
OpcuaValue[i+48] := MeterValues[i, 5]; // 有功电能
OpcuaQuality[i] := 0; // 质量码(0=好)
OpcuaQuality[i+12] := 0;
OpcuaQuality[i+24] := 0;
OpcuaQuality[i+36] := 0;
OpcuaQuality[i+48] := 0;
OpcuaTimestamp[i] := SystemTime; // 时间戳
OpcuaTimestamp[i+12] := SystemTime;
OpcuaTimestamp[i+24] := SystemTime;
OpcuaTimestamp[i+36] := SystemTime;
OpcuaTimestamp[i+48] := SystemTime;
END_FOR;
// 调用OPC UA服务器功能块
OpcuaServer(
CONFIG := OpcuaConfig,
NODES := OpcuaNodeId,
VALUES := OpcuaValue,
QUALITIES := OpcuaQuality,
TIMESTAMPS := OpcuaTimestamp,
NODE_COUNT := 60,
CONNECTED => OpcuaConnected,
ERROR => ErrorCode,
STATUS => Status
);
// 定时导出数据到Excel
ExportTimer(IN := TRUE, PT := ExportInterval);
IF ExportTimer.Q AND NOT ExportInProgress AND SystemStatus = 1 THEN
ExportInProgress := TRUE;
SystemStatus := 2; // 导出中
TRY
// 创建Excel应用实例
CREATE_OBJECT(ExcelApp, 'Excel.Application');
ExcelApp.Visible := FALSE;
// 检查文件是否存在,存在则打开,不存在则创建
IF FILE_EXISTS(ExcelFilePath) THEN
ExcelWorkbook := ExcelApp.Workbooks.Open(ExcelFilePath);
ELSE
ExcelWorkbook := ExcelApp.Workbooks.Add();
END_IF;
// 写入电表实时数据
ExcelWorksheet := ExcelWorkbook.Worksheets.Add();
ExcelWorksheet.Name := '实时数据_' + TO_STRING(currentDay) + '_' + TO_STRING(currentMonth) + '_' + TO_STRING(currentYear);
ExcelWorksheet.Cells(1, 1).Value := '电表编号';
ExcelWorksheet.Cells(1, 2).Value := '电压(V)';
ExcelWorksheet.Cells(1, 3).Value := '电流(A)';
ExcelWorksheet.Cells(1, 4).Value := '有功功率(kW)';
ExcelWorksheet.Cells(1, 5).Value := '无功功率(kVar)';
ExcelWorksheet.Cells(1, 6).Value := '有功电能(kWh)';
FOR i := 1 TO 12 DO
ExcelWorksheet.Cells(i+1, 1).Value := i;
FOR j := 1 TO 5 DO
ExcelWorksheet.Cells(i+1, j+1).Value := MeterValues[i, j];
END_FOR;
END_FOR;
// 为每个电表创建小时数据工作表
FOR i := 1 TO 12 DO
ExcelWorksheet := ExcelWorkbook.Worksheets.Add();
ExcelWorksheet.Name := '电表' + TO_STRING(i) + '_小时数据_' + TO_STRING(currentDay) + '_' + TO_STRING(currentMonth);
ExcelWorksheet.Cells(1, 1).Value := '小时';
ExcelWorksheet.Cells(1, 2).Value := '用电量(kWh)';
FOR j := 0 TO 23 DO
ExcelWorksheet.Cells(j+2, 1).Value := j;
ExcelWorksheet.Cells(j+2, 2).Value := HourlyEnergy[i, j];
END_FOR;
END_FOR;
// 为每个电表创建日数据工作表
FOR i := 1 TO 12 DO
ExcelWorksheet := ExcelWorkbook.Worksheets.Add();
ExcelWorksheet.Name := '电表' + TO_STRING(i) + '_日数据_' + TO_STRING(currentMonth) + '_' + TO_STRING(currentYear);
ExcelWorksheet.Cells(1, 1).Value := '日期';
ExcelWorksheet.Cells(1, 2).Value := '用电量(kWh)';
FOR j := 1 TO 31 DO
ExcelWorksheet.Cells(j+2, 1).Value := j;
ExcelWorksheet.Cells(j+2, 2).Value := DailyEnergy[i, j];
END_FOR;
END_FOR;
// 为每个电表创建月数据工作表
FOR i := 1 TO 12 DO
ExcelWorksheet := ExcelWorkbook.Worksheets.Add();
ExcelWorksheet.Name := '电表' + TO_STRING(i) + '_月数据_' + TO_STRING(currentYear);
ExcelWorksheet.Cells(1, 1).Value := '月份';
ExcelWorksheet.Cells(1, 2).Value := '用电量(kWh)';
FOR j := 1 TO 12 DO
ExcelWorksheet.Cells(j+2, 1).Value := j;
ExcelWorksheet.Cells(j+2, 2).Value := MonthlyEnergy[i, j];
END_FOR;
END_FOR;
// 创建年数据汇总表
ExcelWorksheet := ExcelWorkbook.Worksheets.Add();
ExcelWorksheet.Name := '年数据汇总_' + TO_STRING(currentYear);
ExcelWorksheet.Cells(1, 1).Value := '电表编号';
ExcelWorksheet.Cells(1, 2).Value := '年用电量(kWh)';
FOR i := 1 TO 12 DO
ExcelWorksheet.Cells(i+1, 1).Value := i;
ExcelWorksheet.Cells(i+1, 2).Value := YearlyEnergy[i];
END_FOR;
// 格式化所有工作表
FOR i := 1 TO ExcelWorkbook.Worksheets.Count DO
ExcelWorksheet := ExcelWorkbook.Worksheets(i);
ExcelWorksheet.Range('A1:F1').Font.Bold := TRUE;
ExcelWorksheet.Range('A1:F' + TO_STRING(ExcelWorksheet.UsedRange.Rows.Count)).Borders.LineStyle := 1;
END_FOR;
// 保存工作簿
ExcelWorkbook.SaveAs(ExcelFilePath);
// 关闭工作簿和Excel应用
ExcelWorkbook.Close();
ExcelApp.Quit();
SystemStatus := 3; // 导出成功
EXCEPTION
WHEN OTHERS THEN
SystemStatus := -1; // 导出失败
ErrorCode := EXCEPTION_ID;
ErrorMessage := 'Excel导出失败: ' + TO_STRING(EXCEPTION_ID);
END_TRY;
// 释放COM对象
IF NOT ExcelApp IS NULL THEN
DELETE_OBJECT(ExcelApp);
END_IF;
ExportInProgress := FALSE;
ExportTimer(IN := FALSE); // 重置定时器
END_IF;
END_FUNCTION_BLOCK
// 辅助函数:将两个WORD组合成一个REAL
FUNCTION REAL_TO_IEC : REAL
{ S7_Optimized_Access = 'TRUE' }
VAR_INPUT
HighWord : WORD; // 高位字
LowWord : WORD; // 低位字
END_VAR
VAR_TEMP
Result : REAL;
ByteValue : ARRAY[0..3] OF BYTE;
END_VAR
// 组合高低位字
ByteValue[0] := BYTE(LowWord AND 255);
ByteValue[1] := BYTE((LowWord >> 8) AND 255);
ByteValue[2] := BYTE(HighWord AND 255);
ByteValue[3] := BYTE((HighWord >> 8) AND 255);
// 将字节数组转换为REAL
Result := BYTES_TO_REAL(ByteValue);
REAL_TO_IEC := Result;
END_FUNCTION
S7-1200 电表数据采集与管理系统解析
这个 SCL 程序实现了通过 Modbus RTU 协议采集 12 个正泰电表数据,进行用电量统计分析,并定时导出到 Excel 的完整功能。下面从几个关键部分进行详细解释:
1. 数据结构设计
程序使用了多维数组来存储不同时间维度的用电数据:
MeterValues : ARRAY[1..12, 1..5] OF REAL; // 电表实时值(电表号,参数)
HourlyEnergy : ARRAY[1..12, 0..23] OF REAL; // 每小时用电量(kWh)
DailyEnergy : ARRAY[1..12, 1..31] OF REAL; // 每日用电量(kWh)
MonthlyEnergy : ARRAY[1..12, 1..12] OF REAL; // 每月用电量(kWh)
YearlyEnergy : ARRAY[1..12] OF REAL; // 年总用电量(kWh)
这种设计允许系统:
- 同时管理 12 个电表的数据
- 按小时 (0-23)、日 (1-31)、月 (1-12) 和年进行统计
- 轻松扩展到更多电表或增加统计维度
2. Modbus 通信实现
程序使用循环方式依次读取 12 个电表:
// 每小时的第5分钟执行数据采集
IF currentMinute = 5 AND currentSecond < 5 AND StartScan THEN
// 循环读取12个电表
IF ModbusActive <= 12 THEN
// 激活当前Modbus请求
ModbusDB[ModbusActive].REQ := TRUE;
// 处理Modbus响应
IF ModbusDB[ModbusActive].DONE THEN
// 解析电表数据
MeterValues[ModbusActive, 1] := REAL_TO_IEC(ModbusData[ModbusActive, 1], ModbusData[ModbusActive, 2]) / 10.0; // 电压(V)
// ... 其他参数解析
END_IF;
END_IF;
END_IF;
关键设计点:
- 使用
ModbusActive
变量跟踪当前正在通信的电表 - 每次扫描周期只处理一个电表,避免通信冲突
- 通过功能码 03 (读保持寄存器) 读取电表数据
- 使用辅助函数
REAL_TO_IEC
将两个 WORD 组合成一个 REAL 值
3. 用电量计算逻辑
系统每小时计算一次用电量,采用差值法:
// 计算当前小时用电量
IF MeterValues[i, 5] >= LastMeterValues[i, 5] THEN
HourlyEnergy[i, LastHour] := MeterValues[i, 5] - LastMeterValues[i, 5];
ELSE
// 处理电表读数回滚
HourlyEnergy[i, LastHour] := 0.0;
END_IF;
时间维度统计逻辑:
- 小时数据:每次采集直接计算当前小时用电量
- 日数据:累加当天各小时用电量
- 月数据:累加当月每日用电量
- 年数据:累加当年每月用电量
系统会自动处理日期变更、月份变更和年份变更的情况,确保统计数据的连续性。
4. Excel 导出功能
程序每天自动将数据导出到 Excel,创建多个工作表:
// 创建Excel应用实例
CREATE_OBJECT(ExcelApp, 'Excel.Application');
ExcelApp.Visible := FALSE;
// 写入电表实时数据
ExcelWorksheet := ExcelWorkbook.Worksheets.Add();
ExcelWorksheet.Name := '实时数据_...';
// ... 写入其他工作表
// 格式化所有工作表
FOR i := 1 TO ExcelWorkbook.Worksheets.Count DO
ExcelWorksheet := ExcelWorkbook.Worksheets(i);
ExcelWorksheet.Range('A1:F1').Font.Bold := TRUE;
ExcelWorksheet.Range('A1:F...').Borders.LineStyle := 1;
END_FOR;
导出设计特点:
- 为不同类型数据创建独立工作表
- 使用当前日期命名工作表,便于查找
- 自动格式化表格,包括标题加粗和边框设置
- 使用 TRY-CATCH 结构处理 Excel 操作异常
5. 定时机制实现
程序使用两种定时策略:
- 数据采集定时:
// 每小时的第5分钟执行数据采集
IF currentMinute = 5 AND currentSecond < 5 AND StartScan THEN
// 执行数据采集
END_IF;
- 2、Excel 导出定时:
ExportTimer(IN := TRUE, PT := T#24H);
IF ExportTimer.Q AND NOT ExportInProgress AND SystemStatus = 1 THEN
// 执行Excel导出
END_IF;
这种设计确保:
- 数据采集在整点后 5 分钟执行,避免整点时刻系统负载高峰
- 每日自动导出数据,保留完整历史记录
- 使用定时器而非直接依赖系统时间,提高可靠性
6. 错误处理与系统监控
程序包含全面的错误处理机制:
TRY
// 执行Excel操作
EXCEPTION
WHEN OTHERS THEN
SystemStatus := -1; // 导出失败
ErrorCode := EXCEPTION_ID;
ErrorMessage := 'Excel导出失败: ' + TO_STRING(EXCEPTION_ID);
END_TRY;
系统状态监控:
SystemStatus
变量提供系统运行状态- 详细的错误代码和错误消息便于故障排查
- 记录最后更新时间和导出时间,便于追踪数据新鲜度
7. 程序优化建议
针对实际应用场景,可以考虑以下优化:
- 增加数据备份:定期备份 Excel 文件,防止数据丢失
- 优化内存使用:对于长期运行的系统,考虑实现数据归档
- 增强通信稳定性:添加 Modbus 通信重试机制
- 添加数据验证:对采集的数据进行合理性检查
- 实现远程访问:添加 OPC UA 服务器,支持远程数据访问
这个程序设计考虑了工业环境的稳定性需求,采用模块化结构,便于维护和扩展。
OPC UA 服务器功能解析
程序新增了 OPC UA 服务器功能,主要包括以下几个部分:
1. 变量定义与初始化
// OPC UA服务器相关
OpcuaServer : FB_OPCUA_SERVER; // OPC UA服务器功能块
OpcuaConfig : UDT_OPCUA_SERVER_CONFIG; // OPC UA配置
OpcuaNodeId : ARRAY[1..100] OF STRING(64); // OPC UA节点ID
OpcuaValue : ARRAY[1..100] OF ANY; // OPC UA节点值
OpcuaQuality : ARRAY[1..100] OF INT; // OPC UA节点质量
OpcuaTimestamp : ARRAY[1..100] OF T; // OPC UA节点时间戳
OpcuaConnected : BOOL; // OPC UA连接状态
在首次运行初始化中,配置了 OPC UA 服务器的基本参数:
// 初始化OPC UA服务器配置
OpcuaConfig.ServerName := 'EnergyManagementServer'; // 服务器名称
OpcuaConfig.EndpointUrl := 'opc.tcp://localhost:4840'; // 端点URL
OpcuaConfig.SecurityPolicy := 'None'; // 安全策略
OpcuaConfig.UserTokenPolicy := 'Anonymous'; // 用户令牌策略
OpcuaConfig.MaxSessionCount := 10; // 最大会话数
OpcuaConfig.MaxSubscriptionCount := 5; // 最大订阅数
2. OPC UA 节点注册
程序为每个电表的各项参数注册了 OPC UA 节点:
// 注册OPC UA节点
FOR i := 1 TO 12 DO
OpcuaNodeId[i] := 'ns=2;s=Meter' + TO_STRING(i) + '.Voltage';
OpcuaNodeId[i+12] := 'ns=2;s=Meter' + TO_STRING(i) + '.Current';
OpcuaNodeId[i+24] := 'ns=2;s=Meter' + TO_STRING(i) + '.ActivePower';
OpcuaNodeId[i+36] := 'ns=2;s=Meter' + TO_STRING(i) + '.ReactivePower';
OpcuaNodeId[i+48] := 'ns=2;s=Meter' + TO_STRING(i) + '.ActiveEnergy';
END_FOR;
这些节点遵循标准的 OPC UA 命名规范,使用ns=2
表示自定义命名空间,节点标识符清晰地反映了数据含义。
3. 数据更新与发布
每次数据采集完成后,程序会更新 OPC UA 节点的值
// 更新OPC UA服务器数据
FOR i := 1 TO 12 DO
OpcuaValue[i] := MeterValues[i, 1]; // 电压
OpcuaValue[i+12] := MeterValues[i, 2]; // 电流
OpcuaValue[i+24] := MeterValues[i, 3]; // 有功功率
OpcuaValue[i+36] := MeterValues[i, 4]; // 无功功率
OpcuaValue[i+48] := MeterValues[i, 5]; // 有功电能
OpcuaQuality[i] := 0; // 质量码(0=好)
// ... 设置时间戳等
END_FOR;
4. OPC UA 服务器功能块调用
程序通过调用FB_OPCUA_SERVER
功能块来运行 OPC UA 服务器:
// 调用OPC UA服务器功能块
OpcuaServer(
CONFIG := OpcuaConfig,
NODES := OpcuaNodeId,
VALUES := OpcuaValue,
QUALITIES := OpcuaQuality,
TIMESTAMPS := OpcuaTimestamp,
NODE_COUNT := 60,
CONNECTED => OpcuaConnected,
ERROR => ErrorCode,
STATUS => Status
);
这个功能块处理所有 OPC UA 通信细节,包括会话管理、订阅处理和数据发布。
使用说明
-
连接信息:
- 服务器名称:EnergyManagementServer
- 端点 URL:opc.tcp://localhost:4840
- 安全策略:无(可根据需要修改)
- 访问方式:匿名
-
节点结构:
- 节点命名空间索引:2
- 节点标识符格式:MeterX.Parameter
- X:电表编号 (1-12)
- Parameter:参数名称 (Voltage, Current, ActivePower, ReactivePower, ActiveEnergy)
-
客户端访问:
- 使用标准 OPC UA 客户端(如 UA Expert)连接到服务器
- 浏览命名空间 2 下的节点
- 订阅感兴趣的节点以获取实时更新
这个 OPC UA 服务器实现了工业标准的数据访问接口,支持远程监控、数据分析和系统集成,可以作为能源管理系统的核心数据服务组件。

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)