在工业自动化领域,WinCC作为西门子旗下的经典SCADA系统,经常需要与Excel进行数据交互。典型的应用场景包括:设备参数查询、工艺配方管理、生产记录存储等。传统实现方式是通过VBA脚本直接操作Excel对象,但这种方案存在明显的性能瓶颈。
我在最近一个汽车零部件生产线的项目中,遇到了这样的典型场景:需要根据PLC实时采集的设备状态码(约每秒2-3次),从包含2000多条记录的Excel表格中查询对应的工艺参数。最初采用的标准VBA方案,每次查询都要经历以下耗时操作:
实测平均每次查询耗时8秒左右,这对于需要实时响应的生产线控制是完全不可接受的。更糟糕的是,当多个查询请求并发时,还会出现Excel进程卡死的情况。
传统方案性能低下的根本原因在于IO瓶颈。每次查询都涉及:
这就像每次查字典都要重新印刷一本新字典,效率可想而知。
经过技术调研,我们评估了三种改进方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 数据库迁移 | 将Excel数据导入SQL Server | 专业数据库性能高 | 实施复杂,需修改现有架构 |
| 文件缓存 | 将Excel转为CSV/TXT读取 | 减少COM开销 | 仍需频繁文件IO |
| 内存驻留 | 使用ADO一次性加载到内存 | 内存操作极快 | 需合理管理内存 |
综合考虑实施成本和效果,我们选择了内存驻留方案,具体采用ADO+全局变量的实现方式。
vbs复制' 推荐使用ACE引擎而非JET,支持新版本Excel
connStr = "Provider=Microsoft.ACE.OLEDB.12.0;" & _
"Data Source=D:\data.xlsx;" & _
"Extended Properties='Excel 12.0;HDR=Yes;IMEX=1'"
' HDR=Yes 表示第一行是列标题
' IMEX=1 强制混合数据转换为文本,避免类型猜测错误
特别提醒:在WinCC环境运行时,需要确保服务器安装了对应的Access Database Engine。建议使用"Excel 97-2003"格式(.xls)以获得最佳兼容性。
vbs复制Dim globalData ' 模块级变量存储数据
Sub InitData()
On Error Resume Next
Set conn = CreateObject("ADODB.Connection")
conn.Open connStr
Set rs = CreateObject("ADODB.Recordset")
' 明确指定列而非SELECT *,减少内存占用
rs.Open "SELECT ID,Param1,Param2 FROM [工艺参数$]", conn
' 关键性能操作:GetRows将记录集转为二维数组
globalData = rs.GetRows()
rs.Close
conn.Close
On Error Goto 0
' 验证数据加载
If Not IsArray(globalData) Then
HMIRuntime.Trace "数据加载失败!错误:" & Err.Description
Else
HMIRuntime.Trace "成功加载 " & UBound(globalData,2)+1 & " 条记录"
End If
End Sub
重要提示:GetRows返回的数组是列优先的[col,row]结构,与常规认知相反。UBound(globalData,2)获取的是行数。
基础版本(线性搜索):
vbs复制Function QueryByID(deviceID)
If Not IsArray(globalData) Then InitData ' 惰性初始化
For row = 0 To UBound(globalData, 2)
If CStr(globalData(0, row)) = CStr(deviceID) Then
QueryByID = globalData(1, row) ' 返回Param1列
Exit Function
End If
Next
QueryByID = "N/A"
HMIRuntime.Trace "未找到设备ID:" & deviceID
End Function
高级版本(字典索引):
vbs复制Dim dict ' 模块级字典对象
Sub BuildIndex()
Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = 1 ' 文本比较模式
For row = 0 To UBound(globalData, 2)
key = CStr(globalData(0, row)) ' 设备ID作为键
If Not dict.Exists(key) Then
dict.Add key, row
End If
Next
End Sub
Function FastQuery(deviceID)
If dict Is Nothing Then BuildIndex
If dict.Exists(CStr(deviceID)) Then
row = dict(CStr(deviceID))
FastQuery = globalData(1, row)
Else
FastQuery = "N/A"
End If
End Function
在2000条记录(20列)的测试环境中:
| 查询方式 | 平均耗时 | 适用场景 |
|---|---|---|
| 传统Excel交互 | 8000ms | 不推荐 |
| 数组线性搜索 | 300ms | 数据变动频繁 |
| 字典索引查询 | <5ms | 数据稳定,查询频繁 |
内存占用方面:
推荐在以下时机调用InitData:
vbs复制Sub SafeInitData()
On Error Resume Next
' 尝试释放现有连接
If IsObject(conn) Then conn.Close
If IsObject(rs) Then rs.Close
' 初始化新连接
Set conn = CreateObject("ADODB.Connection")
conn.Open connStr
If Err.Number <> 0 Then
HMIRuntime.Trace "连接失败:" & Err.Description
Exit Sub
End If
' ...其余初始化代码...
On Error Goto 0
End Sub
常见错误处理场景:
vbs复制Sub CheckMemory()
If IsArray(globalData) Then
HMIRuntime.Trace "当前缓存记录数:" & UBound(globalData,2)+1
End If
End Sub
vbs复制Sub Project_Close()
Set globalData = Nothing
Set dict = Nothing
End Sub
vbs复制Function QueryByMulti(deviceID, productType)
' 构建复合键
compositeKey = CStr(deviceID) & "|" & CStr(productType)
' 预加载索引
If dict Is Nothing Then BuildIndex
' 查询处理
If dict.Exists(compositeKey) Then
row = dict(compositeKey)
result = "温度:" & globalData(2,row) & _
" 压力:" & globalData(3,row)
QueryByMulti = result
End If
End Function
通过FileSystemObject监控文件变更:
vbs复制Dim lastModTime
Sub MonitorExcelFile()
Set fso = CreateObject("Scripting.FileSystemObject")
Set file = fso.GetFile("D:\data.xlsx")
If file.DateLastModified <> lastModTime Then
lastModTime = file.DateLastModified
InitData ' 重新加载数据
BuildIndex
HMIRuntime.Trace "检测到数据变更,已重新加载"
End If
End Sub
在某汽车焊装车间的实施中,我们应用这套方案实现了:
实施效果:
一个实用的调试技巧:在开发阶段添加Trace输出,记录查询过程和结果:
vbs复制HMIRuntime.Trace "查询条件:" & deviceID & _
" 返回结果:" & result & _
" 耗时:" & Timer-startTime & "秒"
这套方案经过多个项目验证,在数据量小于10万条的生产场景中,都能提供令人满意的性能表现。对于更大规模的数据,建议考虑专业的数据库解决方案。