先交代一下背景最近在给客户做一套包装设备的程序标准化同一个机架根据订单可以选配不同配置硬生生分出8种型号。第一版程序我干了一件现在想起来都脸红的蠢事——每种型号各写一个FB逻辑几乎一样参数稍有不同8个FB加起来两千多行。改一个共性缺陷就要打开8个文件各改一遍改漏一处现场就给你颜色看。后来我痛定思痛把这8个FB砍成1个通用FB用ST语言重写用一个机型枚举参数来区分所有差异。这篇文章把完整写法拆开讲包括枚举定义、配置表设计、轴与IO差异的数据化处理以及我踩过的几个坑。如果你也在做多机型设备的PLC程序这篇值得看完。为什么要强调ST而不是别的语言因为FB做复用核心是允许我们把机型差异从代码结构差异转化为数据差异而ST在处理数组、结构体、枚举、循环这些数据化手段时最顺手。你用梯形图做8路分支也能复用但维护起来还是痛。下面直接进入正文。1. 先复盘8个机型到底差在哪为什么直接写8个FB是坑1.1 我这里8种机型的真实差异清单这8种型号看着是不同产品拆开看其实就几类差异轴数不同从单轴到8轴、有没有压合单元和温控模块、速度加速度行程参数不同、节拍目标不同。我整理了一张表做标准化之前先把差异摸清楚这点非常重要。型号轴数最高速度(mm/s)加速度(mm/s²)行程(mm)压合单元温控模块节拍目标(ms)MDL_A1300500200无无1500MDL_B2500800300无无1200MDL_C38001000350有无1000MDL_D48001000400无无1000MDL_E2500800300无有1300MDL_F610001200500有有900MDL_G1200400800无无2000MDL_H812001500600有有800做程序前花半天把这张表理出来后面省的时间远超半天。你看这些差异绝大多数是数值不同和有没有某个可选单元真正的动作逻辑八成是共通的。这就是复用的前提——如果8种机型连动作流程都完全不同那硬塞进一个FB反而是灾难后面我会讲边界条件。1.2 复制FB改参数这套打法为什么会崩把第一个型号的FB复制一份改改参数加上条件编译或者IF分支第三个型号开始你就难受了共性bug修8遍漏修一个现场那台就出问题而且问题可能隔几周才暴露根本想不到是当初漏改参数。版本漂移严重A型号的FB改过新功能B型号的没跟上客户问为什么两台设备功能不一样答不上来。软件工程师离职后新人接手面对8个近乎相同的FB每个几百行改哪个都怕改错。我拆掉第4个机型后就下决心重构了。核心思路一句话把机型差异从代码差异变成数据差异让FB只有一个数据有8份。这就是标题里1个FB vs 8种机型的由来。2. 复用第一层把机型变成一个参数而非一个代码分支2.1 用枚举类型声明8种机型别用魔法数字我见过有人用INT参数0到7代表不同机型调用方写注释说明。这种做法最大的问题是可读性差而且编译器不帮忙检查。ST里定义枚举类型十几行搞定TYPE E_MODEL : ( MDL_A : 0, MDL_B : 1, MDL_C : 2, MDL_D : 3, MDL_E : 4, MDL_F : 5, MDL_G : 6, MDL_H : 7 ); END_TYPE定义这个枚举的价值不只是给数字起名字。你在FB输入参数里声明为这个枚举类型调用方如果传了一个不存在的机型编号编译直接报错如果是INT魔法数字运行期才能暴露。用枚举还有个好处后面配置表的下标可以直接跟它对应代码里的CASE、循环都能用这个类型做判断可读性上去一个台阶。2.2 FB接口里加一个机型输入取代8个FB实例重构后我的通用FB接口长下面这样。注意输入参数里加了eModel这就是整个复用的开关FUNCTION_BLOCK FB_ModelDriver VAR_INPUT bEnable : BOOL; eModel : E_MODEL; (* 机型选择MDL_A..MDL_H *) rSpeedRef : REAL; (* 速度给定 mm/s *) rPosRef : REAL; (* 位置给定 mm *) bHome : BOOL; (* 回零触发 *) END_VAR VAR_OUTPUT bReady : BOOL; bBusy : BOOL; bError : BOOL; iErrorCode : INT; stActiveConfig : ST_MODEL_CONFIG; (* 当前机型的完整配置方便监控 *) END_VAR原来程序里8个FB实例的地方现在只保留一个实例比如叫fbMainModel调用方式从8套不同的逻辑调用变成同一套调用只是eModel参数由HMI或者PLC上层逻辑决定。调用点少了出错的概率天然就降下来了。这里我多说一句FB实例减少之后监控窗口干净了。原来在Online监视里要看8个FB的输出现在一个FB的输出配合机型参数和配置结构体现场一眼能看清当前是哪台设备、跑了哪套参数。这个优势在调试时非常舒服。2.3 为什么用ST而不用梯形图做这层梯形图也能做枚举判断但有一个硬伤梯形图的分支逻辑是画出来的8个机型要画8条并行分支每条分支里还有参数赋值、功能块调用画面极其臃肿而且梯形图对数组和结构体的支持远不如ST方便。ST是文本语言写循环、写数组初始化、写结构体赋值天然契合数据驱动的思路。做标准化的核心不是画图好看而是让逻辑和数据分离ST就是干这个的料。关于ST语言的学习曲线给个实在建议如果只会梯形图别慌ST其实不复杂。ST的语法比C语言简单得多没有指针部分环境支持但可以不碰、没有复杂的继承关系你只要会用IF、CASE、FOR、:赋值、VAR声明就够做本文这套方案了。真正的难点不是ST语法本身而是把8份业务差异抽象成一张数据表的思维方式。3. 复用第二层配置表驱动而不是CASE瀑布3.1 为什么不推荐把所有参数写进CASE分支机型枚举放进FB之后你自然会想到用CASE按机型分支赋值CASE eModel OF MDL_A: iAxisCount : 1; rMaxSpeed : 300.0; rAcc : 500.0; rStroke : 200.0; ... MDL_B: iAxisCount : 2; rMaxSpeed : 500.0; ... END_CASE这个方案能跑但说实话只比复制8个FB高级一丢丢。问题在于每个CASE分支里堆了十几个参数8个型号就是一百多行参数赋值而且一旦参数多了你找某个型号的某项参数要在一个分支里翻很久。新增型号要在CASE里加一块删型号要删一块改起来牵扯函数体内部。更麻烦的是参数混在逻辑代码里版本管理工具上看不出参数变更和逻辑变更的区别。3.2 配置文件结构体一张表装下8套机型参数我做标准化时选择的做法是先把所有机型参数定义成一个结构体然后建一张全局配置表一次性把8套参数都塞进去。先定义结构体TYPE ST_MODEL_CONFIG : STRUCT eModel : E_MODEL; (* 机型编号 *) iAxisCount : INT; (* 轴数量 *) rMaxSpeed : REAL; (* 最高速度 mm/s *) rAcc : REAL; (* 加速度 mm/s² *) rStroke : REAL; (* 行程 mm *) rTorqueLimit : REAL; (* 扭矩限制 % *) bHasPressUnit : BOOL; (* 是否带压合单元 *) bHasTempControl : BOOL; (* 是否带温控模块 *) iCycleTimeGoal : INT; (* 节拍目标 ms *) (* 后面如果要加新参数直接在这个结构体里补字段 *) END_STRUCT END_TYPE再定义全局配置表放在独立的GVL里VAR_GLOBAL GVL_ModelConfigs : ARRAY[0..7] OF ST_MODEL_CONFIG : [ (eModel : MDL_A, iAxisCount : 1, rMaxSpeed : 300.0, rAcc : 500.0, rStroke : 200.0, rTorqueLimit : 60.0, bHasPressUnit : FALSE, bHasTempControl : FALSE, iCycleTimeGoal : 1500), (eModel : MDL_B, iAxisCount : 2, rMaxSpeed : 500.0, rAcc : 800.0, rStroke : 300.0, rTorqueLimit : 70.0, bHasPressUnit : FALSE, bHasTempControl : FALSE, iCycleTimeGoal : 1200), (* 注意这里8个机型的配置逐行列上顺序和枚举编号保持一致 *) ... (eModel : MDL_H, iAxisCount : 8, rMaxSpeed : 1200.0, rAcc : 1500.0, rStroke : 600.0, rTorqueLimit : 95.0, bHasPressUnit : TRUE, bHasTempControl : TRUE, iCycleTimeGoal : 800) ]; END_VAR数组下标从0到7正好对应枚举值0到7。这样8套参数集中在一个声明块里哪台机器什么参数打开GVL一眼就能看到改参数只需要改这一处函数块本身一个字节都不用动。3.3 FB内部一行代码加载当前机型配置FB内部做一件关键的事根据输入的枚举值把对应的配置结构体取出来存到输出变量stActiveConfig里。因为ST环境下枚举本质上是整数可以直接当数组下标用在TwinCAT 3、CODESYS 3.5等主流环境都支持// 加载当前机型的配置 stActiveConfig : GVL_ModelConfigs[eModel];有的PLC环境不允许枚举直接做下标那就加一个映射函数把枚举转成INT。我见过一个项目代码量不大但坚持做了一层映射原因是他要从配置表中间插一行不希望枚举值和数组下标强绑定。我的建议是初期还是让枚举下标一致简单直接如果将来机型排序经常变再做映射层也不迟。配置加载之后剩余的逻辑通通只读stActiveConfig里的字段不再出现如果机型是MDL_A这种判断。打个比方FB像一个通用插座不同的机型参数像不同的插头插上谁就是谁。3.4 配置表的隐藏收益在线切换和现场调试配置表驱动的另一个好处是FB运行时可以在线切换机型参数。原来8个FB的方案你想验证MDL_F的参数得下载另一套程序现在你只需要在HMI上把当前机型从MDL_A切到MDL_FFB内部读到新的配置立刻按新参数运行。我在现场调机时就靠这一招反复对比不同机型的运动曲线省了大量下载程序的时间。但要注意在线切换不能无脑切尤其是正在运行时切换轴数从8变成1多出来的轴如果不先禁用会直接报错。这个我在第5章重点讲。4. 复用第三层轴数量、IO差异、行为差异全部数据化4.1 轴数量不一样用轴数组而不是固定轴变量8种机型轴数从1到8这是最麻烦的差异之一。如果每个轴单独写一个FB实例fbAxis1、fbAxis2……逻辑里就会出现大量如果轴数大于3就执行fbAxis3的条件分支。我的做法是把轴驱动FB声明成数组VAR fbAxis : ARRAY[1..8] OF FB_AxisControl; END_VAR然后所有逻辑都用循环去访问轴轴的数量由配置表决定// 例子按配置表轴数批量使能 FOR i : 1 TO stActiveConfig.iAxisCount DO fbAxis[i]( bEnable : bRun, rSpeedRef : rSpeedRef, rPosRef : rPosRef ); END_FOR; // 超出轴数的轴强制禁用避免残留使能 FOR i : (stActiveConfig.iAxisCount 1) TO 8 DO fbAxis[i](bEnable : FALSE); END_FOR;轴数组的引入彻底消灭了按轴数写分支的代码。新增机型哪怕轴数是7、8、9只要小于等于数组上限配置表里改个数就行。如果你猜未来机型轴数可能超过8那就把数组上限直接定义成16留好余量不要将来为了扩数组又要改逻辑。4.2 IO差异不要在FB里写物理地址用逻辑信号数组多机型项目里IO地址差异也是个坑。一开始我也试图在FB内部直接引用物理IO地址比如QX0.3、IW4这种每个机型一个映射关系。后来发现这是最坏的做法因为物理地址映射是PLC硬件配置层的事不该出现在控制逻辑里。正确的做法是FB只接收逻辑信号数组物理IO映射交给PLC的IO配置或者顶层映射变量去处理。VAR_INPUT arrInputSignals : ARRAY[1..16] OF BOOL; (* 逻辑输入信号 *) END_VAR VAR_IN_OUT arrOutputSignals : ARRAY[1..16] OF BOOL; (* 逻辑输出信号 *) END_VAR每个机型在硬件配置里定义自己的物理IO到逻辑信号的对应关系。比如MDL_C的压合到位信号映射到IX5.2MDL_H的同一逻辑信号映射到IX7.3这些映射在PLC的IO映射表里完成FB内部完全不感知。逻辑信号数组的大小按IO最多的机型定少的机型只用前几路剩余的不接即可。这么分层的价值在于换IO点、换PLC模块、换机型控制逻辑一行都不用改只动映射表。这对后续设备维护、甚至移植到下一个项目都是巨大的便利。4.3 行为差异用配置里的BOOL开关控制可选功能块比如压合单元和温控模块不是所有机型都有。这类有和没有的差异直接做成配置表里的BOOL字段TYPE ST_MODEL_CONFIG : STRUCT ... bHasPressUnit : BOOL; bHasTempControl : BOOL; END_STRUCT END_TYPEFB内部调用对应的功能块时用配置开关控制IF stActiveConfig.bHasPressUnit THEN fbPressUnit( bEnable : bEnable, bCylinderHome : arrInputSignals[1], bCylinderOut : arrOutputSignals[1] ); END_IF; IF stActiveConfig.bHasTempControl THEN fbTempCtrl( bEnable : bEnable, rSetpoint : stActiveConfig.rTempSetpoint, rActualTemp : rThermocoupleValue, bHeaterOn : arrOutputSignals[2] ); END_IF;没有压合单元的机型fbPressUnit不会被执行内部输出也不会刷新逻辑上天然隔离。这个方案比用CASE包一层所有可选动作要清爽得多因为可选单元往往不是一个FB而是一组动作用开关控制调用、用配置表控制开关可读性最好。4.4 时序差异把动作步骤也变成一张表如果只是参数和可选单元的差异配置表足够。但有一类差异更隐蔽——动作时序。比如某些机型要先压合再移动某些机型移动完才压合节拍不同导致等待时间不同。这种差异用BOOL开关很难描述清楚。我的解法是引入步骤表。把整个工艺动作拆成若干步骤每个机型的步骤序列用一张表表达步骤是固定的每个步骤是否启用、超时多少通过配置表控制TYPE ST_SEQ_STEP : STRUCT bEnable : BOOL; (* 当前机型是否启用该步骤 *) tMaxTime : TIME; (* 步骤超时时间 *) sStepName : STRING(20); (* 步骤名用于HMI和报警显示 *) END_STRUCT END_TYPE配置表里增加一个步骤表字段arrSteps : ARRAY[1..16] OF ST_SEQ_STEP;状态机跑的时候按顺序遍历步骤表bEnable : FALSE的步骤直接跳过FOR i : 1 TO 16 DO IF stActiveConfig.arrSteps[i].bEnable THEN // 执行第i步置输出、等到位信号、判断超时 ... IF iStepComplete THEN iStepIndex : i 1; END_IF; EXIT; // 只处理当前步骤 END_IF; END_FOR;这算是我这套方案里最高级的用法。当你的机型差异从参数不同升级到动作顺序不同时步骤表能兜住最后一层差异。如果连步骤表都表达不了——比如某机型有完全独立的工艺段那基本上就到了该拆分FB的边界了我在第6章谈这个。5. 实测中踩过的坑机型切换、数组越界、在线调试5.1 坑一运行中切换机型残留状态导致轴报警配置表这么好用难免手痒想在线切机型。我第一次这么干是在设备运行状态下把eModel从MDL_A1轴切到MDL_H8轴结果就是抱闸没控制好轴使能状态混乱报警一大片。问题根源不在切换本身而在于切换瞬间FB内部旧的轴使能状态、旧报警、旧状态机步数还挂在变量里而配置已经换成新的了。新旧混跑一个扫描周期现场就炸。正确的做法是给机型切换做一个专用流程先全停、再复位、再加载配置IF eModel eLastModel THEN // 第一步全轴禁用工艺初始化复位 FOR i : 1 TO 8 DO fbAxis[i](bEnable : FALSE, bReset : TRUE); END_FOR; // 第二步清空FB内部状态 iStepIndex : 0; bError : FALSE; iErrorCode : 0; // 第三步加载新机型配置 stActiveConfig : GVL_ModelConfigs[eModel]; eLastModel : eModel; // 第四步等一个扫描周期后再开始执行避免旧状态残留 bWaitOneCycle : TRUE; END_IF我这里为了让切换在停机待机状态下才允许还在外面做了一个互锁如果主流程不在待机状态HMI下发机型切换指令会被拒绝并提示请先停机再切换。这个互锁可以放在FB内部也可以放在上层状态机但一定要有。毕竟在线切机型是为了调试方便不是为了在生产中玩火。5.2 坑二枚举下标和配置表行数对不上参数全是0配置表8行枚举值0到7看着稳稳的但人总会犯错。我曾经加了一个新机型MDL_I先扩展了枚举配置表里忘了加数据结果程序编译通过运行时机型切到新枚举值数组越界或者读到默认值所有参数归零轴速直接飙到0设备纹丝不动还报一堆错。我后来养成了一个习惯FB初始化时做一次配置表自检把所有机型配置扫一遍发现轴数越界、行程为0等明显异常就直接置错误标志并且不允许运行// 初始化自检校验所有机型配置合法 IF NOT bConfigChecked THEN FOR i : 0 TO 7 DO IF GVL_ModelConfigs[i].iAxisCount 1 OR GVL_ModelConfigs[i].iAxisCount 8 OR GVL_ModelConfigs[i].rMaxSpeed 0.0 THEN bError : TRUE; iErrorCode : 100 i; // 错误码含义配置表第i项异常 EXIT; END_IF; END_FOR; bConfigChecked : TRUE; END_IF这套自检逻辑看着笨但一个人面对8种机型、几十个参数难免会漏。让程序自己兜底比自己翻眼睛核对靠谱得多。5.3 坑三配置表字段太多HMI显示和监控反而成了重点用了配置表之后在线监控FB内部变量你会发现满屏都是stActiveConfig的子字段值确实都在但人眼分辨当前机型压合单元是否启用要在一堆字段里找。现场调试效率反而降了。我后来在HMI做了一个机型参数总览页面直接读stActiveConfig的字段把轴数、速度、加速度、节拍目标这些关键项用表格控件展示出来现场切机型后一眼确认参数有没有加载对。这里也有个小技巧HMI上机型选择用一个下拉框绑定到eModel变量选项文本直接写MDL_A-单轴基础款这种带描述的名字比裸枚举值直观太多。操作员不知道MDL_F是什么但知道带压合带温控的六轴高速款是哪台。5.4 坑四配置表要不要集中放GVL还是藏在FB内部我第一版偷懒把配置表直接写在FB内部变量区。结果是每次要改某个机型的参数得打开FB源码找声明段然后在初始化块里做一大段赋值。虽然逻辑没问题但对同事不友好——他们不敢动FB内部怕改坏逻辑。后来我把配置表挪到独立的GVL全局变量列表FB只读不写。这样分工就清楚了调参数的人只碰GVL写逻辑的人只碰FB。GVL文件命名也直白一点比如GVL_ModelParams打开就是8行星号注释说明每个机型的用途新同事接手也不会不知所措。版本管理上看变更记录也更清晰改参数就是GVL_ModelParams一个文件的diff逻辑代码完全不动。6. 从8种扩展到更多机型这套写法的边界与扩展思路6.1 新机型接入的完整流程一个字都不用改逻辑这套方案稳定之后我总结出一个新机型接入流程照着做就行在E_MODEL枚举里加一个新值比如MDL_I : 8。在GVL_ModelConfigs里加一行配置数据按结构体字段填完整。如果新机型轴数超过现有数组上限先把fbAxis的数组上限扩一下同样arrInputSignals、arrOutputSignals的长度也要检查。如果新机型有全新可选单元需要在配置结构体里加BOOL字段FB里加一个对应的IF调用块。编译、下载、用HMI切到新机型验证参数加载和动作。整个流程里步骤1和2是纯数据操作步骤3和4是扩展操作。正常8成的新机型只需要改枚举和配置表FB逻辑层零改动。这也是复用的真正含义——不是省一次编码时间而是把增加机型这件事从代码开发变成了填表。6.2 配置表数据来源CSV或者配方管理避免手改代码做设备厂的人应该都有体会客户要的参数变来变去今天这个速度、明天那个节拍。全写在GVL里每次都要改程序再下载很被动。进阶做法是把配置表数据外部化TwinCAT 3 可以用Recipe ManagerCODESYS 可以用配方管理Recipe把配置表导出成文件保存在PLC的存储卡里运行时加载不同配方文件。更轻量一点的做法是自己写个CSV解析功能块把配置表做成CSV文件HMI上填表导入GVL只保留默认配置兜底。我实际项目里没有全部外部化因为设备厂客户大多就那几台机器GVL改参数加下载程序完全够用。但如果你的产品线面向最终用户、机型数量未来可能翻倍我建议尽早把配置数据往配方/文件方向走。这里切记外部化之前先把第5.2节说的配置自检做扎实否则一个填错的CSV导入进去设备动作乱套。6.3 边界条件什么时候该1个FB拆成多个FB写文章要讲清楚方案的适用范围。配置表ST这套写法能对付大多数情况但遇到下面几种特征时别硬塞机型之间的工艺动作顺序完全不同比如一款机型是上料-压合-裁切另一款是裁切-上料-贴合步骤表的16步不够表达或者步骤表已经大到看不懂。机型之间有本质的安全逻辑差异比如一款机型要求双手启动另一款只有单手启动安全相关逻辑不建议用统一FB做数据化容易埋雷。可选单元之间还有互锁关系比如压合单元和温控单元在某种机型里不允许同时工作这种规则写在配置表里只能靠BOOL但互相制约的规则用代码描述更清楚。我个人的经验判断线是这样的如果8种机型动作流程相似度超过七成一个FB加配置表完全没问题如果相似度只有五成左右可以用一个FB加配置表加步骤表但前提是你对状态机有足够的掌控力低于五成老老实实拆成两个FB但保留公共的报警、状态、使能逻辑做一个公共FB让两个FB都调用它。这是我在另一个非标设备项目里验证过的边界。6.4 这套方案对团队协作的意义最后说点技术之外的。代码复用表面上是省代码实际省的是沟通成本。以前8个FB每个FB需要调试、需要验证、需要测试团队里谁写了哪个模块、参数改了哪里全靠互相邮件同步。现在只有一个FB加一张配置表新同事花半天看明白结构体的字段含义就能独立改参数。现场出问题确认机型、看stActiveConfig各字段、查错误码三个步骤定位问题比在8份FB源码里翻找快太多了。我个人的体会是做设备标准化做到最后受益最大的不是代码量而是整个团队对设备的理解和对变更的掌控。你要问我这套方案后面还能怎么扩展我建议一是把配置表做成外部配方二是针对每个机型的调试参数PID、前馈、滤波也一并收进配置表三是当你积累了3个以上项目的机型配置后可以抽出共性的结构体做成你自己厂里的标准模板库。当然这些都是后话先把一个FB吃透让8种机型在一张配置表里跑顺就已经是很大的进步了。