目录

S7-1200 电表数据采集与管理系统解析

1. 数据结构设计

2. Modbus 通信实现

3. 用电量计算逻辑

4. Excel 导出功能

5. 定时机制实现

6. 错误处理与系统监控

7. 程序优化建议

OPC UA 服务器功能解析

1. 变量定义与初始化

2. OPC UA 节点注册

3. 数据更新与发布

4. OPC UA 服务器功能块调用

使用说明


这个程序实现了以下功能:

  1. Modbus RTU 通信

    • 配置 Modbus 主站与 12 个正泰电表通信
    • 循环读取每个电表的数据
    • 处理通信错误和超时情况
  2. 数据采集与解析

    • 每小时采集一次电表数据(整点后 5 分钟执行)
    • 解析电压、电流、功率和电能等参数
    • 处理电表读数回滚情况
  3. 用电量计算

    • 计算每小时、每日、每月和每年的用电量
    • 维护 12 个电表的独立统计数据
    • 自动处理跨日、跨月和跨年的数据统计
  4. Excel 导出

    • 每日自动导出数据到 Excel 文件
    • 为每个电表创建独立的工作表
    • 分别保存实时数据、小时数据、日数据、月数据和年数据
  5. 系统状态监控

    • 提供系统状态码和错误信息
    • 记录最后更新时间和导出时间

使用说明:

  1. 需要在 PLC 中创建相应的数据类型(UDT_MODBUS_MASTER_CONFIG 和 UDT_MODBUS_MASTER_DB)
  2. 根据实际情况调整 Modbus 通信参数(波特率、从站地址等)
  3. 根据正泰电表的寄存器映射表调整数据解析逻辑
  4. 确保 PLC 有访问指定 Excel 文件路径的权限
  5. 可以通过监控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. 定时机制实现

程序使用两种定时策略:

  1. 数据采集定时: 
// 每小时的第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. 程序优化建议

针对实际应用场景,可以考虑以下优化:

  1. 增加数据备份:定期备份 Excel 文件,防止数据丢失
  2. 优化内存使用:对于长期运行的系统,考虑实现数据归档
  3. 增强通信稳定性:添加 Modbus 通信重试机制
  4. 添加数据验证:对采集的数据进行合理性检查
  5. 实现远程访问:添加 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 通信细节,包括会话管理、订阅处理和数据发布。

使用说明

  1. 连接信息

    • 服务器名称:EnergyManagementServer
    • 端点 URL:opc.tcp://localhost:4840
    • 安全策略:无(可根据需要修改)
    • 访问方式:匿名
  2. 节点结构

    • 节点命名空间索引:2
    • 节点标识符格式:MeterX.Parameter
      • X:电表编号 (1-12)
      • Parameter:参数名称 (Voltage, Current, ActivePower, ReactivePower, ActiveEnergy)
  3. 客户端访问

    • 使用标准 OPC UA 客户端(如 UA Expert)连接到服务器
    • 浏览命名空间 2 下的节点
    • 订阅感兴趣的节点以获取实时更新

这个 OPC UA 服务器实现了工业标准的数据访问接口,支持远程监控、数据分析和系统集成,可以作为能源管理系统的核心数据服务组件。

Logo

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

更多推荐