
1. 从1个FB vs 8种机型说起这个标题到底在讲什么第一次看到1个FB vs 8种机型ST复用这样写这个标题很多做PLC和工控的朋友应该会心一笑。FB就是Function Block功能块ST就是Structured Text结构化文本IEC 61131-3标准里那套长得像Pascal的编程语言。标题的意思很直白用一个功能块去覆盖八种不同机型的控制逻辑而且是用ST语言写的复用方案。这事在工控圈里其实是个老生常谈但又总有人踩坑的话题。你去看任何一个非标自动化项目只要机型超过三种代码里必然出现大量复制粘贴改一改的痕迹。A机型的轴控逻辑抄到B机型改两个参数B机型再抄到C机型再改两个参数。抄到第八个机型的时候原始代码长什么样已经没人记得了改一个bug要改八遍漏一遍就等着现场调试的时候炸。所以这个标题背后真正要解决的问题是如何用ST语言设计一个高内聚、低耦合的功能块让它通过参数化配置适配多种机型而不是为每个机型写一份独立的逻辑。关键词里的配置表三个字是核心线索——这意味着方案不是靠IF-ELSE堆出来的而是靠数据驱动。这篇文章适合谁看如果你正在做多机型兼容的PLC项目或者你手里已经有一堆复制粘贴出来的功能块想重构再或者你刚学ST语言想知道工程上到底怎么组织代码那接下来的内容应该对你有用。我会从设计思路、ST语言特性、配置表结构、实操步骤、常见坑几个维度把这个方案拆开讲尽量做到你看完能直接套到自己项目里。2. 为什么是1个FB而不是8个FB复用方案的设计逻辑2.1 多机型项目的典型困境先说说大多数人的做法。假设你有八种机型每种机型的差异集中在几个地方轴的数量不同有的3轴有的5轴、气缸数量不同、安全门配置不同、HMI交互流程略有差异、报警阈值不一样。很多工程师的做法是建八个FBFB_MachineA、FB_MachineB……每个里面把逻辑写一遍。这种做法在项目初期看起来很快因为你可以直接复制上一个机型的代码改改就行。但问题会在三个时间点集中爆发第一个时间点是客户改需求。比如所有机型都要增加一个急停后自动复位的功能。你得打开八个FB每个里面加同样的逻辑。加完之后测试发现第七个FB里漏了一个变量没改现场跑起来复位到一半卡住了。第二个时间点是新人接手。新人打开项目看到八个几乎一样的FB完全不知道它们之间的差异在哪里改任何一个都怕影响其他机型。第三个时间点是版本管理。八个FB意味着八份代码要维护Git diff看起来一片红code review根本没法做。2.2 单FB方案的核心思想变化点分离单FB方案的核心思想其实就一句话把变化的部分抽出来变成数据把不变的部分留下来变成逻辑。八种机型之间什么是变的轴数、气缸数、阈值、使能条件、时序参数。什么是不变的状态机的流转逻辑、报警的处理流程、轴的使能-回零-定位-停止这套动作序列。不变的部分写进FB变化的部分通过配置表传入。这就像做菜。八个机型是八道菜但炒菜这个动作是一样的——热锅、下油、下料、翻炒、调味、出锅。你不需要为每道菜写一个炒菜功能块你只需要一个炒菜功能块把什么料、什么火候、炒多久作为参数传进去。2.3 为什么选ST而不是梯形图这里要专门说一下语言选型。梯形图LD做逻辑互锁很直观但做数据驱动和复杂数据结构很吃力。你要在梯形图里搞一个结构体数组的遍历画出来的图能占满三屏维护起来是灾难。ST语言的优势在于它支持结构体STRUCT、数组ARRAY、循环FOR/WHILE、条件分支CASE这些恰好是数据驱动方案需要的基础设施。你可以定义一个配置结构体数组用FOR循环遍历每个元素对应一个轴或一个气缸。这种写法在ST里可能就二十行在梯形图里可能要画两百个网络。当然ST也不是万能的。涉及安全逻辑比如安全门、急停链的部分很多公司规范还是要求用梯形图或者安全功能块来做因为可读性和审查便利性更好。我的做法是安全相关的用LD或认证安全块业务逻辑和数据处理用ST两者在同一个程序里混用各取所长。2.4 方案的整体架构整个方案的架构分三层配置层一个全局的配置结构体数组每个元素描述一个机型或一个子模块的参数。这部分数据可以来自HMI、来自文件、来自上位机下发也可以直接常量写死。功能层一个通用的FB接收配置数据作为输入内部用ST实现状态机、轴控、气缸控、报警处理等通用逻辑。适配层机型选择逻辑根据当前机型编号从配置表里取出对应的配置喂给功能层。这个架构的好处是新增一个机型只需要在配置表里加一行数据功能层的代码一行都不用动。改一个通用逻辑八个机型同时生效不存在漏改的问题。3. ST语言实现复用的关键技术点3.1 结构体设计配置表的骨架配置表的核心是结构体。以轴控为例一个轴需要哪些参数轴号、使能条件、回零方式、软限位正负、定位速度、加速度、到位判定窗口、超时时间。把这些打包成一个结构体TYPE ST_AxisConfig : STRUCT nAxisNo : INT; // 轴号 bEnable : BOOL; // 是否启用该轴 nHomingMode : INT; // 回零方式 0-直接 1-找原点 2-找Z相 fSoftLimitPos : REAL; // 正软限位 fSoftLimitNeg : REAL; // 负软限位 fSpeedFast : REAL; // 快速速度 fSpeedSlow : REAL; // 慢速速度 fAccel : REAL; // 加速度 fPosWindow : REAL; // 到位窗口 tTimeout : TIME; // 超时时间 END_STRUCT END_TYPE然后定义一个数组长度就是最大轴数VAR_GLOBAL aAxisCfg : ARRAY[1..8] OF ST_AxisConfig; END_VAR八种机型的差异就体现在这个数组的填充内容上。机型A可能只用3个轴那aAxisCfg[4..8].bEnable就都是FALSE机型B用5个轴前5个bEnable为TRUE。功能块里遍历这个数组遇到bEnable为FALSE的就跳过逻辑完全不用改。注意结构体里的成员命名建议加前缀n表示INTf表示REALb表示BOOLt表示TIME这样在ST代码里一眼就能看出变量类型减少类型不匹配的编译错误。这个习惯在大型项目里能省很多时间。3.2 功能块接口设计输入输出怎么定FB的接口设计直接决定了复用性好不好。一个常见的错误是把太多东西做成输入输出变量导致调用的时候要连几十根线。正确的做法是配置数据通过结构体数组传入状态数据通过结构体数组传出只有少量控制信号用单独的引脚。FUNCTION_BLOCK FB_MachineCtrl VAR_INPUT nMachineType : INT; // 机型编号 1-8 bStart : BOOL; // 启动 bStop : BOOL; // 停止 bReset : BOOL; // 复位 aAxisCfgIn : ARRAY[1..8] OF ST_AxisConfig; // 轴配置 aCylCfgIn : ARRAY[1..16] OF ST_CylConfig; // 气缸配置 END_VAR VAR_OUTPUT nState : INT; // 当前状态 bReady : BOOL; // 就绪 bError : BOOL; // 故障 aAxisStatus : ARRAY[1..8] OF ST_AxisStatus; // 轴状态 END_VAR VAR // 内部变量 i : INT; nActiveAxis : INT; fbAxis : ARRAY[1..8] OF FB_AxisCtrl; // 轴控FB实例 END_VAR这里有个细节轴控FB实例用了数组。这意味着每个轴都有一个独立的FB实例它们之间互不干扰。在ST里FB数组是允许的调用的时候用fbAxis[i](...)这种形式。这个特性是做复用方案的关键没有它就得为每个轴声明一个单独的实例变量八个轴就是八行声明虽然也能用但不够优雅。3.3 状态机设计用CASE而不是IF堆叠状态机是机型控制的核心。八种机型的状态流转逻辑大同小异差异只在某些状态的进入条件和超时时间上。用CASE语句写状态机结构清晰扩展方便CASE nState OF 0: // 空闲 IF bStart AND bReady THEN nState : 10; END_IF 10: // 回零 FOR i : 1 TO 8 DO IF aAxisCfgIn[i].bEnable THEN fbAxis[i](bEnable : TRUE, nMode : 1); IF NOT fbAxis[i].bDone THEN RETURN; END_IF END_IF END_FOR nState : 20; 20: // 定位 // ... 30: // 完成 // ... END_CASE这种写法的好处是新增一个状态只需要加一个CASE分支不会影响其他状态。而且每个状态内部的逻辑是独立的调试的时候可以单独测试某个状态。3.4 配置表的加载方式配置表的数据从哪来三种常见方式第一种是常量写死。在全局变量初始化的时候直接赋值适合机型固定、不常改的场景。优点是简单可靠缺点是改配置要重新下载程序。第二种是HMI输入。操作员在触摸屏上选择机型PLC根据机型编号从预设的配置表里取数据。这种方式适合机型切换频繁的场景但配置表本身还是写在程序里。第三种是文件或数据库加载。配置存在SD卡、配方文件或上位机数据库里PLC启动时读取。这种方式最灵活但实现复杂度也最高需要考虑文件格式、读取失败的处理、版本兼容等问题。我的建议是大多数项目用第二种就够了。在PLC里维护一个二维数组第一维是机型编号第二维是参数索引HMI只负责传机型编号。这样既灵活又可靠不需要处理文件读写的各种异常。4. 实操过程从零搭建一个可复用的机型控制FB4.1 第一步梳理机型差异矩阵动手写代码之前先拿一张纸或者Excel把所有机型的差异列出来。这一步不能省省了后面一定返工。矩阵的行是机型列是差异点差异项机型A机型B机型C机型D机型E机型F机型G机型H轴数量33554466气缸数量466846810安全门无有有有无有有有回零方式Z相Z相原点原点Z相Z相原点原点节拍要求快快中中快中慢慢这张表出来之后哪些参数需要进配置结构体就一目了然了。轴数量、气缸数量、安全门有无、回零方式、节拍参数全部变成配置项。4.2 第二步定义配置结构体和全局配置表根据差异矩阵定义结构体。这里要注意一个原则结构体成员只放因机型而异的参数不放所有机型都一样的常量。比如轴使能后的等待时间如果八个机型都是200ms那就不要放进结构体直接写在FB里作为常量。放进去反而增加了配置的复杂度。TYPE ST_MachineConfig : STRUCT nAxisCount : INT; // 轴数量 nCylCount : INT; // 气缸数量 bHasSafetyDoor : BOOL; // 是否有安全门 nHomingMode : INT; // 回零方式 tCycleTarget : TIME; // 目标节拍 fSpeedScale : REAL; // 速度缩放系数 END_STRUCT END_TYPE VAR_GLOBAL CONSTANT cMachineCfg : ARRAY[1..8] OF ST_MachineConfig : [ (nAxisCount:3, nCylCount:4, bHasSafetyDoor:FALSE, nHomingMode:2, tCycleTarget:T#8S, fSpeedScale:1.0), (nAxisCount:3, nCylCount:6, bHasSafetyDoor:TRUE, nHomingMode:2, tCycleTarget:T#8S, fSpeedScale:1.0), (nAxisCount:5, nCylCount:6, bHasSafetyDoor:TRUE, nHomingMode:1, tCycleTarget:T#12S, fSpeedScale:0.8), (nAxisCount:5, nCylCount:8, bHasSafetyDoor:TRUE, nHomingMode:1, tCycleTarget:T#12S, fSpeedScale:0.8), (nAxisCount:4, nCylCount:4, bHasSafetyDoor:FALSE, nHomingMode:2, tCycleTarget:T#9S, fSpeedScale:1.0), (nAxisCount:4, nCylCount:6, bHasSafetyDoor:TRUE, nHomingMode:2, tCycleTarget:T#10S, fSpeedScale:0.9), (nAxisCount:6, nCylCount:8, bHasSafetyDoor:TRUE, nHomingMode:1, tCycleTarget:T#15S, fSpeedScale:0.7), (nAxisCount:6, nCylCount:10, bHasSafetyDoor:TRUE, nHomingMode:1, tCycleTarget:T#15S, fSpeedScale:0.7) ]; END_VAR这段代码是配置表的核心。八个机型每个一行所有差异参数都在里面。新增机型只需要加一行改参数只需要改对应的数值。提示VAR_GLOBAL CONSTANT表示全局常量编译后存在ROM里掉电不丢失。如果你的配置需要在线修改就不能用CONSTANT要用普通的VAR_GLOBAL并且考虑掉电保持的问题。4.3 第三步编写通用FB的主体逻辑FB的主体逻辑分几个模块初始化、状态机、轴控、气缸控、报警处理。每个模块都用ST写通过配置参数控制行为。初始化模块负责根据配置设置内部变量// 初始化 IF bInit THEN nActiveAxis : cMachineCfg[nMachineType].nAxisCount; nActiveCyl : cMachineCfg[nMachineType].nCylCount; bSafetyDoorEn : cMachineCfg[nMachineType].bHasSafetyDoor; fSpeedFactor : cMachineCfg[nMachineType].fSpeedScale; bInit : FALSE; END_IF轴控模块遍历轴数组只处理使能的轴FOR i : 1 TO nActiveAxis DO fbAxis[i]( bEnable : bAxisEnable[i], nHomingMode: cMachineCfg[nMachineType].nHomingMode, fSpeed : fBaseSpeed * fSpeedFactor, fAccel : fBaseAccel * fSpeedFactor ); aAxisStatus[i] : fbAxis[i].stStatus; END_FOR注意这里fSpeedFactor的作用。机型G和H的节拍要求慢速度缩放系数0.7意味着所有轴的速度都降到70%。这个逻辑只写了一遍八个机型通用。气缸控模块类似但气缸的逻辑通常比轴简单主要是伸出、缩回、到位检测、超时报警FOR i : 1 TO nActiveCyl DO fbCyl[i]( bExtend : bCylCmd[i], tTimeout : T#3S, bSensorExt : bCylSensorExt[i], bSensorRet : bCylSensorRet[i] ); END_FOR4.4 第四步机型切换的处理机型切换有两种场景一种是断电切换设备关机操作员换机型重新上电另一种是在线切换设备运行中操作员在HMI上改机型。断电切换简单上电初始化的时候读一次机型编号就行。在线切换复杂必须确保设备处于安全状态才能切换否则轴还在动的时候改了轴数量程序直接乱套。我的做法是在线切换必须满足三个条件——设备在空闲状态、所有轴已停止、所有气缸已复位。满足条件后先执行一个卸载流程把当前机型的资源释放掉再执行加载流程按新机型初始化。这个逻辑用一个独立的状态机来管不要混在主状态机里。CASE nSwitchState OF 0: // 空闲等待切换命令 IF bSwitchCmd AND (nState 0) AND bAllAxisStopped AND bAllCylHome THEN nSwitchState : 10; END_IF 10: // 卸载当前机型 bInit : TRUE; // 触发重新初始化 nSwitchState : 20; 20: // 加载新机型 nMachineType : nNewMachineType; nSwitchState : 0; END_CASE4.5 第五步测试与验证单FB方案最大的风险是改一处影响八处。所以测试必须覆盖所有机型。我的测试流程是先做单元测试每个机型单独跑一遍完整流程确认基本功能正常。然后做回归测试改一个通用逻辑八个机型全部重跑。最后做边界测试比如机型A只有3个轴但配置表里第4个轴的bEnable如果被误设为TRUE会怎样程序应该能容错而不是直接崩掉。测试的时候建议做一个机型模拟器在HMI上做一个页面可以手动设置机型编号、手动触发各个状态、手动模拟传感器信号。这样不用实际接设备就能测大部分逻辑。5. 常见问题与排查技巧实录5.1 配置表数据错位这是最常见的问题。配置表是一个二维数组如果某一行的参数顺序写错了或者少写了一个参数编译可能不报错但运行起来行为完全不对。排查方法写一个配置校验程序在初始化的时候检查配置的合理性。比如轴数量不能超过数组上限、速度系数必须在0.1到2.0之间、回零方式只能是0/1/2。校验不通过就报警不要让它带着错误配置跑。// 配置校验 IF (cMachineCfg[nMachineType].nAxisCount 1) OR (cMachineCfg[nMachineType].nAxisCount 8) THEN bCfgError : TRUE; sCfgErrMsg : 轴数量配置错误; END_IF5.2 FB实例数组的调用陷阱ST里FB数组的调用有个坑不能跳过某个实例不调用。如果你声明了fbAxis : ARRAY[1..8] OF FB_AxisCtrl那么每个扫描周期八个实例都会被调用即使某个轴的bEnable是FALSE。这本身不是问题但如果你在FB内部写了bEnable为FALSE时复位所有输出的逻辑那没问题如果你没写FB可能会保持上一次的状态导致轴使能信号残留。解决方法在FB内部第一行就处理bEnableIF NOT bEnable THEN // 复位所有输出 bDone : FALSE; bBusy : FALSE; bError : FALSE; RETURN; END_IF5.3 机型切换时的状态残留在线切换机型的时候如果旧机型的状态机还停在某个中间状态新机型加载后可能会从这个中间状态继续跑导致逻辑混乱。解决方法切换时强制把状态机复位到空闲状态所有内部变量清零。这个复位逻辑要写在加载流程里不能依赖状态机自己走回空闲。5.4 常见问题速查表问题现象可能原因排查方法解决方案某机型轴不动配置表中该轴bEnable为FALSE在线监控配置数组修改配置表对应项切换机型后报警旧状态未复位查看nState值切换时强制复位状态机速度不对速度系数配置错误检查fSpeedScale修正配置表编译报错类型不匹配结构体成员类型用错查看编译错误行检查变量前缀和类型运行中突然停止配置校验未通过查看bCfgError修正配置数据气缸动作顺序错气缸配置顺序与实物不符对照IO表调整配置表顺序5.5 几个实操心得第一个心得配置表的注释要写清楚。每个参数的含义、取值范围、单位全部写在注释里。我见过太多项目配置表里一个nParam1、nParam2过了半年连原作者都不知道是什么意思。第二个心得保留一份配置表的Excel版本。PLC里的配置表是给程序用的Excel是给人看的。每次改配置先改Excel再同步到PLC代码这样不容易出错。第三个心得机型编号从1开始不要从0开始。虽然ST数组默认从0开始但机型编号从1开始更符合人的直觉而且0可以保留作为未选择机型的默认值。第四个心得新机型上线前先用模拟器跑一遍完整流程。不要直接上实机实机调试的成本太高一个逻辑错误可能导致撞机。6. 这个方案的扩展方向单FB方案跑通之后还有几个可以继续优化的方向。第一个方向是配置表的外部化。把配置从PLC代码里挪到配方文件或数据库中通过HMI或上位机下发。这样新增机型不需要改PLC程序只需要更新配置文件。适合机型多、变化快的场景。第二个方向是自动机型识别。通过读取设备上的硬件配置比如拨码开关、RFID、或者IO模块的型号自动判断当前是什么机型不需要操作员手动选择。这样能避免选错机型导致的问题。第三个方向是参数自整定。某些参数比如速度、加速度可以根据实际运行数据自动优化不需要人工调。这个方向实现难度较高但对节拍要求严格的产线很有价值。第四个方向是与MES系统对接。机型信息、生产参数、运行状态全部上传到MES实现远程监控和数据分析。这个方向已经超出了PLC编程的范畴涉及到上位机开发和网络通信但和单FB方案是天然兼容的——因为配置数据已经结构化了上传和下发都很方便。我在实际项目里用这套方案做过一个八机型兼容的装配线从第一版跑通到最终交付功能层的代码只改了三次每次都是增加新功能而不是修bug。配置表从最初的八行扩展到后来的十二行客户中途加了四个机型功能层一行没动。这种改数据不改代码的体验是复制粘贴方案永远给不了的。