
干设备调试这行谁没在深夜接过售后电话我这边的情况更典型同一套设备平台衍生出8种机型单工位、双工位转台、龙门式、多工位环线样子不同但核心动作高度相似控制程序却在几年间滚成了8套半。今天想聊的是我用ST结构化文本重构的一套设备模板FB代码复用架构目标很直接——让8种机型共用同一个功能块所有机型差异全部收敛进数据配置。这里的FB是PLC里的Function Block功能块不是广告投放那个FB也不是电源芯片的反馈引脚。下面这套思路基于Codesys、TIA Portal这类支持IEC 61131-3的PLC环境语言上以ST为主但核心方法在任何一个支持结构体、数组和多实例的PLC平台都能平移。我先说一句代码复用在PLC圈子里一直被低估但一旦你把设备行为拆成状态机管流程、配置表管差异、IO层管物理世界多机型维护真的可以不再复制粘贴。1. 一套设备程序被8种机型拖垮之后我为什么重构出模板FB1.1 复制粘贴式开发是怎么把维护成本滚到八倍大的先说清楚我踩过的坑。最早这个平台只有两种机型程序就两套那时候复制粘贴还挺香。等到第四个机型出来程序变成四套改一个气缸延时要把四套程序同时打开逐行对照着改到第八种机型时现场工程师已经说不清哪几套是有效版本哪几套是历史废弃版。最狠的一次第6套程序是从第3套拷贝的拷贝时忘了改某工位的夹紧气缸保持时间结果新版机型在节拍提升后出现工件错位一撞就是好几个。客户半夜打电话我连夜赶到现场在四套程序里翻了两个小时才定位到是一个延时参数没有改。那一刻我就下定了决心不能再这么维护下去了。当时我算过一笔账8套程序每套平均5000行梯形图合计4万行每次新机型开发从拷贝到调试平均2周每次老机型工艺变更要同步8套程序平均耗时1天。这还不算版本混用带来的风险。重构模板FB一次性投入大概3天换来的是后续新机型开发从两周压到半天工艺变更从改8套变成改1份配置。这笔账怎么算都值得。其实搞代码复用这件事PLC圈子里一直有个错觉觉得设备千差万别根本没法抽象。我的体会是绝大多数非标设备的差只差在三个地方——输入输出点怎么接、动作按什么顺序做、参数调到什么值。这三样东西恰好都是数据而数据是可以抽出来的。真正的难点不是技术是你愿不愿意在项目不紧急的时候先花三天做设计。1.2 重构前提三条红线把灵活框进边界动工之前我给自己定了三条红线防止模板FB越写越万能最后变成四不像。红线一不改任何一台现有设备的工艺时序。模板不是重新发明机器而是把现有8台机器已有的动作规律原样装进一套代码。这个前提非常重要它决定了抽象时必须忠于现状而不是理想化的我觉得应该统一。红线二一套源码为所有机型共用机型专属逻辑必须以数据配置的方式存在不允许在程序里堆一堆IF 机器型号 1、IF 机器型号 3的分支。一旦允许分支代码出现这套模板很快就会退化回8套逻辑缝在一起的老样子只是把复制粘贴换成了满屏IF。红线三先抽象动作模型再写代码。我当时反复问自己这8台机器抛开传感器和气缸这些表象底层共通的东西到底是什么答案是执行一串有序动作每个动作等待传感器反馈或时间到然后进下一步。把机器看成一台动作序列发生器剩下的一切都有了抓手。至于为什么用ST而不是梯形图8种机型差异主要靠结构体、枚举、数组、循环来表达梯形图的气泡图和线圈模型处理这种数据密集型逻辑极为吃力。ST是IEC 61131-3里的文本化语言写算法、做索引、遍历数组非常顺手。不是说梯形图不好纯开关量控制用梯形图开发效率很高但一个模板覆盖8种机型这种场景从第一天我就知道必须上ST。2. 模板FB的骨架设计接口、状态机和参数表按三层分工2.1 把机器当自动售货机表现层、逻辑层、配置层设计模板FB之前我脑子里出现了一台自动售货机的画面外面那排按键是表现层顾客按哪个键、出货口有没有掉东西对应PLC里的输入输出点中间那个收到钱、判断货道、电机转、落货的流程是逻辑层对应FB内部的状态机货道里哪个格子放可乐、哪个格子放薯片、每个货道电机转几圈是配置层对应工艺参数表。换一个饮料机型没人会去重写售货机程序换一张货道配置表就够了。设备模板FB的目标就是达到这个状态。具体到实现我把模板FB内部拆成三层IO表现层负责把物理输入输出点映射到逻辑编号状态机逻辑层负责推进当前动作、检查反馈、决定下一步配置层则从外部传入机型参数和工艺配方。每一层只和上下层通过约定好的接口说话任何一层的变化不牵动另外两层。这样IO点变了就改映射表动作顺序变了就改动作序列配置工艺参数变了就改配方数据块。2.2 引脚定义机型枚举是模板的第一个灵魂模板FB的接口看起来很简单但每个引脚都有讲究。先看ST代码(* 模板FB接口定义 *) FUNCTION_BLOCK FB_DeviceTemplate VAR_INPUT eMachineType : EMachineTypeEnum; (* 机型枚举 *) bStart : BOOL; (* 启动命令上升沿有效 *) bStop : BOOL; (* 停止命令电平有效 *) bReset : BOOL; (* 故障复位 *) END_VAR VAR_IN_OUT stParams : ST_DeviceParams; (* 工艺参数引用外部配方块 *) END_VAR VAR_OUTPUT eState : E_StateMachine; (* 当前状态 *) bReady : BOOL; (* 待机就绪 *) bRunning : BOOL; (* 正在运行 *) bFault : BOOL; (* 故障汇总 *) wFaultCode : WORD; (* 故障代码 *) END_VAReMachineType是机型枚举这个引脚是整个模板的灵魂。FB内部很多地方不需要真的知道具体型号只需要在关键分支处查一下枚举值就能决定当前走哪套逻辑。另一个容易忽略的点是stParams用了VAR_IN_OUT而不是VAR_INPUT。原因很简单结构体体积大如果走VAR_INPUT每个扫描周期都会把整块参数复制一遍既慢又容易出现数据不一致VAR_IN_OUT把参数数据块的引用交给FB实例直接读写原表这在Codesys和TIA里都是更稳妥的做法。机型枚举我建议用带语义的名字别用M1到M8TYPE EMachineTypeEnum : ( MACH_A01, (* 单工位标准版 *) MACH_A02, (* 单工位高速版 *) MACH_B11, (* 双工位转台式 *) MACH_C21, (* 龙门式 *) MACH_D31, (* 多工位环线 *) MACH_D32, (* 多工位环线强化版 *) MACH_E41, (* 全伺服定制版 *) MACH_F51 (* 专机版 *) ); END_TYPE8个枚举值对应8种机型剩下的位置留给下一款新机型。用带语义的名字不是因为情怀而是三个月后回来看代码你绝对不想对着M3和M5猜这是哪台机器。2.3 状态机8台机器共用一套执行模型的关键状态机本身不复杂但它是所有机型共用的执行骨架TYPE E_StateMachine : ( ST_IDLE, (* 空闲 *) ST_HOMING, (* 回原点 *) ST_READY, (* 就绪 *) ST_RUNNING, (* 运行中 *) ST_DONE, (* 周期完成 *) ST_FAULT (* 故障停机 *) ); END_TYPE这套状态机看似只有6个状态但所有动作序列都在ST_RUNNING内部被解释执行。我专门讲一下执行模型因为这是和普通脚本程序最大的区别PLC是循环扫描的FB每次被OB1调用一次一个扫描周期就执行一遍。如果你在FB里写一个循环把100个步骤全部跑完一个扫描周期设备就走过完整流程了现场根本没法看。正确的做法是每周期只推进一个动作状态做完一步、扫描周期结束、下一个周期继续做下一步。我把这个原则叫单周期单步推进。伪逻辑大概是ST_IDLE收到bStart上升沿进HOMINGHOMING完成置READYREADY收到运行信号进RUNNINGRUNNING内部取出nStepIndex对应的动作指令执行动作并等待完成信号或超时最后一步做完置DONE等待复位回IDLE。任何一步异常直接进FAULT锁存故障码。这个执行模型8种机型一模一样变化只发生在动作指令数组的内容里。3. 把8种机型的差异压进三个维度IO映射、动作序列、工艺配方3.1 先盘点8台机器的真实差异清单开始写配置结构体之前我建议所有做设备模板的人都先做这么一张表把8台机器的差异列成清单再逐项问自己这个差异是结构性的还是数值性的结构性的差异要抽成配置项或扩展位数值性的差异直接塞进参数结构体。差异维度A01 单工位B11 双转台C21 龙门式D32 多工位环线收敛方式IO范围开关量24点/2轴开关量48点/3轴开关量60点/4轴开关量96点/8轴IO映射表数组二次寻址动作顺序固定单一顺序转台工位并行龙门取放压装环线连续多工位动作指令块数组工艺参数2组压力配方4组速度配方6组位置配方12组配方结构体数组参数外部表特殊功能无在线测厚扫码追溯压力监控追溯功能扩展标志位列完这张表你就会发现真正需要程序处理的分支极少90%的差异都是数据层面的。我当时甚至有点意外原来8种机型这么吓人的差异落到代码上几乎不产生新逻辑。3.2 IO映射表用二次寻址把硬件点变成逻辑编号最笨的做法是程序里直接写I0.0、Q2.3这种绝对地址一换机型地址就得改代码。IO映射表的思路是程序里永远只跟逻辑编号打交道比如传感器1号、执行器3号再通过一张映射表把逻辑编号翻译成真实的物理地址。这一步就是所谓二次寻址。TYPE ST_IOMap : STRUCT aInputIndex : ARRAY[0..MAX_INPUTS-1] OF INT; (* 逻辑编号-输入映射 *) aOutputIndex : ARRAY[0..MAX_OUTPUTS-1] OF INT; (* 逻辑编号-输出映射 *) bActiveLow : ARRAY[0..MAX_INPUTS-1] OF BOOL; (* TRUE表示常闭输入 *) END_STRUCT END_TYPE新机型IO点不同只需要把映射表配一遍。更换地址时也不用翻代码改表里一行值就行。常闭还是常开的差异也放到配置项里避免程序里为传感器是常开还是常闭写两份逻辑。这里有一条现场经验PLC里通过索引访问I区时各家平台语法差异很大。我习惯在每个扫描周期开头把硬件输入整批复制到一个输入缓冲数组之后FB全部从缓冲数组按索引取数FOR i : 0 TO MAX_INPUTS-1 DO bIoBuffer[i] : bHardwareInput[i]; END_FOR这样做的好处是逻辑代码与硬件扫描解耦。输出同理维护一个输出缓冲数组周期末尾统一刷给硬件。这套缓冲机制在IO数量大的机型上还能顺带解决信号抖动引起的逻辑误判属于加量不加价。3.3 动作指令块把工艺流程格式化成数据这是整个模板设计里我最满意的一块。传统梯形图里一个气缸动作就是网络里的一个线圈和定时器流程图千变万化写出来的程序也就千变万化。我的做法是定义动作指令块ST_StepCommand把每一步抽象成五个字段动作类型、执行目标、目标值、超时时间、是否连续执行。一整条工艺流程就变成了这个结构体的一串数组。TYPE ST_StepCommand : STRUCT eAct : E_Action; (* 动作类型夹紧、进给、吹气、检测等 *) uiCfg : UINT; (* 目标索引IO映射表中执行器编号/轴号 *) rVal : REAL; (* 目标值压力、位置、时间 *) rTimeout : REAL; (* 超时时间单位毫秒 *) bContinue: BOOL; (* 完成当前步后立即进入下一步 *) END_STRUCT END_TYPE TYPE ST_DeviceParams : STRUCT aRecipe : ARRAY[0..MAX_STEP-1] OF ST_StepCommand; (* 动作指令序列 *) aRecipeLen : UINT; (* 有效步骤数 *) rCycleTime : REAL; (* 节拍目标仅用于监控 *) aUserParam : ARRAY[0..MAX_USERPARAM-1] OF REAL; (* 工艺配方区 *) iIomap : ST_IOMap; (* IO映射表 *) END_STRUCT END_TYPE实际配置一个动作序列在初始化块里写起来很直观stParams.aRecipe[0] : (eAct : ACT_CLAMP, uiCfg : 3, rVal : 80.0, rTimeout : 3000, bContinue : FALSE); stParams.aRecipe[1] : (eAct : ACT_SAFE_RAISE, uiCfg : 2, rVal : 120.5, rTimeout : 5000, bContinue : FALSE); stParams.aRecipe[2] : (eAct : ACT_WAIT_IO, uiCfg : 7, rVal : 0.0, rTimeout : 2000, bContinue : TRUE);这样状态机不管面对第7种机型还是第3种机型它只做一件事取出aRecipe[nStepIndex]看eAct是什么动作执行等待反馈或超时然后nStepIndex加一。写代码只写一遍变的是数据表。从8种机型扩到12种机型程序里需要加的行数非常少——加枚举值、加一张参数表而已。3.4 工艺配方参数与程序物理隔离设备调机阶段最烦的是程序在里面参数在外面改一次参数重新下装一次程序。有了上面的结构体工艺参数天然落在参数数据块里FB运行过程中直接引用。触摸屏上画一张表格运维工程师在线改配方不需要PLC工程师到场。这里有个细节配方区最好和程序区物理隔离。我习惯把参数放到独立全局DBFB只通过VAR_IN_OUT引用。这样将来更新固件时只需更新程序数据块配方块保留在线不会丢数据。代价是要小心两个数据块的地址一致性这个我放到第4章讲模板版本校验就是专门治这个的。4. 实例化与背景数据块一台模板FB怎么同时扛住8台机器4.1 数组实例化一行声明代替八份重复代码模板FB写完之后剩下的问题就是怎么把它变成8个实例。最傻的做法是把FB拖8次出来各配一个背景DB调用代码照样写8遍。真正省事的是把实例声明成数组配合FOR循环统一调用VAR asDevices : ARRAY[1..8] OF FB_DeviceTemplate; (* 8个设备实例 *) apParams : ARRAY[1..8] OF ST_DeviceParams; (* 对应8套参数 *) END_VAR FOR i : 1 TO 8 BY 1 DO asDevices[i]( eMachineType : aeMachineType[i], bStart : abStartCommand[i], bStop : abStopCommand[i], bReset : abResetCommand[i], stParams : apParams[i], eState aeState[i], bReady abReady[i], bRunning abRunning[i], bFault abFault[i], wFaultCode awFaultCode[i] ); END_FOR;数组实例化之后背景DB的生成也是一份带下标的DB。TIA Portal里这种用法很成熟Codesys上也完全支持。需要注意的坑是如果工程里用了FOR循环调用带VAR_IN_OUT的FB且传入参数是数组元素不同平台编译器支持度有差异。我在Codesys上遇到过需要把数组元素先拷贝到临时变量再传入的情况编译才不报警。遇到这类平台差异不要硬绕按平台习惯调整就好。4.2 背景DB只装状态配方块独立存放我见过有人把所有参数都塞进背景DB结果一个实例的背景DB塞了几百个变量下装一次要等半天。正确的分工是背景DB里只放FB运行态——当前执行到第几步、超时计时器、输入缓冲、输出缓冲、故障锁存机型差异、工艺配方、IO映射表全部放在独立的全局参数DB里通过VAR_IN_OUT引用。这样有三个好处背景DB小巧固件更新不覆盖配方换机型时可以直接替换参数DB的指向。这里需要再强调一下VAR_IN_OUT它意味着FB对传入数据有写权限模板内部要注意别把读取参数误写成修改参数。我在模板里养成了一个习惯所有工艺配方读取统一封装成读取方法刻意把写操作集中到少数几个接口里防止状态机某个分支手滑改了配方。4.3 模板版本变化后的数据兼容一个字节的版本校验设备程序最难处理的事情之一是升级模板FB改了背景DB的布局可能变旧背景DB里的数据还残留着下载后新程序读到的却是旧布局里的值设备表现千奇百怪。我的做法是在全局参数DB里放一个模板版本号变量FB每次调用时先校验版本对不上直接进FAULT并提示重新下装配方。这个成本只有一个字节却避免了无数个现场怪问题。IF stConfig.dwTemplateVersion CONFIG_VERSION THEN bFault : TRUE; wFaultCode : FAULT_CFG_VERSION; RETURN; END_IF;代码和配方版本分开管理CONFIG_VERSION是程序侧版本配方DB内部还可以放一个配方版本。程序升级时如果结构体字段有增删CONFIG_VERSION要手动加1如果只是调参数配方版本加1程序版本不动。这套规则在团队协作时必须写进开发规范不然版本号形同虚设。注意模板FB升级时永远先重建数据块再测试不要在线直接覆盖旧背景DB否则你分不清问题是出在新逻辑还是旧数据。5. 调试期踩坑链路从能编译到8台全稳定之间隔着四个坑5.1 坑一第6台一上电就报401故障查硬件却一切正常第一台联调的是A01一遍流程跑通我当时觉得这套架构稳了。结果把第6台D31机型下装之后一上电就报Fault 401故障码锁在HOMING阶段。开始我怀疑是回原点传感器坏了拿万用表量信号正常看状态机卡在ST_HOMING第3步传感器反馈却始终读不到FALSE最后我索性把所有传感器的映射表打出来一行一行对发现第6套配置里第18号传感器映射索引填成了40而整个输入映射数组长度只有32索引越界读取到的自然是一个莫名其妙恒真的地址。根因是配置表在某次Excel复制粘贴时串行。这也是IO映射表方案避不开的坑配置数据一旦人工填写早晚会出错。所以我在FB里加了一个IO映射索引边界校验函数调用时把所有映射索引扫描一遍越界直接报配置错误而不是用错误地址去读IO。这个校验让后续再也遇不到看似硬件问题、其实配置问题的隐蔽故障。5.2 坑二FOR循环里同一个周期调用8个实例状态机被一拖到底用数组实例加上FOR循环之后出现了一个让我想了一整天的怪毛病按下A01的启动按钮B11居然也把整个流程跑完了。两个机型共用同一套IO不可能。后来我用变量监控表逐个看8个实例的状态机发现所有实例都在同一个扫描周期里从IDLE冲到了DONE。问题出在FOR循环的调用方式上。一个PLC扫描周期内FOR循环会把asDevices[1]到asDevices[8]连续执行一遍如果我FB内部对bStart用的是电平判断而且状态机一步执行完紧接着执行下一步那么第一个实例执行时把整个动作序列循环跑完占用了这个扫描周期的大把时间。但这还不至于让别的实例也跑——真正的原因是FB内部的状态变化条件在一周期内被反复刷新比如动作完成标志在循环下一次调用时还保持TRUE新实例一看上一步已完成自己也就跟着往下跳。具体到我的代码是超时定时器在一个周期内被多次调用后没有正确复位后一个实例读到的超时计数是前一个实例残留的。排查链路很曲折先怀疑背景DB冲突再怀疑共用全局变量最后定位到FB内部把当前周期已执行的状态写到实例变量之外去了。修复方式两条。第一严格坚持单周期单步推进FB每个扫描周期最多执行一个动作指令执行完立即退出把下一步留给下一个扫描周期。第二状态机的推进只依赖实例自身的背景数据任何临时状态不写到外部公共变量。这样即使一个周期里调用8次每个实例也只能老老实实前进一步。改完之后我专门写了一个测试在同一个周期里给两个实例同时给启动信号观察它们的状态步进确认是每周期一步而不是一周期全跑完。5.3 坑三升级模板后3号机运行到夹紧位就停住手动捅一下阀又好模板从V2升级到V3之后3号机每次走到夹紧工位就停住设备不再往下走手动捅一下夹紧阀流程又恢复了。客户以为是阀有问题换阀也没用。我远程连上去看状态机停在ST_RUNNING第5步等待夹紧确认信号但信号一直没有。手动捅阀等于代替程序把夹紧做完了信号有了设备就继续。查到最后发现背景DB是从V2在线升级到V3的V2版本里有一个夹紧完成的保持型变量在V3里这个变量被重用了初始值是旧数据TRUE导致FB启动自检时把该信号当成了已经完成的历史状态于是程序认为第5步未满足条件。对就是背景DB新旧数据错位。我后来立了一条规矩模板版本升级时必须走一次数据块重建初始化。具体做法是新增一个启动标志首启动时把所有保持型变量清零再进入HOMING如果升级前有需要保留的工艺配方配方数据单独放在不会被重建的参数DB里。最粗暴有效的校验法就是前面说过的版本号检测到CONFIG_VERSION变了FAULT并提示重新初始化不让旧数据跟着新布局走。这一步几乎是所有设备程序迭代都会踩的坑它比逻辑错误隐蔽得多。5.4 坑四用同一台触摸屏反复切换机型参数怎么串味了等到8种机型全调完我又发现一个交互层的坑。现场触摸屏上一台HMI管8台设备设备操作工在A01的配方页把压力调到80切到C21界面一看速度参数变成80了。一开始我以为是HMI配方窗口串位后来发现不是——触摸屏在切机型时没有把当前正在编辑的配方表切换到对应的apParams[i]而是把同一块地址里的数据当成了当前机型的参数。根子在于参数索引和HMI变量绑定没有分离。HMI上直接绑定了apParams[1]切到C21时只是切换了界面显示变量地址没跟着切换操作工修改的永远是apParams[1]。做法是在PLC里写一个机型切换功能块HMI侧切换机型时只下发一个eSelectedMachine枚举PLC统一执行读取目标机型参数表、复制到HMI编辑缓冲、更新界面刷新标志三步。操作工动参数时改的是缓冲表确认下发后由PLC写回对应机型的参数DB。所有变量地址在HMI侧统一绑定缓冲块而不是8份设备的直接地址。这套做法顺带解决了误操作问题再也不会因为绑定错地址把A01的参数写到B11头上。6. 这套架构的边界什么情况不适合以及再往前走一步6.1 什么时候别硬套模板FB写完这套架构我得说点劝退的话。模板FB不是万能的遇到这三种情况别硬套一、机型数量太少只有1台且定型后基本不改模板化反而增加设计成本二、机型之间差异大到除了外壳都一样的程度比如一台是液压压机一台是纯输送皮带动作模型完全不共用硬套模板的结果就是配置项比代码还复杂新工程师看到配置表直接崩溃三、现场工程师完全没有数据结构概念你在配置表里放结构体数组他们改不动这种场景不如老老实实分机型写专人维护专人改。我后来总结了个粗略判断法先列差异清单如果差异点超过40%集中在执行机构不同这种结构性层面模板方案就不要强推如果差异点集中在数量多少、压力大小、时间长短这种数值层面模板方案收益非常大。一句话模板抽的是共同行为配置放的是不同数据行为不像就不要硬装。6.2 从设备模板到产线模板数据驱动架构还能长多大这套架构落地后最大的变化不是代码量少了而是设备交付方式变了。以前客户说加一种型号我要排一周的开发周期现在技术员在触摸屏上新增一套配方半天就能试跑。更进一步我正把这套模板往产线层扩展每台设备保持一个模板FB加一份参数DB产线之间用统一的工艺数据块做交接这样整条线的节拍、互锁、配方下发都是同一套数据语言。走到这一步上层的MES、追溯系统只需要和统一数据接口对话不需要知道具体哪台设备是什么结构。从8种机型的设备模板开始我逐渐意识到数据驱动设备这件事真正的坎不是PLC编程技巧而是能不能把设备行为彻底拆分状态机管流程配置表管差异IO层管物理世界。想清楚这三件事任何数量的机型追加都不会再让工程师陷入复制粘贴的泥潭。最后分享一点个人体会我做完这套重构最大的收获并不是那几段ST代码而是先把差异列成数据、再动手写程序的习惯。现在接到任何新项目我先问的不是用哪个PLC而是一共有几种机型差异在哪里。机型大于等于3的时候不管客户催得多急我都会先花半天理配置表再花两天搭模板。事实证明这半天设计时间永远比后期维护省下的时间划算得多。这套模板FB在项目上跑了一年多8台设备稳定运行我也从跟着设备跑的调试工变成了按配置交付设备的工程师。希望这篇总结能给被多机型维护折磨的朋友一条新思路——代码复用真的不只是IT圈的事。