ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

WinCC用户归档实战:从配方批次追溯到SQL直连的完整案例

WinCC用户归档实战:从配方批次追溯到SQL直连的完整案例

简介:这份资源是面向工业自动化工程师与WinCC学习者的用户归档实践案例包,聚焦西门子SCADA系统中动作与标准模块的协同机制,帮助解决实时数据存储、历史数据检索与归档策略设计等实际问题。压缩包共290个文件,约36.21MB,以rpl归档数据文件为主,辅以mdf、ldf数据库文件、dum转储文件、sav与ini配置、pdl画面、xml与cfg配置及多种脚本模块文件,覆盖从归档数据到项目配置的完整结构。已有1150人学习下载,适合需要动手搭建归档系统的初、中级技术人员参考。资源内含示例项目文件、脚本代码与步骤指南,读者可据此理解动作触发归档、标准模块完成数据过滤与压缩的实现方式,并学习归档间隔、保留期限与存储介质的选择思路,提升WinCC项目中的数据管理效率与故障排查能力。

1. 从一次配方批次追溯翻车说起:wincc 用户归档案例到底解决什么问题

去年帮一家做精细化工的客户排查问题,他们的操作工在 HMI 上改了反应釜的保温时长,第二天质检发现整批料不合格,但谁也说不清当时到底是谁、在几点、把哪个参数从多少改成了多少。趋势曲线只记录了过程值,操作动作和配方参数根本没进历史库。这就是典型的「数据在跑,但没留痕」。wincc 用户归档案例要解决的,正是这类场景:把配方、批次、操作记录、质检结果这些结构化数据,按自定义字段存进 SQL Server,再在画面里做查询、导出和报表。它和 wincc 自带的历史趋势曲线是两套东西——趋势曲线存的是过程值,用户归档存的是你定义字段的业务数据。适合谁看?做流程行业上位机、需要做批次追溯和电子批记录的工程师,尤其是被 GMP 或客户审核逼着要「谁在什么时候改了什么」的那批人。下面按我实际落地的顺序,从建归档、写脚本、做查询一路讲到踩过的坑。

2. 用户归档的底层逻辑与建库前的三个选型决定

2.1 用户归档和趋势曲线、报警记录的本质区别

很多人第一次接触 wincc 用户归档,会以为它是趋势曲线的升级版,其实两者底层存储和访问方式完全不同。趋势曲线走的是 WinCC 的归档系统,数据点按时间戳压缩存储,查询靠控件;用户归档走的是独立的 SQL Server 数据库,每条记录是你自己定义的字段组合,本质是一张可读写的业务表。这意味着用户归档能做趋势曲线做不了的事:存字符串、存批次号、存操作员姓名、存配方版本,还能在运行时用脚本往里面插记录、改记录、删记录。

从架构上看,WinCC 用户归档由三部分组成:归档组态(定义字段和类型)、运行时数据库(SQL Server 里的实际表)、以及访问接口(WinCC 脚本或控件)。归档组态在 WinCC 项目管理器里完成,运行时数据库由 WinCC 自动在 SQL Server 里创建,访问接口则是你写脚本的地方。理解这三层,后面排查问题就不会乱——数据没进去,先看组态字段对不对,再看脚本有没有执行,最后看数据库连接通不通。

还有一个容易混淆的点:用户归档的「归档」二字容易让人以为只能追加不能修改。实际上用户归档支持运行时写入、更新和删除,这恰恰是它适合做配方管理和批次记录的原因。趋势曲线的数据你基本不会去改,但配方参数、批次状态这些是需要随流程更新的。

2.2 字段设计:哪些该进用户归档,哪些不该

字段设计是用户归档案例里最容易埋雷的地方。我见过有人把整个配方几十个参数全塞进一张归档表,结果查询慢、维护难、导出报表列多得没法看。我的经验是按业务对象拆表,而不是按画面拆表。

具体来说,配方管理建一张表,字段包括配方编号、配方名称、版本号、创建人、创建时间、审核状态;批次记录建另一张表,字段包括批次号、配方编号、开始时间、结束时间、操作员、设备号、质检结果。两张表通过配方编号关联。这样查询「某批次用了哪个版本的配方」就是一句关联查询的事。

字段类型选择上,WinCC 用户归档支持有限的数据类型:数值、字符串、日期时间、布尔。注意字符串字段要设长度,默认长度可能不够存中文名称。日期时间字段建议统一用 WinCC 的日期时间格式,不要用字符串存时间,否则后面做时间范围查询会非常痛苦。

提示:字段名不要用中文,也不要用 SQL 保留字(如 order、group、desc),否则脚本访问时会出现莫名其妙的语法错误。

2.3 建归档组态的最小步骤

在 WinCC 项目管理器里,用户归档位于「用户归档」编辑器下。右键新建归档,命名建议用英文加下划线,比如 recipe_master、batch_record。新建后先定义字段,字段顺序会影响脚本里按索引访问的结果,所以建议把关键字段放在前面。

建完字段后,需要设置归档的属性:最大记录数、是否循环覆盖、是否允许运行时修改。最大记录数根据你的数据量和 SQL Server 空间来定,批次记录这种只增不改的,建议设大一点并关闭循环覆盖,避免历史数据被冲掉。配方表这种记录数少的,可以设小一点。

组态完成后,WinCC 会在 SQL Server 里生成对应的表。你可以在 SQL Server Management Studio 里看到这些表,表名通常带前缀。确认表生成成功,是后面脚本能跑通的前提。

3. 用 C 脚本和 VBS 把数据写进用户归档

3.1 两种脚本接口的选择与最小写入示例

WinCC 提供两套脚本访问用户归档:C 脚本(ANSI-C)和 VBS。C 脚本性能好、执行稳定,适合高频写入;VBS 语法友好、调试方便,适合逻辑复杂的场景。我一般这样分:配方保存、批次开始结束这种关键动作,用 C 脚本;查询、导出、界面交互,用 VBS。

先看 C 脚本写入的最小例子。假设已经建好归档 recipe_master,字段依次是 recipe_id、recipe_name、version、create_time。

// C脚本:向用户归档 recipe_master 插入一条配方记录 // 触发点:画面按钮的鼠标点击事件 #include "apdefap.h" void OnClick(char* lpszPictureName, char* lpszObjectName, char* lpszPropertyName) { // 打开用户归档,返回归档句柄 long hArchive = UA_OpenArchive("recipe_master"); if (hArchive <= 0) { // 打开失败,写诊断消息 printf("open archive failed\r\n"); return; } // 创建一条新记录 long hRecord = UA_CreateRecord(hArchive); if (hRecord <= 0) { UA_CloseArchive(hArchive); return; } // 按字段索引写值,索引从0开始 UA_SetFieldValue(hRecord, 0, "RCP-001"); // recipe_id UA_SetFieldValue(hRecord, 1, "反应釜A标准配方"); // recipe_name UA_SetFieldValue(hRecord, 2, 1); // version UA_SetFieldValue(hRecord, 3, GetLocalTime()); // create_time // 提交记录,真正写入数据库 UA_WriteRecord(hRecord); // 释放句柄 UA_CloseRecord(hRecord); UA_CloseArchive(hArchive); }

这段代码的逻辑是:打开归档、创建记录、按索引填字段、提交、释放。关键参数说明:UA_OpenArchive 的参数是归档名,必须和组态里完全一致;UA_SetFieldValue 的第二个参数是字段索引,从 0 开始,顺序就是组态里字段的顺序;UA_WriteRecord 才是真正落库的动作,前面只是构造记录。如果只调 UA_SetFieldValue 不调 UA_WriteRecord,数据不会进数据库,这是新手最常见的翻车点。

3.2 VBS 查询与更新:按批次号取记录

查询场景用 VBS 更顺手。下面这个例子是按批次号查询记录,并把结果写到画面上的 MSFlexGrid 或列表控件里。

' VBS脚本:按批次号查询用户归档 batch_record ' 触发点:查询按钮的鼠标点击事件 Dim hArchive, hRecord, hQuery Dim batchNo, result batchNo = HMIRuntime.Tags("QueryBatchNo").Read ' 从画面输入框读批次号 hArchive = HMIRuntime.UserArchive.Open("batch_record") If hArchive <= 0 Then HMIRuntime.Trace "open archive failed" & vbCrLf Exit Sub End If ' 构造查询条件:字段索引0是批次号 hQuery = HMIRuntime.UserArchive.Query(hArchive, 0, batchNo) If hQuery <= 0 Then HMIRuntime.Trace "no record found" & vbCrLf HMIRuntime.UserArchive.Close hArchive Exit Sub End If ' 遍历查询结果 Do While HMIRuntime.UserArchive.MoveNext(hQuery) > 0 result = HMIRuntime.UserArchive.GetFieldValue(hQuery, 0) & " | " & _ HMIRuntime.UserArchive.GetFieldValue(hQuery, 1) & " | " & _ HMIRuntime.UserArchive.GetFieldValue(hQuery, 2) HMIRuntime.Trace result & vbCrLf Loop HMIRuntime.UserArchive.CloseQuery hQuery HMIRuntime.UserArchive.Close hArchive

逻辑说明:先打开归档,用 Query 方法按字段索引和值构造查询,返回查询句柄,然后 MoveNext 遍历结果集,GetFieldValue 按索引取值。参数上,Query 的第二个参数是字段索引,第三个是匹配值,只支持等值匹配,不支持模糊查询。如果需要模糊查询或范围查询,得用 SQL 直接访问数据库,或者把数据取出来在脚本里过滤。

更新记录用 UA_UpdateRecord 或 VBS 对应的 Update 方法,思路是先查询定位到记录,改字段值,再提交。注意更新前要确保记录句柄有效,否则会静默失败。

3.3 脚本触发时机与性能注意点

脚本触发时机直接决定数据完整性。配方保存按钮用鼠标点击触发没问题,但批次开始结束这种跟设备状态绑定的,建议用变量触发或全局脚本周期执行,避免操作工忘记点按钮导致记录缺失。

性能上,C 脚本单次写入通常在毫秒级,但如果你在循环里连续写几百条,建议批量构造后一次性提交,或者用事务。VBS 查询大量记录时,MoveNext 遍历会明显变慢,超过一千条建议直接走 SQL 查询。另外,脚本里打开归档后一定要关闭,否则句柄泄漏会导致后续操作失败,这个坑我在现场踩过,表现是运行几小时后所有归档操作都无响应。

4. 把用户归档接到画面查询、报表导出和批次追溯

4.1 用 WinCC 控件做查询界面的最小配置

WinCC 自带用户归档控件(User Archive Control),可以直接拖到画面上做查询和显示,不用写太多脚本。配置步骤:在画面里插入控件,设置归档名,配置要显示的列(对应字段),然后绑定查询条件变量。控件支持运行时输入查询值、翻页、导出 CSV。

但自带控件样式比较固定,客户往往要求自定义界面。我的做法是用 MSFlexGrid 或 ListView 控件配合 VBS 脚本,自己控制显示逻辑。这样界面自由度大,但需要自己处理分页和排序。分页思路是查询时只取当前页需要的记录,用 SQL 的 TOP 和 OFFSET,或者把结果集缓存在脚本变量里。

注意:用户归档控件在 Web 发布或 WinCC 冗余环境下行为可能不一致,如果项目涉及这些场景,建议提前验证。

4.2 导出 Excel 报表的脚本实现

报表导出是用户归档案例里客户最常提的需求。下面用 VBS 调用 Excel COM 对象,把查询结果写进 Excel 并保存。

' VBS脚本:把用户归档查询结果导出到Excel Dim hArchive, hQuery, xlApp, xlBook, xlSheet, rowIdx hArchive = HMIRuntime.UserArchive.Open("batch_record") hQuery = HMIRuntime.UserArchive.Query(hArchive, 0, HMIRuntime.Tags("QueryBatchNo").Read) Set xlApp = CreateObject("Excel.Application") Set xlBook = xlApp.Workbooks.Add Set xlSheet = xlBook.Worksheets(1) ' 写表头 xlSheet.Cells(1,1) = "批次号" xlSheet.Cells(1,2) = "配方编号" xlSheet.Cells(1,3) = "操作员" xlSheet.Cells(1,4) = "开始时间" rowIdx = 2 Do While HMIRuntime.UserArchive.MoveNext(hQuery) > 0 xlSheet.Cells(rowIdx,1) = HMIRuntime.UserArchive.GetFieldValue(hQuery, 0) xlSheet.Cells(rowIdx,2) = HMIRuntime.UserArchive.GetFieldValue(hQuery, 1) xlSheet.Cells(rowIdx,3) = HMIRuntime.UserArchive.GetFieldValue(hQuery, 2) xlSheet.Cells(rowIdx,4) = HMIRuntime.UserArchive.GetFieldValue(hQuery, 3) rowIdx = rowIdx + 1 Loop xlBook.SaveAs "D:\Reports\batch_" & HMIRuntime.Tags("QueryBatchNo").Read & ".xlsx" xlBook.Close xlApp.Quit Set xlSheet = Nothing Set xlBook = Nothing Set xlApp = Nothing

逻辑说明:创建 Excel 应用对象,新建工作簿,逐行写入查询结果,最后按批次号命名保存。参数上,SaveAs 的路径要确保 WinCC 运行账户有写权限,否则会报权限错误。另外 Excel COM 调用比较慢,记录多时建议改用 CSV 直接写文件,速度快且不依赖 Office 安装。

4.3 批次追溯的完整数据链路

批次追溯的完整链路是:批次开始时写入批次记录(批次号、配方编号、开始时间、操作员),过程中把关键工艺参数写入用户归档或趋势归档,批次结束时更新批次记录的结束时间和质检结果。查询时通过批次号关联配方表和批次表,还原出「谁、在什么时间、用什么配方、做了什么、结果如何」。

这条链路里最容易断的是过程中参数记录。如果工艺参数只进了趋势归档,追溯时要把趋势数据和批次时间范围做关联查询,比较麻烦。我的建议是关键工艺参数同时写一份到用户归档,字段包括批次号、参数名、参数值、时间戳,这样追溯时一张表就能查全。

5. 用户归档避坑与排查:五条血泪经验

5.1 现象:脚本执行了但数据库里没数据

原因:最常见的是只调了 UA_SetFieldValue 没调 UA_WriteRecord,记录只在内存里构造,没提交。其次是归档名拼写和组态不一致,UA_OpenArchive 返回失败但脚本没做错误处理,静默跳过。还有一种情况是字段索引越界,比如组态只有 4 个字段,脚本写了索引 5,写入被拒绝。

解决:在脚本里对每一步的返回值做判断,失败时写诊断消息到文件或报警。归档名建议从组态里复制,不要手敲。字段索引和组态顺序核对一遍,建议在脚本开头用注释标明索引对应关系。

5.2 现象:查询返回空结果但数据库里明明有数据

原因:查询条件字段索引写错了,或者查询值类型不匹配。比如批次号字段是字符串,查询时传了数值,等值匹配就失败。还有一种情况是查询值带了空格或不可见字符,从画面输入框读出来没做 Trim。

解决:先确认字段索引,再确认类型。字符串查询值统一做 Trim 处理。如果还是查不到,直接在 SQL Server 里用 SELECT 语句查同一条件,确认数据确实存在且值一致。

5.3 现象:运行几小时后归档操作全部失败

原因:句柄泄漏。脚本里打开归档和查询后没有关闭,句柄耗尽后后续操作全部失败。这个问题的隐蔽性在于前期一切正常,运行时间长了才暴露。

解决:所有 Open 操作必须有对应的 Close,建议用 On Error Resume Next 配合错误处理,确保异常时也能释放。C 脚本里 UA_CloseArchive 和 UA_CloseRecord 都要调,VBS 里 CloseQuery 和 Close 都不能漏。

5.4 现象:中文配方名写入后显示乱码

原因:WinCC 用户归档的字符串字段编码和脚本传入的编码不一致,或者字段长度不够导致截断。C 脚本里字符串默认是 ANSI 编码,如果组态字段是 Unicode,需要转换。

解决:字段长度设够,中文名称建议至少 64 字符。C 脚本里用 MultiByteToWideChar 转换编码,或者直接用 VBS 写入,VBS 字符串默认 Unicode,兼容性更好。

5.5 现象:导出 Excel 时报权限错误或文件被占用

原因:WinCC 运行账户对目标路径没有写权限,或者上一次导出的 Excel 进程没退出,文件被锁。Excel COM 调用后如果没正确 Quit,进程会残留在后台。

解决:导出路径选 WinCC 运行账户有写权限的目录,比如项目目录下的 Reports 文件夹。每次导出前检查是否有残留的 Excel 进程,脚本里确保 Quit 和释放对象。如果还是不稳定,改用 CSV 导出,不依赖 Office。

6. 进阶:用 SQL 直连做复杂查询和归档表结构优化

用户归档自带的查询接口只支持等值匹配,做范围查询、模糊查询、多表关联时很受限。我的做法是复杂查询直接走 SQL Server。WinCC 用户归档的表在 SQL Server 里是普通表,表名通常是 UA 加归档名,字段名对应组态里的字段名。你可以用 ADO 或 SQL 脚本直接查。

-- 查询某时间段内所有不合格批次的配方使用情况 SELECT b.batch_no, b.operator, r.recipe_name, r.version, b.qc_result FROM UA_batch_record b LEFT JOIN UA_recipe_master r ON b.recipe_id = r.recipe_id WHERE b.start_time BETWEEN '2024-01-01' AND '2024-01-31' AND b.qc_result = 'NG' ORDER BY b.start_time DESC;

这条 SQL 做了两件事:按时间范围过滤批次,关联配方表取配方名称和版本。参数上,时间范围用字符串格式传入,SQL Server 会自动转换;关联字段 recipe_id 在两张表里类型要一致,否则关联会隐式转换导致性能下降。

表结构优化上,建议给常用查询字段建索引,比如批次号、开始时间、配方编号。索引能显著提升查询速度,但会稍微拖慢写入,批次记录这种写入频率不高的场景,建索引是划算的。另外,用户归档表的数据量大了之后,建议定期归档旧数据到历史表,保持主表轻量。

我自己的习惯是:项目交付前一定用 SQL 直连做一次全量数据核对,确认用户归档里的记录数和业务预期一致,字段值没有截断或乱码。这个动作帮我提前发现过好几次字段长度不够的问题。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进