做非标设备这些年,我有个特别深的体会:如果你们公司的产品线有七八种机型,那程序管理基本就是个灾难。新机型来了,直接复制老项目的程序,改改IO映射,加一段新逻辑,再改改触摸屏的变量。半年之后,手头会攒出七八个版本高度相似但细节互不相通的程序。更要命的是,每改一个bug,都得把这个修改同步到所有机型的工程里——漏改一个版本,现场就会出问题。这事我吃过不少亏,后来痛定思痛,花了几周时间把所有机型程序重构了一遍,最后形成了这么个架构:用ST语言(结构化文本)写一个设备模板FB(功能块),通过配置数据区分机型差异,一套代码覆盖了8种不同机型。今天就把这个架构的设计思路、接口抽象方法、完整实操步骤和踩过的坑一次性讲清楚。
本文适合谁看?如果你的工作是PLC编程(尤其是西门子、倍福、博世力士乐这类支持IEC 61131-3标准、可以用ST语言编程的平台),如果你的项目里也有多机型、多版本、程序复制粘贴越来越难维护的问题,那这篇文章基本能给你一套可落地的重构方案。纯电工接线、不碰程序的朋友可能看起来会有点吃力,但理解了核心思路——"把差异从代码里剥离开,变成数据",对你做电气标准化设计也会有启发。
1. 多机型设备程序的设计困境与破局思路
1.1 为什么程序会越写越乱:版本分叉的根源
先说一个很典型的场景。公司有一款基础设备,客户A要的是2个伺服轴的版本,客户B要3个轴外加一个贴标机构,客户C又要4个轴但是不要贴标、要加一个视觉检测。每个销售订单提回来,工程师的第一反应都是"新建一个项目文件夹,复制上一个项目的代码,开改"。
这种开发方式最直接的问题就是版本分叉。两个项目一开始可能只有5%的差异,但改着改着就变成30%、50%的差异。改到后面,没有人能准确说出来"3轴版本的程序里到底有没有包含2轴版本的某个bug修复"。
复制粘贴式开发还会带来一个更隐蔽的问题:程序与硬件绑定太死。IO变量是直接写死在逻辑里的,轴号也是写死的,安全联锁逻辑跟具体接线地址绑在一起。一旦某个机型换了PLC的IO模块型号或者轴控模块从1个变成3个,程序内部就得跟着改一堆地址映射,这种修改极其容易出错。
我做过的那个项目,8种机型的差异点特别典型:
| 机型 | 轴数量 | 有无贴标机构 | 有无视觉检测 | 最大速度范围 | 工艺顺序差异 |
|---|---|---|---|---|---|
| 机型A | 2轴 | 无 | 无 | 1200rpm | 常规 |
| 机型B | 2轴 | 有 | 无 | 1500rpm | 先贴标后定位 |
| 机型C | 3轴 | 无 | 无 | 1200rpm | 常规 |
| 机型D | 3轴 | 有 | 有 | 1800rpm | 先检测后贴标 |
| 机型E | 4轴 | 无 | 有 | 1500rpm | 先定位后检测 |
| 机型F | 4轴 | 有 | 无 | 1000rpm | 先贴标后定位 |
| 机型G | 5轴 | 有 | 有 | 1200rpm | 先检测后定位后贴标 |
| 机型H | 5轴 | 无 | 有 | 1800rpm | 常规 |
你看,轴数从2到5不等,功能模块有4种组合,速度上限还各不相同。如果按照传统做法,这就是至少4套主程序、每套程序还要再分支出不同的工艺顺序——代码量爆炸不说,维护的人绝对会疯掉。
1.2 核心思路:从复制粘贴到一套模板加差异配置
破局思路其实就一句话:把设备当成同一种东西的变体,通过数据配置来描述差异,而不是通过代码分支来描述差异。
这句话怎么理解?举个例子。一台设备的工艺逻辑,无非就是启动、停止、回原点、自动运行、报警处理、安全联锁。2轴版本和5轴版本在这套逻辑上的区别是什么?不是逻辑本身不同,而是"数据"不同——谁参与运行、谁需要同步、谁先动谁后动,这些是配置数据决定的。2轴版本配置两套轴的数据,5轴版本配置五套轴的数据,但驱动它们运行的框架是完全一样的。
这就是模板化FB功能块的核心哲学:公共逻辑只写一遍,差异全部往外抽,只留下一个统一的数据接口,用配置填平所有差别。
我最后落地的架构长这样:
- 一个统一的设备模板FB:
FB_DeviceTemplate,内部包含主状态机、轴控逻辑、IO映射、报警处理框架。 - 一套配置数据结构:
ST_DeviceConfig,包含轴数量、有无贴标、有无视觉、速度上限、工艺顺序等全部差异点。 - 一个调用框架:在主程序中用一个循环或一组调用,把8个机型的配置数据分别绑定到8个FB实例上。
这样做的好处非常直接——以后不管是新增一个机型,还是修改某个机型的工艺逻辑,都只动配置数据或者扩展模板的内部能力,完全不需要再复制整个程序。
2. 设备模板FB的架构设计与接口抽象
2.1 定义通用设备接口:输入输出参数的抽象方法
做模板FB的头号难点是接口怎么设计。接口定小了,某机型的需求覆盖不了;接口定大了,每个实例都得绑一堆没用的管脚,程序看着都累。我的原则是:接口只面向设备逻辑需求,不面向电气接线差异。
先看我最终定义的FB接口:
FUNCTION_BLOCK FB_DeviceTemplate VAR_INPUT // 控制命令 bCmdStart : BOOL; // 启动命令 bCmdStop : BOOL; // 停止命令 bCmdReset : BOOL; // 故障复位命令 bCmdHome : BOOL; // 回原点命令 eModeSel : INT; // 工作模式选择(手动/自动/调试) // 工艺参数(实际运行时由HMI写入) rSpeedSet : REAL; // 速度设定值 rAccSet : REAL; // 加速度设定值 rPosOffset : REAL; // 位置偏移 // 配置绑定(关键输入!) pstConfig : ST_DeviceConfig; // 本实例的机型配置结构体 END_VAR VAR_OUTPUT // 状态反馈 bRunning : BOOL; // 运行中 bHomed : BOOL; // 已回原点 bError : BOOL; // 有故障 wErrorCode : WORD; // 故障代码 iCurStep : INT; // 当前状态机步骤号(用于诊断) // 统计信息 ulProdCount : ULINT; // 生产计数 // 轴状态数组(当前实例实际包含的轴状态) arrAxisState : ARRAY[1..MAX_AXIS] OF ST_AxisInfo; END_VAR VAR_IN_OUT // IO映射(与硬件接线层交互的数据结构,由上层程序填充) iobIO : ST_DeviceIO; axisIF : ST_AxisInterface; END_VAR有几个接口设计细节值得拎出来说。
控制命令不以沿信号做输入。我见过很多人把bStart做成上升沿触发,结果HMI上的启动按钮如果按下时间太短,PLC没扫描到就丢命令了。我的做法是让FB内部的逻辑自己做上升沿检测,这样外部调用方只要给电瓶信号就行,抗丢信号能力强很多。
配置结构体直接作为输入参数传递。这是整个架构的核心。FB内部不允许出现"机型A怎么走、机型B怎么走"的硬编码分支,所有行为都由pstConfig里的数据决定。这样做有一个额外的好处:你可以给同一个机型定义多种配置,比如"机型A-高速版"和"机型A-普通版",不用动程序。
IN_OUT区放IO映射和轴接口。这是为了把"设备逻辑层"和"IO接线层"彻底分开。设备逻辑层不关心IO模块具体在哪个地址,它只关心抽象的"启动按钮信号""气缸伸出到位信号""伺服使能信号"这些逻辑量。真正去读物理地址、处理硬件映射的活,放在上层OB或专门的功能里完成,然后把结果填到iobIO结构体里传给FB。
2.2 机型差异点清单:识别8种机型的关键差异
接口设计之前,先得有一份扎实的差异点清单。我当时把8种机型的技术协议全部摊在桌上,一个功能一个功能地过,最后归纳出五个维度的差异:
- IO点数差异:轴多的机型,伺服使能、到位、报警IO自然就多。这个差异用数组处理。
- 功能模块差异:有贴标机构和无贴标机构的机型,控制逻辑不同。这个差异用布尔标志位处理。
- 轴控参数差异:速度上限、加速度上限、回零方向、软限位位置不同。这个差异直接用参数配置。
- 工艺顺序差异:比如3轴版本要先贴标再定位,5轴版本要先检测后定位再贴标。这个差异最复杂,我最后用"工艺步骤表"来配置。
- 安全联锁差异:有视觉检测的机型,视觉信号必须参与联锁;没有的机型,该联锁直接旁路。
这五个维度的差异,全部落到ST_DeviceConfig里:
TYPE ST_DeviceConfig : STRUCT // 基本信息 wDevType : WORD; // 机型编号(1~8) strDevName : STRING(20); // 机型名称,用于诊断显示 // 轴配置 iAxisCount : INT; // 实际轴数量(2~5) arrAxisCfg : ARRAY[1..MAX_AXIS] OF ST_AxisConfig; // 每个轴的独立配置 // 功能模块配置 bHasLabeler : BOOL; // 有无贴标机构 bHasVision : BOOL; // 有无视觉检测 // 工艺参数 rMaxSpeed : REAL; // 速度上限(rpm或mm/s) rMaxAcc : REAL; // 加速度上限 rMaxPos : REAL; // 正方向软限位 rMinPos : REAL; // 负方向软限位 // 工艺步骤表:最多16步 iStepCnt : INT; // 工艺步骤数量 arrSteps : ARRAY[1..16] OF ST_StepItem; // 每个步骤的类型、轴号、目标值 // 联锁配置 bVisionInterlock : BOOL; // 视觉信号是否参与安全联锁 END_STRUCT END_TYPE TYPE ST_AxisConfig : STRUCT iAxisNo : INT; // 硬件轴编号,对应轴控模块上的通道号 eHomeMode : INT; // 回零方式:0=当前位置, 1=负限位, 2=正限位, 3=原点开关 rHomePos : REAL; // 回零完成后的目标坐标 bEnable : BOOL; // 该轴是否参与本机型的运行 END_STRUCT END_TYPE TYPE ST_StepItem : STRUCT eStepType : INT; // 步骤类型:1=定位, 2=等待IO, 3=延时, 4=贴标, 5=检测, 6=夹紧/松开 iAxisNo : INT; // 关联的轴编号(步骤类型为1时有效) rTargetPos : REAL; // 目标位置(定位步骤) rTargetSpd : REAL; // 该步骤的目标速度 tDelay : TIME; // 延时时间(延时步骤) iIOMask : WORD; // 等待IO时的信号掩码(等待IO步骤) eIOMode : INT; // IO等待条件:1=全部为ON, 2=全部为OFF, 3=任意ON END_STRUCT END_TYPE这看起来代码量不小,但注意,这是每一个FB实例只占一份的数据结构,而不是每种机型各一份不同的逻辑代码。数据多不是问题,问题是指令分支多。
2.3 内部逻辑的分层设计:公共逻辑层与差异层
FB内部我严格分成三层:
- 接口层:上面定义的那一堆变量,负责与外部世界交互。
- 公共逻辑层:启动、停止、复位、故障处理、主状态机推进。这部分代码对8种机型完全一样,一个字都不能有差异。
- 差异解析层:负责读
pstConfig,根据配置决定具体行为——比如轴使能几个、步骤表怎么走、联锁旁路不旁路。
用一张通俗的比喻:公共逻辑层是驾照考试里的交规,不管开的是轿车还是卡车都得遵守;差异解析层是车辆类型标识,告诉考官你开的这辆车是手动挡还是自动挡、能不能上高速。
公共逻辑层里最重要的就是主状态机。我建议不要用梯形图那种星罗棋布的M继电器写状态机,用ST的CASE语句一把梭:
CASE iState OF ST_IDLE: // 空闲态:等待启动命令 IF bRiseStart THEN iState := ST_HOMING; END_IF ST_HOMING: // 回原点态:逐轴回零 bHomed := FALSE; // 差异解析:只对配置中bEnable=TRUE的轴执行回零 FOR i := 1 TO pstConfig.iAxisCount DO IF pstConfig.arrAxisCfg[i].bEnable THEN // 调用该轴的回零功能 END_IF END_FOR // 全部就绪后切换 iState := ST_READY; ST_READY: // 就绪态:等待自动循环启动 bHomed := TRUE; IF bCmdStart THEN iState := ST_RUNNING; END_IF ST_RUNNING: // 运行态:主工艺循环 bRunning := TRUE; // 核心:解析步骤表,逐条执行工艺步骤 iCurStep := diStepIndex; // ... ST_ERROR: // 错误态:停机并等待复位 bError := TRUE; IF bCmdReset THEN iState := ST_IDLE; END_IF END_CASE注意这里的FOR循环。传统写法会这样:2轴机型的程序写两个轴的使能,5轴机型写五个轴的使能。用配置结构体之后,一个FOR i := 1 TO pstConfig.iAxisCount循环就把2到5轴全覆盖了。这就是"少写很多代码"的秘密——你写的不是8个版本的程序,而是8个版本的数据。
3. 代码复用架构的实操实现
3.1 定义配置结构体与实例化的具体方法
接下来是实操部分。我用实际代码走一遍整个实现过程。
第一步,定义用户数据类型UDT。在PLC的PLC数据类型(UDT)里新建ST_DeviceConfig,内容按上面那段代码填进去。注意MAX_AXIS我设定为8,因为目前最大机型是5轴,留3轴余量方便以后扩展。
第二步,在主程序(OB1或MAIN)里创建一个配置数组和一个FB实例数组:
VAR // 8种机型的配置数据表——这是唯一的"机型差异源头" arrDevCfg : ARRAY[1..8] OF ST_DeviceConfig; // 8个FB实例,分别对应8种机型 arrDevice : ARRAY[1..8] OF FB_DeviceTemplate; // 每个实例的IO映射 arrDevIO : ARRAY[1..8] OF ST_DeviceIO; END_VAR第三步,在初始化组织块(OB100)里给8个机型分别写入配置数据。这里的关键是:配置数据的初始化要集中写,一眼能看全,不能东一个西一个。
// 机型1:2轴基础版 WITH arrDevCfg[1] DO wDevType := 1; strDevName := 'MODEL_A'; iAxisCount := 2; bHasLabeler := FALSE; bHasVision := FALSE; rMaxSpeed := 1200.0; rMaxAcc := 500.0; rMaxPos := 5000.0; rMinPos := 0.0; iStepCnt := 6; // 步骤表 ... // 联锁 ... END_WITH;这种集中初始化最大的好处是:当新增第9种机型时,你只需要在数组的末尾加一行配置——不需要动任何FB内的逻辑。程序本体完完整整地只维护一份,就是那个FB。
3.2 FB内部实现中的关键细节和难点
模板FB内部代码写起来有几个细节特别容易翻车,我逐个说一下。
第一个细节:上升沿检测变量必须用FB的静态变量。
这是新手最容易犯的错。ST里如果在FB内部临时用了R_TRIG实例去检测输入信号的上升沿,而这个R_TRIG实例用的是VAR(局部变量),那每次扫描周期这个实例都会被重置,上升沿永远检测不到。正确做法是:把R_TRIG实例声明为FB的静态VAR变量,这样它才会在被多次扫描时保留内部状态。
VAR trigStart : R_TRIG; trigStop : R_TRIG; END_VAR第二个细节:配置数据只在FB的pstConfig输入参数上做读取,绝不能做写入。
为什么?因为同一个配置结构体可能被多个不同机型的FB实例共享引用,如果你在FB内部修改了配置值,等于把所有用这份配置的实例全改了。这是架构层面的红线。如果确实需要在运行中调整参数,应该在FB内部做一份"工作副本",改副本,不改模板。
第三个细节:类型选择和精度问题。
轴位置、速度这类工艺参数建议统一用REAL,不要有的用REAL有的用LREAL有的用INT。虽然ST支持类型转换,但多机型架构中光是处理类型转换的代码就会增加大量噪音,也容易埋下精度丢失的坑。我的惯例是:工艺参数一律REAL,计数器用ULINT,状态字用WORD。
第四个细节:FOR循环与直接寻址的权衡。
我上面建议用FOR i := 1 TO pstConfig.iAxisCount来遍历轴。但有朋友反馈说,这种方式在轴数量少的时候扫描周期略长。实际上PLC扫描这种程度的循环,耗时是微秒级的,对设备控制根本构不成影响。真正要注意的反而是:循环遍历时,必须把数组下界处理成1开始的索引。有些PLC的数组默认下界是0,你在配置里如果写arrAxisCfg[1],编译就会报范围错误。所以UDT定义数组时我建议明确写ARRAY[1..MAX_AXIS],强行把下界定为1,这样索引计算不会乱。
3.3 主程序中的循环调用方式
FB实例化好后,主程序里怎么调用?我用的是一个FOR循环统一扫描所有实例:
// 主循环:扫描所有8个设备实例 FOR i := 1 TO 8 DO arrDevice[i]( bCmdStart := xCmdStart[i], bCmdStop := xCmdStop[i], bCmdReset := xCmdReset[i], bCmdHome := xCmdHome[i], eModeSel := iModeSel[i], rSpeedSet := rSpeedSet[i], rAccSet := rAccSet[i], rPosOffset := rPosOffset[i], pstConfig := arrDevCfg[i], bRunning => xRunning[i], bHomed => xHomed[i], bError => xError[i], wErrorCode => wErrCode[i], iCurStep => iCurStep[i], ulProdCount => ulProdCnt[i], arrAxisState => arrAxisInfo[i], iobIO := arrDevIO[i], axisIF := arrAxisIF[i] ); END_FOR;这段代码的精妙之处在于:无论今天有2种机型上线还是8种机型同时上线,主程序的代码都不会变。你只需要根据车间实际的机型,把某个实例的输入命令、IO映射接到对应的HMI画面和物理IO上。其余的部分,全部透明。
当然,有些项目只需要当前机型是A时就跑A的实例,B时跑B的实例,不需要同时跑8个实例。这种情况更简单,只要把FOR循环改成根据当前机型号调用对应实例即可。我之所以做8个实例同时扫描,是因为那套设备是流水线,8个工位同时在线,不同工位是不同机型。各位要根据自己实际的设备结构来选择。
4. 8种机型的适配过程实录
4.1 第一步:整理异机型别参数表
动手写代码之前的准备工作,比写代码本身更重要。我把8种机型的原始电气图纸、IO分配表、工艺说明全部整理成一份Excel参数表,表格列就是ST_DeviceConfig里的每一个字段,行就是8种机型。
这一步能逼着你把8100%的差异都暴露在纸面上。比如轴2在机型B和机型C里回零方式不同(一个是负限位回零、一个是原点开关回零),这种细节如果你不提前填进表格,写FB时就会漏掉配置项,等到现场调试才暴露,代价极高。
参数表的整理结果直接影响UDT里字段的设计。我当时的表单里出现过几次"所有机型数值都一样"的列——比如"最大夹紧力",8个机型都是500N。这种字段不需要进配置结构体,直接写死在FB公共逻辑里就行,否则就是过度参数化,结构体会越来越臃肿,让后人不明白到底哪些参数该配哪些不该配。
所以我建议你按这个顺序走:先拉表格,再看"哪些列有差异",只有有差异的列才需要变成配置字段。没有差异的列直接留在FB代码里当常量。这一步做完,你的架构会非常干净。
4.2 第二步:初始化配置数据与工艺步骤表
整理完参数表,剩下的工作就清晰了:把表格里的数据逐一填进初始化代码块。
这个阶段有个实践技巧值得分享。8个机型里,部分机型工艺顺序看起来差异很大,但你可能80%的步骤其实是重复的。比如"定位到取料位"这步,几乎所有机型都有,只是目标位置数值不同。那么配置表里eStepType设置为1(定位),rTargetPos填各自的数值即可。同样,"等待夹紧到位"这步,设置eStepType为2(等待IO),iIOMask填夹紧气缸的到位信号位,eIOMode填1(全部为ON)。
在工艺步骤表上多花心思,是模板FB能否真正覆盖8种机型的分水岭。有的工程师把步骤表做成"顺序分支",也就是每个机型单独一列步骤清单,这在本质上又变成了复制粘贴。正确的做法是把步骤表做成统一的一张表,用步骤类型做行为区分,用参数做数据区分。
我定义的步骤类型我再说一遍,可以对照你自己的机型差异扩展:
| 步骤类型 | 含义 | 关键参数 |
|---|---|---|
| 1 | 轴定位 | 轴号、目标位置、目标速度 |
| 2 | 等待IO信号 | IO信号掩码、等待条件 |
| 3 | 延时 | 延时时间 |
| 4 | 触发贴标 | 贴标机构编号、延时 |
| 5 | 触发视觉检测 | 检测项目编号、超时时间 |
| 6 | 气缸动作 | 气缸编号、动作方向 |
步骤表最多支持16步,目前最长的一个机型是12步,留了4步余量。如果你的工艺更复杂,可以把数组上限扩到32或64,代价只是FB实例多占一些数据块空间,对扫描时间的影响微乎其微。
4.3 第三步:仿真验证到现场联调的要点
配置填完,别急着上机。我习惯先在PLC仿真环境里跑一遍,HMI画面先把所有机型都拖进去,逐一切换机型验证状态机是否正常推进。仿真主要验证三件事:
第一,状态机能否在无硬件的情况下到达READY态。理论上,没有硬件就不存在IO信号,等待IO的步骤会永远卡住,所以仿真是验证逻辑流向,不是验证硬件行为。
第二,配置数据有没有引错实例。用错实例的症状是:HMI上显示机型A,但状态反馈和轴配置却是机型C的参数。检查方法很简单,把wDevType和strDevName这些信息字实时显示在HMI诊断页面上,一切换就能看出来。
第三,参数是否有越界。比如某机型的轴数配置成了6,但MAX_AXIS只有8,而且arrAxisCfg数组里第6个轴的数据还是空的,运行到回零逻辑大概率就会用零值去控制一个根本不存在的轴。这类错误要在仿真阶段通过边界值测试暴露。
现场联调阶段,我的习惯是一个机型一个机型地过。先接好一个机型的IO,把HMI机型切换键锁定到该机型,然后按"单步执行"功能一步一步走工艺步骤表,确认每个步骤的行为与工艺说明书一致。确认无误后再切换下一个机型。这样虽然慢,但每一步的状态都是确定的,排查问题极其高效。
5. 常见问题与排查技巧
5.1 配置多但没生效:常见的引用错误
我在实际使用这个架构的过程中,最常遇到的问题是"配置明明填了,但FB跑起来不认"。
排查思路先看输入参数是否真的传进来了。调试模式里把FB实例的pstConfig变量展开,看里面填的值和初始化块里是否一致。如果不一致,多半是调用时把arrDevCfg[i]写成了arrDevCfg[1]——这种低级错误藏得还挺深,因为编译不会报错,运行也不崩溃,就是行为不对。
另一个常见的坑是:初始化块执行时机晚于FB首次调用。PLC的启动组织块(OB100)执行完后,主程序才开始扫描。但如果你把配置初始化放在某个从OB1调用的功能里,而这个功能又因为某些条件没有第一时间执行,那FB第一次扫描拿到的就是"空配置",某些机型名显示为空白,部分默认值被当作真实值用。解决方法是:初始化逻辑只放OB100这类启动OB,确保最早只执行一次,且任何FB调用之前配置已经就绪。
5.2 轴数越界与数组下标错误
数组越界是ST编程的老大难,尤其是在FB内部用FOR i := 1 TO pstConfig.iAxisCount遍历时。如果iAxisCount被错误地配成大于MAX_AXIS的值(比如9),虽然大部分PLC的ST运行时不会立即崩溃,但会读取相邻内存位置的值,产生极其玄幻的随机行为。
我的硬性防御措施:在FB内部的第一段逻辑里,强制对iAxisCount做上限钳位:
iAxisCountSafe := MIN(pstConfig.iAxisCount, MAX_AXIS);然后用iAxisCountSafe做循环边界,绝对不用原始的iAxisCount。这样即使配置填错,也不会发生越界读,只是某些轴不会执行而已,故障现象会直观得多。
5.3 模板改一个点,所有机型都受影响怎么办
这个架构带来一个"幸福的烦恼":因为所有机型共用一份代码,FB里改一行逻辑,8种机型全部会变。
好的一面是,bug修复只改一次,不会出现"这个机型修了、那个机型忘了修"的问题。风险的一面是,如果改动影响了某种机型的特殊行为,而你又没有测试到位,所有机型可能一起翻车。
应对策略是:给FB内部逻辑增加"机型分叉钩子"。比如在特定步骤前后预留一个CASE pstConfig.wDevType OF的扩展点,允许针对特定机型做微调,但限制这种分叉只能出现在明确标记的区位,且每个分叉点必须写注释说明原因。为什么不提倡全局大量用这种分叉?因为分叉多了,代码会重新走向"复制粘贴"的老路,失去统一维护的优势。控制分叉数量在5个以内,并保证每个分叉点是一个独立的小修改,这是我在实践中总结的安全边界。
5.4 新增第9种机型的标准操作流程
最后说一个大家最关心的问题:这套架构下,来一个第9种机型,到底要花多少工作量?
我的标准流程是这样:
- 在
arrDevCfg数组末尾新增一个元素,填写wDevType := 9、strDevName、轴配置、功能模块、工艺步骤表。 - 在HMI的设备选择画面里,把机型9下拉项加进去,绑定到新实例对应的HMI变量。
- 如果有新功能模块不在现有步骤类型里(比如增加了一个"称重模块"),这时候才需要打开FB,新增一个步骤类型(比如
eStepType := 7),并实现该类型的处理逻辑。
也就是说,80%的情况只需要做数据配置的工作,剩下20%才需要动模板代码。而且就算要动模板代码,也只需要在一个FB里改,改完8种机型同步受益。
我经历过一次完整的第9种机型导入,从拿到技术协议到现场跑通,一共用了3个工作日。而对比过去复制粘贴的开发模式,新增一个机型的调试周期一般在2到3周。这就是架构冗余能力带来的直接收益。
最后再分享一个维护上的心得:配置数据表一定要纳入版本管理。我见过很多人不把PLC工程放进Git/SVN,这是很危险的。因为这个配置表就是设备的"DNA",一旦现场出现过一次误改配置导致的事故,你就会明白把配置只读化、版本化、定期备份有多重要。我在每个FB里都加了配置校验和(一个简单的CRC),如果配置表和FB内部存储的校验值不匹配,直接进错误态并报故障码,这样可以杜绝"莫名其妙的原因"导致的设备异常。
重构这套架构的那几周,说实话挺痛苦的。要顶住"改好了倒好,改不好全完蛋"的压力,把8个机型的差异全部梳理清楚,还要说服团队里其他工程师改变写程序的习惯。但等到所有机型都跑起来的那一刻,那种释放感是实实在在的:以后再也不用为多机型维护熬夜了。