ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PLC多机型程序复用:FB+ST实现一套代码适配八种设备

PLC多机型程序复用:FB+ST实现一套代码适配八种设备 做设备开发的朋友应该都经历过这种场面公司旗下同一系列设备有七八种机型机械结构大同小异工艺逻辑八九不离十但每款的IO点位、轴数、速度参数、回零方式全都不一样。以前我带的项目组处理这种需求最直接的做法就是把上一台设备的程序复制一份然后改改参数、删删逻辑硬凑出新机型。结果就是程序库里躺着一堆相似的PLC工程今天改A机型的bug明天得同步改BCDEFG漏改一个就等着现场打电话。直到我把这一摊重构成“1个FB打8种机型”的方案之后维护成本才算真正降下来。这里的FB是功能块Function BlockST是结构化文本Structured Text复用则是把8种机型的差异全部收敛成“数据配置”而不是“程序分支”。一套逻辑代码实例化8个FB分别吃8套参数表哪个机型上电就跑哪个。这篇文章就围绕这套写法把FB的接口划分、参数表设计、实例化调用、在线调试和踩坑经验一次说透。适合正在做设备类PLC程序标准化、或者被多机型维护搞得焦头烂额的工控工程师参考。1. 8种机型相差的不只是外形三类必须处理的差异要谈复用首先得把“差异”拆开看。8种机型听起来复杂但逐条列出来真正需要程序去适配的无非三类IO布局、工艺参数、时序逻辑。很多复用方案失败就是因为没把这三类差异分开处理而是全部搅在逻辑里面。1.1 IO布局差异同一个功能不同端子不同机型的硬件配置不一样最典型的就是IO映射。比如A机型“启动按钮”接在I0.0B机型接在I1.2C机型可能根本没有实体按钮而是从上位机发命令。伺服使能信号也是有的机型用DO直接控制有的走总线有的靠通信字。如果程序里到处写死I0.0、Q0.1那机型一多根本没法看。正确做法是把这些点位统一映射成内部变量比如bStartBtn、bServoEnableFB内部只认这些逻辑变量不管它实际来自物理IO还是通信区。映射关系放在上层调用或者硬件配置表里一台机型一张表。1.2 工艺参数差异速度、压力、时间各不同这一类差异最直观。同样是热压机A机型最高压力50kNB机型120kN同样是贴标机A的加速时间是0.8秒B要2.5秒同样走伺服定位A机型轴速6000rpmB机型只有3000rpm。这些数值差异如果散落在程序各个角落改起来就是一场灾难。正确做法是把所有工艺相关的数值参数集中到一张参数结构体里FB内部通过参数读取。这样现场调机只需要打开参数表改数值甚至不需要进程序。1.3 时序差异启动、检测、出料的顺序变来变去这一类比前两类更隐蔽也更难处理。A机型的流程是“启动→加热→保压→冷却→出料”B机型是“启动→定位→点胶→固化→出料”C机型还多一个“自检”步骤。流程顺序不一样如果用梯形图写一大堆串联步进程序会膨胀得很厉害。我的做法是把流程统一抽象成一个状态机待机、启动延时、运行、暂停、故障、复位。具体每个状态里做什么由参数表里的“使能开关”和“目标值”控制。比如某机型不需要冷却就把冷却时间参数填0逻辑自动跳过。这样时序差异就坍缩成了参数差异。2. 为什么我用FBST而不是把梯形图复制8份这个选择不是拍脑袋定的我经历过“复制粘贴8份梯形图”的黑暗时代也试过用SCL写一部分再跟梯形图混搭最后才确定全套STFB的组合。下面说说这个组合到底赢在哪。2.1 FB与FC的区别正好对应“设备对象”与“计算工具”IEC 61131-3里的功能块和函数很多人分不清但恰好可以用在多机型场景里做职责划分。FCFunction功能/函数没有自己的内部存储给同样的输入一定产生同样的输出适合做纯计算比如单位换算、比例缩放。FBFunction Block功能块则带有一块独立的背景数据区实例化一次就有一份自己的内部状态比如定时器、计数器、当前步号、累计产量。8种机型每一台都是一台“会记住自己状态”的设备这天然就是FB的模型。你用FC去描述一台设备得把所有状态都通过输入输出参数传进去传出来接口会爆炸。用FB就顺理成章——每台设备实例化一个FB状态锁在各自的背景数据块里互不干扰。2.2 ST的数组、循环和表达式是批量差异的天然解药梯形图处理单点逻辑很顺手但处理“批量差异”就很痛苦。8种机型的轴参数在ST里就是一个ARRAY[1..8]的数组加上一个FOR循环的事在梯形图里你得拖8个功能块再挨个填参数。更关键的是ST能写复杂的表达式和类型化代码。比如“设备运行速度 参数表最大速度 × 当前节拍系数”这句话用ST写就是一行表达式可读性强不容易错。而当逻辑涉及数据结构、枚举、数组时ST几乎是唯一顺手的选择。我现在写设备控制程序除了极少数需要强可视化的安全回路工艺逻辑全部用ST。2.3 也要泼盆冷水不是所有逻辑都适合塞进FBSTFB不是万能药我见过有人把十几页的逻辑全塞进一个FB里结果这个FB几百个变量光看接口就头大。判断标准很简单如果一个“对象”具备完整生命周期和状态用FB如果一个逻辑只是算个数、转一下格式用FC或者普通函数更合适。还有安全回路、急停链这类要求极高可视性和可追溯性的逻辑梯形图或专用的安全程序块反而是更稳妥的选择。3. FB接口这样划分8种机型才塞得进同一个模板复用方案的核心在FB的接口设计。接口分得好8种机型就是参数的排列组合接口分得差FB内部全是IF分支越写越脏。我的设计心法就一句话把所有“变化面”全部推到输入端FB内部只认逻辑变量和参数表。3.1 先把输入信号分成“机型相关”和“机型无关”两堆设计FB接口之前我会把所有外部信号分成两堆。机型无关的信号比如总使能、急停复位、模式选择这些所有机型都有直接做成FB的BOOL输入。机型相关的信号比如某机型的“料位检测”接在I0.3另一机型的“料位检测”接在I2.5这些不在FB里出现而是在上层映射成逻辑变量后再喂进来。实际项目中我会为每种机型的IO映射单独做一个映射FC或者一个映射表把物理IO和逻辑变量对应起来。FB只接收逻辑变量这样FB对IO布局完全无感。3.2 一张UDT参数表兜底所有数值型差异数值型差异我统一收进UDT用户自定义数据类型。下面给出一个简化版定义实际项目里字段会更多但思路一致TYPE ST_MachineParams : STRUCT iMachineNo : INT; // 机型编号 rMaxSpeed : REAL; // 最大速度(mm/s) rAccTime : REAL; // 加速时间(s) rDecTime : REAL; // 减速时间(s) rTargetPressure : REAL; // 目标压力(kN) rPressureHoldTime : REAL; // 保压时间(s) iAxisCount : INT; // 轴数 bEnableCooling : BOOL; // 是否启用冷却工步 rCoolingTime : REAL; // 冷却时间(s) iHomeMode : INT; // 回零方式: 1原点 2限位 3无 END_STRUCT END_TYPE这张表就是FB和具体机型之间的“契约”。FB内部只读取这份表不关心当前是哪种机型。如果需要加一种新机型往往只需要往表里填一份新数据。3.3 FB内部的状态机写得规矩时序差异才不会失控时序差异由FB内部的状态机消化。我用一个CASE语句管理流程步进每个步进处理自己的动作。下面是一个高度简化的示意CASE nStep OF 0: // 待机 IF bStart AND bEnable THEN nStep : 10; END_IF; 10: // 启动延时 tonDelay(IN : TRUE, PT : DINT_TO_TIME(tParams.rAccTime * 1000.0)); IF tonDelay.Q THEN nStep : 20; END_IF; 20: // 加压运行 rSpeedRef : tParams.rMaxSpeed; IF tParams.bEnableCooling THEN nStep : 30; ELSE nStep : 40; // 无冷却则直接跳转到结束等待 END_IF; 30: // 冷却工步 tonCool(IN : TRUE, PT : DINT_TO_TIME(tParams.rCoolingTime * 1000.0)); IF tonCool.Q THEN nStep : 40; END_IF; 40: // 结束等待 IF bStop THEN nStep : 0; END_IF; END_CASE;注意IF tParams.bEnableCooling这种写法工序是否执行是由参数控制的而不是靠程序版本区分。这样B机型不需要冷却改一个BOOL值就跳过去了整个状态机的骨架完全一致。4. 配置数据与程序分离新增第9种机型时只动数据不动逻辑采用这套写法之后最爽的时刻就是接新机型订单的时候打开参数表复制一份改参数完事。程序本体基本上不需要动。要做到这一点关键是配置数据要独立存放、能被方便地装载和修改。4.1 参数表如何落库存取后台DB/全局DB/配方在TIA Portal环境里我习惯把8种机型的参数表放在一个全局DB里定义成数组VAR_GLOBAL arrMachineParams : ARRAY[1..8] OF ST_MachineParams; END_VAR在CoDeSys环境里做法类似可以放在全局变量列表或者一个独立的GVL里。数组下标就是机型编号1号机读arrMachineParams[1]2号机读arrMachineParams[2]。展示给HMI时直接用这个数组生成表格画面现场调机改起来非常直观。还有一种更灵活的做法是用配方管理Recipe功能。每台机型的整套参数作为一个配方通过HMI一键下装到PLC。这样做的好处是机型参数可以备份、复制、比较甚至通过CSV导入导出。我个人认为配方管理更适合“同一种机型不同批次产品”的场景纯多机型差异用数组全局DB的方式更简单直白。4.2 装载到内存后怎么校验数据表归数据表PLC跑起来还得防止有人填错数据导致设备乱动。我在FB里加了一套参数合理性检查每次使能前扫一遍关键参数。比如速度上限超过硬件允许值、轴数超出实际通道数、步骤号越界这些都要报故障。数控机床那种“参数设错了就撞机”的事在设备行业一样会发生。// 参数检查示例 bParamOk : TRUE; IF tParams.rMaxSpeed rHardwareLimit THEN bParamOk : FALSE; nFaultCode : 101; // 速度超限 END_IF; IF tParams.iAxisCount 0 OR tParams.iAxisCount iMaxAxisSupported THEN bParamOk : FALSE; nFaultCode : 102; // 轴数异常 END_IF;4.3 一个新机型从接单到上线的标准动作当第9种机型排产时我的标准动作只有三步第一步在参数表数组里把ARRAY[1..8]扩成ARRAY[1..9]第二步根据机械图纸填参数第三步在上层调用逻辑里把该机型的启停按钮、故障复位映射到新实例的对应输入上。三步走完程序主体零改动剩下的就是现场调机微调参数。5. 实例化8个FB的完整写法和在线调试思路逻辑讲清楚了接下来是落地环节。一个FB怎么服务8台设备答案是实例化8次每个实例对应一个机型。这一节我把调用代码和在线调试的次序讲透。5.1 调用代码和8个实例的完整写法在TIA Portal中我直接在OB1里写ST调用每个FB调用会自动生成一个背景DB。8个实例就是8个背景DB互不干扰。代码如下// 机型1 fbMachine_1( bEnable : g_bEnableGlobal, bStart : g_bStartSel[1], bStop : g_bStopSel[1], stParams : arrMachineParams[1], bRunning g_bRunning[1], bFault g_bFault[1]); // 机型2 fbMachine_2( bEnable : g_bEnableGlobal, bStart : g_bStartSel[2], bStop : g_bStopSel[2], stParams : arrMachineParams[2], bRunning g_bRunning[2], bFault g_bFault[2]);这串代码虽然看起来重复但每个实例的信号源不同——启动按钮是各自的参数表是各自的输出状态也是各自的。在CoDeSys里则可以更加紧凑地用数组实例化FOR i : 1 TO 8 DO fbMachines[i](bEnable : g_bEnableGlobal, bStart : g_bStartSel[i], bStop : g_bStopSel[i], stParams : arrMachineParams[i], bRunning g_bRunning[i], bFault g_bFault[i]); END_FOR;有基础的朋友可能会问循环调用8个FB扫描周期会不会撑不住实测下来只要FB内部没有死循环、没有重型运算8个实例的扫描增加量通常在微秒级完全可以忽略。我就是这么干的。5.2 全局使能与单机启停多实例协同的接线方式多机型设备有个常见需求有时候希望所有机器一起开工有时候只想单机调试。我的做法是分两层控制。全局层放一个g_bEnableGlobal总使能这个信号不直接启动任何单机而是作为FB的bEnable前置条件。单机层是各自的启动按钮或上位机命令分别接到各自的bStart。再加上一个“调试模式”开关。调试模式下总使能被旁路操作员可以单机点动。正常运行模式下总使能必须为真否则所有机器都进不了运行步。这两种模式在HMI上做个切换即可。5.3 在线修改参数和强制信号的调机顺序到了现场调机阶段我的习惯是先把所有机型参数从HMI拉到PLC然后不开总使能逐个在线监视FB实例的状态变量。先看参数表是否装载正确再看输入映射是否对得上——比如按下一个机型1的启动按钮fbMachine_1.bStart有没有变TRUE。所有输入确认无误后再开总使能跑空循环最后才带料跑工艺。这里有个非常实用的细节监视FB实例时可以同时打开背景数据块看内部变量。TIA Portal和CoDeSys都支持这种操作。我在调试时通常把关键变量做成一个监控屏状态步号、当前压力、当前速度、故障码一屏看完省去切来切去。6. 我在这套复用写法上踩过的4个坑方案的优点说了不少坑也得摆一摆。这套“1个FB打8种机型”的写法我用了几年踩过的问题主要集中在调用方式、内存规划、数据边界和状态残留四个方面。每一个都让现场吃过苦头写出来希望你能绕开。6.1 一个FB实例被两个任务调用背景数据被抢着写这是我最早期犯的错误。当时为了优化扫描周期把一部分逻辑放在循环中断里另一部分放在主程序中。结果一个FB实例既在OB1里调又在刷新中断里调两个任务抢占同一个背景数据块导致状态跳变异常、定时器忽快忽慢查了很久才定位到。原因很简单同一个FB实例被多处调用内部状态变量会被交替写入逻辑上完全是未定义行为。规则就是“一个实例只在一处调用”如果要跨任务访问走接口参数传递不要直接塞同一个实例。6.2 枚举越界不报错的诡异现象参数表里有不少枚举型字段比如回零方式iHomeMode设计时约定1/2/3三种。有一次现场工程师填参数时不小心输了个8程序不报编译错误运行起来却走了一个完全没定义的CASE分支设备动作变得鬼畜。ST对枚举值越界的检查很弱很多情况下编译期不报错运行时行为不可预期。这事的教训就是参数入口必须加范围校验同时HMI上尽量用下拉框而不是自由输入框来限定可选值。6.3 8个实例的背景DB体积被低估导致装载失败FB内部变量越多实例化后的背景数据块越大。有一版FB我塞了大量数组和诊断记录8个实例的存储需求加起来超过了某些老CPU的数据块上限结果下载时直接报存储不足。解决办法是“瘦身”——把不必要的大数组挪出FB改成全局DB或用FC临时计算同时在做方案时要提前估算8个实例的存储总量别等下载报错才回头改。6.4 机型切换时残留状态没复位多机型设备里经常出现“这台机型今天跑A工艺明天换B工艺”的柔性生产场景。切换机型时如果只是改参数表不把FB的内部状态清掉会出现一个很隐蔽的bug上一批产品跑完时状态机停在“结束等待”切到新机型后按启动状态机还在旧位置动作乱套。我的方案是在FB内部加一个“机型切换检测”IF iMachineNo iLastMachineNo THEN nStep : 0; bRunning : FALSE; bFault : FALSE; iLastMachineNo : iMachineNo; END_IF;新机型号一进来内部状态全部归零再重新走流程。类似的原则也适用于配方切换生产设备切换产品型号时状态复位永远应该放在第一位。最后再分享一个我个人的习惯每次把新机型灌进这套FB框架之前我都会在电脑上模拟一遍参数表的边界值——速度设成0、时间设成负数、轴数设成超限值看程序能不能自己报警而不是跑飞。这套写法最值钱的地方不是省了那几份复制粘贴的程序而是把“设备逻辑”和“机型差异”彻底分开让调试和维护都变得可以一步步推导。如果你正被多机型程序折腾得够呛不妨按这个思路重构一次前期的接口设计工作换来的是后面几年不用做“人肉diff”。
返回列表