news 2026/10/1 16:21:51

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PLC多机型程序复用:FB+ST实现一套代码适配八种设备

做设备开发的朋友,应该都经历过这种场面:公司旗下同一系列设备有七八种机型,机械结构大同小异,工艺逻辑八九不离十,但每款的IO点位、轴数、速度参数、回零方式全都不一样。以前我带的项目组处理这种需求,最直接的做法就是把上一台设备的程序复制一份,然后改改参数、删删逻辑,硬凑出新机型。结果就是程序库里躺着一堆相似的PLC工程,今天改A机型的bug,明天得同步改BCDEFG,漏改一个就等着现场打电话。

直到我把这一摊重构成“1个FB打8种机型”的方案之后,维护成本才算真正降下来。这里的FB是功能块(Function Block),ST是结构化文本(Structured Text),复用则是把8种机型的差异全部收敛成“数据配置”而不是“程序分支”。一套逻辑代码,实例化8个FB,分别吃8套参数表,哪个机型上电就跑哪个。这篇文章就围绕这套写法,把FB的接口划分、参数表设计、实例化调用、在线调试和踩坑经验一次说透。适合正在做设备类PLC程序标准化、或者被多机型维护搞得焦头烂额的工控工程师参考。

1. 8种机型相差的不只是外形:三类必须处理的差异

要谈复用,首先得把“差异”拆开看。8种机型听起来复杂,但逐条列出来,真正需要程序去适配的无非三类:IO布局、工艺参数、时序逻辑。很多复用方案失败,就是因为没把这三类差异分开处理,而是全部搅在逻辑里面。

1.1 IO布局差异:同一个功能,不同端子

不同机型的硬件配置不一样,最典型的就是IO映射。比如A机型“启动按钮”接在I0.0,B机型接在I1.2,C机型可能根本没有实体按钮,而是从上位机发命令。伺服使能信号也是,有的机型用DO直接控制,有的走总线,有的靠通信字。

如果程序里到处写死I0.0、Q0.1,那机型一多根本没法看。正确做法是把这些点位统一映射成内部变量,比如bStartBtn、bServoEnable,FB内部只认这些逻辑变量,不管它实际来自物理IO还是通信区。映射关系放在上层调用或者硬件配置表里,一台机型一张表。

1.2 工艺参数差异:速度、压力、时间各不同

这一类差异最直观。同样是热压机,A机型最高压力50kN,B机型120kN;同样是贴标机,A的加速时间是0.8秒,B要2.5秒;同样走伺服定位,A机型轴速6000rpm,B机型只有3000rpm。这些数值差异如果散落在程序各个角落,改起来就是一场灾难。

正确做法是把所有工艺相关的数值参数集中到一张参数结构体里,FB内部通过参数读取。这样现场调机只需要打开参数表,改数值,甚至不需要进程序。

1.3 时序差异:启动、检测、出料的顺序变来变去

这一类比前两类更隐蔽,也更难处理。A机型的流程是“启动→加热→保压→冷却→出料”,B机型是“启动→定位→点胶→固化→出料”,C机型还多一个“自检”步骤。流程顺序不一样,如果用梯形图写一大堆串联步进,程序会膨胀得很厉害。

我的做法是把流程统一抽象成一个状态机:待机、启动延时、运行、暂停、故障、复位。具体每个状态里做什么,由参数表里的“使能开关”和“目标值”控制。比如某机型不需要冷却,就把冷却时间参数填0,逻辑自动跳过。这样时序差异就坍缩成了参数差异。

2. 为什么我用FB+ST而不是把梯形图复制8份

这个选择不是拍脑袋定的,我经历过“复制粘贴8份梯形图”的黑暗时代,也试过用SCL写一部分再跟梯形图混搭,最后才确定全套ST+FB的组合。下面说说这个组合到底赢在哪。

2.1 FB与FC的区别,正好对应“设备对象”与“计算工具”

IEC 61131-3里的功能块和函数,很多人分不清,但恰好可以用在多机型场景里做职责划分。FC(Function,功能/函数)没有自己的内部存储,给同样的输入,一定产生同样的输出,适合做纯计算,比如单位换算、比例缩放。FB(Function Block,功能块)则带有一块独立的背景数据区,实例化一次就有一份自己的内部状态,比如定时器、计数器、当前步号、累计产量。

8种机型每一台都是一台“会记住自己状态”的设备,这天然就是FB的模型。你用FC去描述一台设备,得把所有状态都通过输入输出参数传进去传出来,接口会爆炸。用FB就顺理成章——每台设备实例化一个FB,状态锁在各自的背景数据块里,互不干扰。

2.2 ST的数组、循环和表达式,是批量差异的天然解药

梯形图处理单点逻辑很顺手,但处理“批量差异”就很痛苦。8种机型的轴参数,在ST里就是一个ARRAY[1..8]的数组加上一个FOR循环的事,在梯形图里你得拖8个功能块再挨个填参数。

更关键的是,ST能写复杂的表达式和类型化代码。比如“设备运行速度 = 参数表最大速度 × 当前节拍系数”,这句话用ST写就是一行表达式,可读性强,不容易错。而当逻辑涉及数据结构、枚举、数组时,ST几乎是唯一顺手的选择。我现在写设备控制程序,除了极少数需要强可视化的安全回路,工艺逻辑全部用ST。

2.3 也要泼盆冷水:不是所有逻辑都适合塞进FB

ST+FB不是万能药,我见过有人把十几页的逻辑全塞进一个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”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 16:19:48

汽车传感器与执行器教材精讲:从感知到执行的工程实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 16:17:53

Python+OpenCV+YOLOv8:苹果叶病害识别检测系统实战解析

简介&#xff1a;这是一套面向毕业设计与智慧农业场景的苹果叶病害识别检测系统源码包&#xff0c;基于Python和OpenCV实现&#xff0c;可检测赤霉病、枯叶病、铁锈病等常见叶片病害&#xff0c;适合计算机视觉、深度学习方向的毕设项目参考与二次开发。压缩包共382个文件&…

作者头像 李华
网站建设 2026/10/1 16:17:00

从零开始做AI工程:从RAG到Agent的系统化实战指南

还记得前两年团队招聘的时候&#xff0c;简历上写"AI工程师"的候选人&#xff0c;十个里有八个聊下来其实是在调API&#xff0c;剩下两个是在跑开源模型的训练脚本。当时我就意识到&#xff0c;这个行业缺的从来不是会用某个模型的人&#xff0c;而是能把AI真正"…

作者头像 李华
网站建设 2026/10/1 16:16:40

LabVIEW 入门避坑:数据流、VI、串口通信与仪器同步采集

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 16:16:37

C语言sizeof深度解析:运算符本质、常见陷阱与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华