去年年初我接手了一条汽车零部件产线的信息化改造:上位机是西门子WinCC 7.5,现场128个变量已经稳定跑了一年多。客户的需求其实不复杂——每批产品都要有一张配方报表,包含配方名称、参数设定值、实际执行值、起止时间,并且能按批次号追溯。麻烦的是,客户的生产管理系统用的是SQL Server,MES那边也指定要用数据库对接,而WinCC自带的配方组件虽然能存配方,但要让它自动生成报表、把数据导给MES,就很别扭。传统做法往往是去改WinCC的C脚本,甚至有人会在WinCC运行时内部数据库的业务表里做文章,一旦系统升级,项目就崩。我这次换了个思路:底层源码不动,只把变量导入和配方报表生成放到运行层的VBS脚本和SQL Server里完成。整个方案上线后稳定运行到现在,这篇就把实现过程、表结构设计、脚本模板和踩坑记录完整复盘一遍。
1. 为什么我把配方数据从WinCC内部组件搬进SQL Server
1.1 传统配方管理在“单机生产”场景够用,但一涉及追溯就力不从心
WinCC自带的配方组件(Recipe)确实能完成配方的新建、下载、上载,配合PLC做参数切换也很方便。但这类组件有一个天然定位:它是给“单机操作员”用的。操作员在WinCC画面上选一个配方,点下载,PLC参数就变了。这套闭环里没有“历史追溯”的环节。
客户这次的需求是两个维度:一是每批产品完成后,要能输出一张包含所有参数设定值和执行值的报表;二是这批数据要能跟MES系统的批次号对上,后续要做SPC分析、不良追溯。这些需求靠WinCC自带配方组件很难直接满足——你可以在画面上把数据导成CSV,但没人想每天手动导一次,更不要说用报表工具自动拉数据了。所以把配方数据落到独立的SQL Server业务库里,是最自然的选择。
1.2 SQL方案适合哪类产线,我说清楚适用边界
不是所有WinCC项目都需要上SQL。如果项目只是单机设备、配方数量少、又不需要跟任何系统对接,只靠WinCC内部配方组件就够了。我判断一个项目要不要上SQL方案,主要看三点:
第一,是否有跨系统数据需求。比如MES、ERP要取配方数据,或者工厂层面要做统一报表平台,那么数据库是唯一能被多个系统同时访问的接口。
第二,追溯周期是否跨批次。现场往往要求“三个月后还能查到某批产品的设定值”,这种要求用WinCC内部归档去做,查询性能和文件管理都不方便,而数据库天然适合长期归档。
第三,配方参数是否已经很多。当配方数量超过几百个、且每个配方包含几十个变量时,靠WinCC画面手动翻页管理已经低效,SQL查询、批量导入导出都是明显优势。
这套方案我建议在WinCC 7.0及以上版本使用,因为VBS脚本引擎在这些版本里已经非常稳定。如果是WinCC Flexible时代的老项目,脚本能力会受限,不太适合直接套用。
1.3 为什么这套方案能“不动源码”
这里先说清楚一个常见误解。标题说的“无需修改源码”,不是指完全不碰任何代码,而是指不需要改动WinCC项目的底层实现——比如项目原有的C脚本、画面逻辑所用到的全局函数、内部归档配置等。WinCC工程的底层源码是西门子的框架代码加上工程师在项目里编写的C函数,这部分修改起来风险很大。任何一个C脚本的改动,都可能要重新编译、重新下载工程,甚至在项目运行状态中引起不可预期的问题。
我采用的方案,是在WinCC运行系统里增加VBS脚本和独立的SQL Server业务库。VBS脚本在WinCC里是应用层的脚本,不依赖项目底层框架;SQL Server业务库也是独立于WinCC运行时数据库的存在,不碰西门子自带的系统表。业务逻辑全部外置,原来的画面逻辑、变量归档机制一概不动。这样做最大的好处是:以后项目升级、WinCC补丁更新、甚至整站迁移,新方案都不会对旧工程产生负面影响。
2. 数据模型设计:配方表该怎么建,变量字段怎么映射
2.1 配方头、配方明细、生产批次三张核心表
我设计数据库时,没有把配方数据塞到一张大宽表里,而是拆成了三张表:配方头表(RecipeHeader)、配方明细表(RecipeDetail)、批次历史表(BatchHistory)。这个拆分逻辑是每个做数据库的人都会告诉你的事:配方模板和实际生产记录是两种不同的东西,放进同一张表很快就会被需求逼着返工。
配方头表保存的是“一个配方是谁”,比如配方名称、产品编码、创建时间。配方明细表保存的是“一个配方里有哪些参数和值”,比如变量名、设定值、单位、顺序号。批次历史表保存的是“某一次实际生产用了哪个配方、什么时间段、结果如何”。三张表之间通过RecipeID和BatchNo关联。
建表语句我直接给出来,字段我都简化过,便于理解:
CREATE TABLE RecipeHeader ( RecipeID int IDENTITY(1,1) PRIMARY KEY, RecipeName nvarchar(64) NOT NULL, ProductCode nvarchar(32) NOT NULL, CreateTime datetime NOT NULL DEFAULT GETDATE(), Status tinyint NOT NULL DEFAULT 0 ); CREATE TABLE RecipeDetail ( ID int IDENTITY(1,1) PRIMARY KEY, RecipeID int NOT NULL, TagName nvarchar(64) NOT NULL, SetValue real NULL, ActualValue real NULL, Unit nvarchar(16) NULL, SortOrder int NOT NULL ); CREATE TABLE BatchHistory ( ID int IDENTITY(1,1) PRIMARY KEY, BatchNo nvarchar(32) NOT NULL, RecipeID int NOT NULL, ProductCode nvarchar(32) NOT NULL, StartTime datetime NOT NULL, EndTime datetime NULL, Qty real NULL, Result nvarchar(16) NULL );这里有个细节:RecipeDetail表的ActualValue字段,是用来存生产过程中回读的实际执行值的。很多做配方系统的人只存设定值,但客户要追溯的时候一定会问“当时设定的是100,实际执行到99.8,到底算不算达标?”所以从设计第一天起就把设定值和实际值分开存,后面做报表、做SPC分析都轻松很多。
2.2 变量映射规则:配置表比硬编码脚本更可靠
WinCC变量名通常长得像“Recipe_Temp_Set”、“PLC_Pressure_Actual”这种。如果把这些变量名一条条写死在VBS脚本里,脚本会变得极其冗长,而且以后现场加一个变量,还得打开脚本改一行代码,这种做法显然不叫“自动化”。
我的做法是在SQL Server里建一张变量映射配置表,脚本启动时先读这张表,再根据表里的变量列表去读取WinCC标签。映射表大概长这样:
CREATE TABLE TagMapping ( TagID int IDENTITY(1,1) PRIMARY KEY, TagName nvarchar(64) NOT NULL, DataType nvarchar(16) NOT NULL DEFAULT 'real', DisplayName nvarchar(64) NULL, IsActive bit NOT NULL DEFAULT 1 );脚本运行时执行一条查询就能拿到所有需要导入的变量清单:
SELECT TagName, DataType FROM TagMapping WHERE IsActive = 1 ORDER BY SortOrder以后需要增加变量,只需要往这张表里插入一条记录,完全不需要再碰脚本代码。这个设计让“无需修改源码”的效果达到最大——变量增减都只改数据库配置,源码层面零改动。
2.3 时间戳与主键:报表追溯的命根子
做配方报表最容易忽略的是时间戳的设计。WinCC变量读出来的值和记录发生的时间必须一一对应。我强烈建议每条写入数据库的记录都包含两个时间概念:一个是WinCC侧的时间标签,一个是数据库服务器的写入时间。这样可以排查是采集延迟还是数据库本身的问题。
批次号是另一个需要提前统一的字段。MES、ERP、设备层往往对批次号的命名规则理解不一致。我在方案里要求客户提供批次号的生成规则,通常由PLC通过变量给到WinCC,或者由MES下发。批次号是BatchHistory表的主键逻辑标识,我在建表时没有把它设成自增主键,而是用自增ID做物理主键,BatchNo作为逻辑主键并加上唯一索引:
CREATE UNIQUE INDEX UX_BatchNo ON BatchHistory(BatchNo);这样做的原因是批次号有业务含义,而ID仅供关联使用。如果你把BatchNo设成物理主键,一旦客户说“批次号规则要改”,你就要动主键,那是灾难级的返工。
3. 变量导入SQL Server:WinCC侧脚本到底怎么写
3.1 连接SQL Server的脚本模板
WinCC的VBS脚本可以通过ADODB操作任何支持OLEDB的数据库。我这里用的连接字符串是SQLOLEDB.1,如果你服务器上装了SQL Server 2012以上版本,也可以换成SQLNCLI11或者MSOLEDBSQL,后者更稳定一些。连接参数里尤其注意Timeout要设置,否则数据库临时故障时脚本会卡死。
Dim conn Set conn = CreateObject("ADODB.Connection") conn.ConnectionString = "Provider=SQLOLEDB.1;Data Source=192.168.1.20;Initial Catalog=RecipeDB;User ID=wincc_rw;Password=YourPassword;Timeout=5" conn.Open连接字符串里的User ID不建议用sa。数据库账号要单独建一个具备读写业务库权限的账号,这样即使脚本被误改,也不会威胁到WinCC系统库和SQL Server其他库。这个权限划分在我后来排查问题时帮了大忙——可以快速定位是权限问题还是脚本问题。
3.2 变量读取与写入的整段VBS脚本逻辑
核心脚本的逻辑分三步:从配置表读取变量清单、循环读取WinCC标签值、批量写入SQL Server。下面是我项目里简化后的可运行模板:
Dim conn, rs, cmd, tag, value Dim tagName, dataType, strSQL, recipeId, batchNo Set conn = CreateObject("ADODB.Connection") conn.ConnectionString = "Provider=SQLOLEDB.1;Data Source=192.168.1.20;Initial Catalog=RecipeDB;User ID=wincc_rw;Password=YourPassword;Timeout=5" conn.Open ' 读取变量映射配置 Set rs = CreateObject("ADODB.Recordset") rs.Open "SELECT TagName, DataType FROM TagMapping WHERE IsActive=1", conn, 1, 1 ' 读取当前配方ID和批次号(这两个变量由PLC/MES下发) recipeId = HMIRuntime.Tags("Recipe_CurrentID").Read batchNo = HMIRuntime.Tags("Batch_CurrentNo").Read ' 循环每个映射变量 Do While Not rs.EOF tagName = rs.Fields("TagName").Value dataType = rs.Fields("DataType").Value Set tag = HMIRuntime.Tags(tagName) value = tag.Read ' 使用参数化方式写入,避免引号问题和类型错误 Set cmd = CreateObject("ADODB.Command") cmd.ActiveConnection = conn cmd.CommandText = "INSERT INTO RecipeDetail(RecipeID, TagName, SetValue) VALUES (?,?,?)" cmd.Parameters.Append cmd.CreateParameter("RecipeID", 3, 1, , recipeId) cmd.Parameters.Append cmd.CreateParameter("TagName", 200, 1, 64, tagName) cmd.Parameters.Append cmd.CreateParameter("SetValue", 4, 1, , CDbl(value)) cmd.Execute rs.MoveNext Loop rs.Close conn.Close这段代码里最关键的一个设计是用ADODB.Command参数化写入,而不是拼接字符串。很多人图省事会写成"INSERT INTO ... VALUES ('" & tagName & "'," & value & ")",看起来没问题,实际上一旦变量的值是字符串且带有单引号,或者某个数值变量读取结果是空值,脚本就会报错。使用参数化后,数据类型的转换和特殊字符的问题都由数据库驱动层处理。
3.3 触发方式和“不碰底层源码”的边界
WinCC里执行VBS脚本的触发机制有三种方式:全局脚本周期触发、变量触发、画面按钮事件调用。我建议根据使用场景组合使用:生产开始时,把配方设定值写入数据库,适合用PLC的状态字触发脚本;生产过程中回读实际值,适合用周期定时扫描;报表生成则用画面按钮或批次结束信号触发。
重点说一说环境安全性。以前很多工程师喜欢直接在WinCC运行系统的C脚本里操作数据库,一旦写了内存分配不当的代码,会导致整个WinCC Runtime崩溃。VBS脚本虽然性能比C脚本低,但它是解释型语言,有脚本引擎保护,不会直接操作内存,也不存在编译后无法回退的问题。我的项目在实施过程中经历了WinCC补丁升级,升级期间旧工程没有任何改动,VBS脚本和SQL脚本在重启后照常运行。这正是“应用层旁路”结构的好处:业务逻辑挂在边上,底层架构怎么变都影响不到它。
4. 配方报表自动生成:从数据库到Excel/HTML的落地
4.1 报表数据查询逻辑:SQL负责汇总,脚本只负责展示
报表的核心数据查询,我的经验是尽量把汇总计算放在SQL里完成,而不要在VBS里写循环求和。这样做的好处是性能和可维护性都更好。下面这条查询是报表的主查询:
SELECT H.RecipeName, H.ProductCode, B.BatchNo, B.StartTime, B.EndTime, D.TagName, D.SetValue, D.ActualValue, D.Unit FROM BatchHistory B INNER JOIN RecipeHeader H ON B.RecipeID = H.RecipeID INNER JOIN RecipeDetail D ON B.RecipeID = D.RecipeID WHERE B.BatchNo = 'B20240218001' ORDER BY D.SortOrder实际项目中,报表经常要统计一批产品的平均值、最大值、最小值,以及是否超差。这些统计字段用SQL的聚合函数处理非常方便:
SELECT TagName, MAX(ActualValue) AS MaxValue, MIN(ActualValue) AS MinValue, AVG(ActualValue) AS AvgValue, COUNT(*) AS SampleCount FROM RecipeDetail WHERE BatchNo = 'B20240218001' GROUP BY TagName脚本需要做的,只是把这批结果填充到Excel的表格里。
4.2 通过VBS把查询结果导出到Excel
工厂环境下客户端一般安装了Office,所以Excel导出是通用做法。VBS操作Excel的最简模板如下:
Dim xl, wb, ws, rs, row, col Set rs = CreateObject("ADODB.Recordset") rs.Open "SELECT ... FROM ... WHERE BatchNo='B20240218001'", conn, 1, 1 Set xl = CreateObject("Excel.Application") xl.Visible = False xl.DisplayAlerts = False Set wb = xl.Workbooks.Add() Set ws = wb.Worksheets(1) ws.Cells(1,1) = "配方名称" ws.Cells(1,2) = "变量名" ws.Cells(1,3) = "设定值" ws.Cells(1,4) = "实际值" ws.Cells(1,5) = "单位" row = 2 Do While Not rs.EOF ws.Cells(row,1) = rs.Fields("RecipeName").Value ws.Cells(row,2) = rs.Fields("TagName").Value ws.Cells(row,3) = rs.Fields("SetValue").Value ws.Cells(row,4) = rs.Fields("ActualValue").Value ws.Cells(row,5) = rs.Fields("Unit").Value rs.MoveNext row = row + 1 Loop wb.SaveAs "D:\RecipeReports\" & batchNo & ".xlsx", 51 wb.Close xl.Quit Set wb = Nothing Set xl = Nothing代码里SaveAs的第二个参数51表示xlsx格式,这个参数值容易记错,写xls是56,写xlsx是51。我实际项目中有一台服务器只有WPS没有Office,换成WPS后兼容性波动,后来干脆在另一台没有办公软件的服务器上改用HTML报表方案了。
4.3 没有Office的机器怎么办:HTML报表方案
如果报表服务器没有安装Office,Excel导出方案会直接失败。我后来在集团标准化项目中改用HTML报表,效果好很多。逻辑也很简单:SQL查询结果拼接成一张HTML表格,加上一些基础的排版样式,保存成.html文件,任何人都能用浏览器打开、打印成PDF。
Dim fso, outFile, html Set fso = CreateObject("Scripting.FileSystemObject") Set outFile = fso.CreateTextFile("D:\RecipeReports\" & batchNo & ".html", True, True) html = "<html><body>" html = html & "<h2>配方报表 " & batchNo & "</h2>" html = html & "<table border='1' cellspacing='0' cellpadding='4'>" html = html & "<tr><th>配方名称</th><th>变量名</th><th>设定值</th><th>实际值</th><th>单位</th></tr>" Do While Not rs.EOF html = html & "<tr>" html = html & "<td>" & rs.Fields("RecipeName").Value & "</td>" html = html & "<td>" & rs.Fields("TagName").Value & "</td>" html = html & "<td>" & rs.Fields("SetValue").Value & "</td>" html = html & "<td>" & rs.Fields("ActualValue").Value & "</td>" html = html & "<td>" & rs.Fields("Unit").Value & "</td>" html = html & "</tr>" rs.MoveNext Loop html = html & "</table></body></html>" outFile.Write html outFile.Close这个方案有一个隐性好处——浏览器对报表的渲染是客户端自己负责的,不需要服务器装任何组件,也不会出现Excel进程残留问题。
5. 128个变量的实测表现,和几个必须避开的坑
5.1 实测数据:别让脚本把WinCC运行时拖垮
先给出一组我在WinCC 7.5 SP2、客户端/服务器架构下的实测数据:128个变量,配方导入频率为每次生产开始一次性写入;生产过程中每5秒扫描一次实际值并更新到RecipeDetail表。整个过程中,WinCC Runtime的CPU占用率增加约2%到3%,内存增加约30MB。这个增量对于现代工控机来说完全可以接受。
但是如果你想把变量采集周期缩短到100毫秒级,比如当成历史趋势曲线采集来用,这套方案就会出问题。VBS脚本和ADODB逐行写入的开销远大于C脚本的批量写入,100毫秒频率下CPU占用会飙升,甚至会导WinCC画面操作变卡。我建议用这套方案时,写入频率不要超过1秒一次;确实需要高频采样的点位,应该用WinCC自带的变量归档功能,然后再从归档表读取,而不是直接用VBS高频写SQL。
5.2 踩坑一:连接字符串选错驱动,报“未找到提供程序”
这个问题在新电脑部署环境时经常遇到。SQLOLEDB.1是Windows自带的旧驱动,一般都有;但如果你用了SQLNCLI11,目标机器就必须安装SQL Server Native Client,否则会报“未找到提供程序”。而且Native Client的版本必须和连接字符串严格对应,装错版本同样连不上。
我的建议是:开发环境用什么驱动,部署环境必须保持一致,并且把驱动安装包放在部署文档里。不要以为局域网里都是SQL Server就不用装驱动,很多瘦客户端上只有最基础的.NET框架,没有OLEDB驱动。
5.3 踩坑二:业务表建到了WinCC系统库里
WinCC安装时会自带一个SQL Server实例,很多工程师觉得“反正是同一台服务器的SQL Server,就直接把业务表建在WinCC的项目库里”。这是我见过风险最高的做法。
WinCC系统库的结构是西门子自己维护的,里面全是系统视图和存储过程。你往里面建自己的表,一时半会没问题,但WinCC补丁升级、项目复制、系统自检都可能因为它找不到预期的库结构而出错。我们自己建库时一定要用SQL Server管理工具新建独立的数据库,例如RecipeDB,并给出独立的账号和权限。
5.4 踩坑三:VBS的Now和SQL Server的GETDATE()对不上
WinCC脚本里用Now取到的是操作系统的本地时间,SQL Server记录的时间取决于数据库服务器的设置。如果两台机器时区不一致,或者Windows时间同步没配好,报表里的开始时间就会错位。
我最终的解法是统一以数据库服务器的GETDATE()为唯一时间来源:写入时不在脚本里传时间,而是调用SQL语句INSERT ... , GETDATE()。这样,无论WinCC客户端在哪台电脑上、系统时间准不准,最终归档的时间都跟着数据库走,报表口径一致了。
5.5 踩坑四:Excel导出后EXCEL.EXE进程杀不掉
VBS操作Excel后如果脚本中途报错,往往会在后台留下一堆EXCEL.EXE进程,时间长了服务器内存被吃光。我后来在脚本里加了一个保护性的进程处理:在脚本开头用WMI查找并清理上次残留的Excel进程。这个操作要谨慎,只能清自己脚本创建的实例,不能把用户在桌面上打开的Excel也关了。
更稳妥的做法是调整脚本里所有可能提前退出的路径,确保无论正常结束还是异常结束,都执行wb.Close、xl.Quit和Set obj = Nothing。我习惯把On Error Resume Next和结束清理组合起来用,也比让脚本直接中断好得多。
6. 我在这套方案上的一些经验体会
6.1 技术选型的核心不是炫技,是降低后续维护风险
这套方案运行一年半,中间经历了产线扩建、点位从128增加到160、WinCC从7.5升级到8.1。因为所有业务逻辑都在应用层的VBS脚本和SQL Server里,这些变动都没有影响到原工程,只改动了映射表配置和SQL查询条件。这让我更确信:在工控项目里,技术方案好不好,要看将来别人接手时能不能看懂、能不能维护,而不是看当初写的时候有多高级。
6.2 实际改动需要沿用的几个小习惯
我现在做的每个类似项目都会固化成三件事:第一,变量映射表必须在数据库里留字段说明备注,谁改谁写清原因;第二,SQL脚本统一放到版本管理目录,哪怕现场没有用Git,至少要有带日期的备份文件夹;第三,每个报表输出目录按批次号自动建子文件夹,避免所有文件堆在一个目录里,后面想找历史报表会极其痛苦。
6.3 接下去还能扩展的方向
这套数据库结构本身是开放的,往上有MES/ERP对接,往下可以接Kepware采集的非PLC数据,横向还能做历史趋势分析。我目前就在把历史趋势曲线脚本也改为从SQL Server取数,统一数据口径。如果你也在规划类似改造,建议一开始就留好扩展字段,比如产品编码、质量判定结果,后续加需求时就不用大改表结构。最后提醒一句:报表页面尽量别绑定WinCC自带控件,用可替换的HTML和Excel方式,以后换报表工具时你会感谢自己当初没写死。