ARTICLE DETAIL

资讯详情

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

一个ST功能块统一控制8种品牌变频器伺服,换硬件不改逻辑

一个ST功能块统一控制8种品牌变频器伺服,换硬件不改逻辑 做设备集成这几年最烦的不是写程序而是同一个功能换个品牌就要重写一遍。今天这篇不聊大道理直接拿我最近做的一条产线来复盘8种不同品牌的变频器和伺服我只写了一个FB功能块用ST结构化文本语言统一封装整套程序里反复调用这一个功能块硬件随便换逻辑代码一行不用改。这篇文章就是把当时的思路、代码骨架和踩过的坑全部摊开如果你也是做自动化项目、天天跟多品牌设备打交道的照着抄能省下好几天的调试时间。先说清楚这个东西到底解决了什么问题一般在产线上同一个工位可能会因为供应商变更、缺货、客户指定等原因用不同品牌的驱动器。每换一个品牌通信地址不一样、控制字不一样、停机模式不一样很多人就乖乖建一个FB结果程序里塞了十几个几乎相同的块。我做的这个FB把“机型差异”封装成内部映射表和状态机对外只暴露统一的控制接口。一套逻辑8种机型随便切ST语言写出来以后不管在CODESYS、TwinCAT还是汇川、信捷这些支持IEC 61131-3的环境里都能直接移植。适合谁看搞PLC编程的、做产线集成的、调试伺服和变频器的新手老手都能看。新司机可以先理解复用的设计套路老司机可以跳过基础部分直接看后面第3章的代码骨架和第4章的问题排查那里面的坑都是真金白银换来的。1. 先搞清楚一件事这个FB到底解决什么问题1.1 现场最常见的痛苦很多人对“复用”的理解就是“拷贝粘贴”。项目做到一半新接一台三菱伺服赶紧复制上一个位控FB改几个地址接着用。这种方式不是复用是暴力拆东墙补西墙前期看着快后期维护起来非常酸爽。真正的复用是把“逻辑”和“差异”分开。先还原一下现场。一条产线十几个轴第一批用的是台达变频器第二批因为交期上了三菱第三批客户又指定了汇川。每换一批不同品牌的驱动器原来的程序就要动不少地方通信报文不一样频率表映射比例不一样状态字里哪个位代表“运行中”也各不相同。如果每个牌子都建一个FB代码里全是Ctrl_AB、Ctrl_CD、Ctrl_EF改一个逻辑就得同步改好几份不出错才怪。我当时的思路很简单既然每一台驱动器本质上都能抽象成“启、停、调速、读状态、读实际值”这几个操作那就不管你里面是什么品牌我对外只给你发这几种指令。不同品牌之间的差异全在这个FB内部消化掉。有人可能会问那通信层不一样怎么办确实有的走Modbus RTU有的走CANopen有的走EtherCAT这不是一个FB能解决的。我这里说的统一是统一在同一个通信总线下比如全部走Modbus RTU或者全部走EtherCAT/CANopenFB封装的是“协议之上的应用层差异”不是协议本身的差异。这一点先讲清楚不然照搬代码会踩大坑。1.2 复用的三层含义这里的“复用”我之前捋了捋至少有三层别小看每一层都有完全不同的价值。第一层是代码复用一个FB写好了程序里需要控制几台设备就实例化几个FB实例每个实例有自己独立的输入输出和内部参数互不干扰。这是IEC 61131-3里最基础的功能块用法也是复用概念的起点。第二层是机型复用这是这篇文章的核心。一个FB内部通过一个机型枚举参数自动载入对应机型的地址映射、比例系数、状态字解析规则。程序里不管接到的是哪个牌子的驱动器调用同一个FB选择对应机型一切自动对齐。第三层是工程复用在这个项目里调好的FB保存到自己的库文件里。下个项目如果用的还是这几个品牌直接拖进来用就算是新品牌只需要在FB内部追加一组映射参数因为主体逻辑不变基本上就是一个礼拜以内的事。这三层我实际做完以后最大的感受是程序的设计时间被大大压缩了因为核心框架是固定的写新机型的适配就是填一张“参数表”而已。当然这种设计也不是没有代价需要你在写FB的时候多做一层抽象多花一点时间设计数据结构和映射表。但相比后面每次换设备都要重写的痛苦前面这一两天的设计投入非常值得。2. FB复用设计的关键接口、状态机与参数映射2.1 接口设计决定复用上限一个FB能不能复用得起来第一步就看接口设计。接口焊死了后面想扩机型都费劲接口留得好FB就像万能插座什么设备插上都能跑。我定义这个FB的时候对外输入输出是这样的参数方向参数名数据类型说明输入eDevTypeDEV_TYPE枚举机型选择对应8个品牌型号输入bEnableBOOL使能信号只有为TRUE时FB才工作输入rSetSpeedREAL目标转速单位rpm统一用工程单位输入bResetFaultBOOL故障复位请求上升沿有效输出bRunningBOOL运行状态反馈所有品牌统一为TRUE运行中输出bFaultBOOL故障状态反馈所有品牌统一为TRUE有故障输出uDriveStatusWORD驱动器详细的原始状态字供上位机显示输出rActSpeedREAL当前实际转速反馈单位rpm为什么要把输入输出都定义成工程单位rpm而不是原始寄存器值这是个关键选择。如果对外接口暴露寄存器原始值那调用方还得知道这个品牌速度寄存器是0~4000对应0~50Hz那个品牌是0~16384对应0~60Hz这就不叫统一接口了。只有对外全部标准化成物理量不同品牌的数据差异才能被“关”在FB里面。还有个很重要的细节输入输出不要贪多。我见过有些人写FB恨不得把所有参数都拉到接口上速度上限、电流限幅、加减速时间全部做成输入输出引脚。这样做接口非常庞大调用起来满天星看着灵活实际上复用的灵活性反而降低了因为你每次调用都要重新理一遍几十个引脚。内部能封装的配置尽量封装成内部参数对外只留真正的控制量和最必要的配置项这个FB才好用。2.2 内部状态机谁都要遵守启停逻辑驱动器不管什么牌子运行逻辑本质上都是同一个套路上电待机、启动、运行、停止、故障报警。这就是一台设备共有的“状态机”。我在FB内部定义了一个简单但核心的状态机STANDBY待机状态驱动器已上电但未运行STARTING启动过程等待驱动器反馈“运行中”信号RUNNING正常运行状态STOPPING停止过程等待速度降到0FAULT故障状态为什么要搞状态机因为很多驱动器的启停不是瞬间完成的。变频器从零加速到设定频率需要几百毫秒到几秒伺服从停止到稳速也需要一段时间。如果你的程序只看“发送了启动指令”就认为设备在运行那么启动瞬间做联锁保护就会出漏洞。状态机的存在就是让FB内部知道设备目前到底处于什么阶段然后据此决定做什么动作、允许做什么动作。比如说在STANDBY状态下如果检测到bEnable为TRUE就发启动命令同时进入STARTING状态。在STARTING状态下如果规定时间内没有收到驱动器的运行反馈置位那就要报超时故障。这种逻辑如果不用状态机写用一堆散乱的定时器和相互嵌套的IF写到第5台设备的时候基本就混乱了。还有一点容易被忽略故障复位后的状态转移。生产现场经常有这种操作——设备报警了操作工按复位按钮如果复位逻辑写在共用FB之外不同机型的复位时序不一样就很容易出问题。所以复位动作我也统一放进状态机收到复位请求后只有当前是FAULT状态才允许执行复位命令复位以后自动回到STANDBY等待下一次启动。2.3 参数映射表8种机型的差异都压在这里这个FB能适配多机型的关键在于内部有一个“机型参数映射表”。简单说就是把每个机型跟默认参数不同或特殊的地方全部抽出来放在一段集中配置的代码里。我定义了一个结构体类型的数组数组元素就是各机型的参数集TYPE DRIVE_PARAMS : STRUCT uCtrlAddr : WORD; // 控制字寄存器地址 uStatusAddr : WORD; // 状态字寄存器地址 uFreqAddr : WORD; // 频率寄存器地址 uFreqActAddr : WORD; // 实际频率反馈寄存器地址 rFreqToRpmFactor : REAL; // 频率到转速的换算系数 wRunBit : WORD; // 状态字中“运行中”对应的位掩码 wFaultBit : WORD; // 状态字中“故障”对应的位掩码 wReadyBit : WORD; // 状态字中“就绪”对应的位掩码 wCtrlRunMask : WORD; // 控制字中“启动”置位的掩码 wCtrlReadyMask : WORD; // 控制字中“使能就绪”的掩码 END_STRUCT END_TYPE这里的参数并不复杂但有一条原则非常重要不同的品牌控制字的“启动”位不同状态字的“运行中”位也不同比如A品牌是位0B品牌是位1C品牌是位2。这跟数码管里的查理复用有异曲同工之妙——同一个引脚通过不同的组合策略可以驱动多个LED本质上就是“用同一份代码和不同映射策略去操作不同的硬件”。搞工控的人如果理解查理复用的思想再看这个FB会觉得豁然开朗复用并不是让所有设备用同一个地址而是让代码在“同一个骨架”里自动适配不同的映射关系。机型选择这里我用的是枚举类型TYPE DEV_TYPE : ( DEV_TAIDA_VFD_M, // 台达VFD-M系列 DEV_MITSUBISHI_D700, // 三菱FR-D700系列 DEV_SIEMENS_MM420, // 西门子MM420 DEV_INOVANCE_MD500, // 汇川MD500系列 DEV_YASKAWA_A1000, // 安川A1000系列 DEV_PANASONIC_MINAS, // 松下MINAS系列 DEV_SCHNEIDER_ATV31, // 施耐德ATV31 DEV_DANFOSS_FC302 // 丹佛斯FC302系列 ); END_TYPE有了枚举之后调用方写程序都变得非常直观fb_DriveCtrl_1.eDevType : DEV_TAIDA_VFD_M;这行代码一眼就能看出当前控制的是什么类型的设备。而且ST语言里枚举比INT安全得多因为编译器能检查范围不会出现传了个INT值9进去的野操作。映射表初始化的ST代码骨架大概是这样的// 在FB的初始化段中根据eDevType选择参数 CASE eDevType OF DEV_TAIDA_VFD_M: stParams.uCtrlAddr : 16#2000; stParams.uStatusAddr : 16#2100; stParams.uFreqAddr : 16#2001; stParams.wRunBit : 16#0002; // bit1 运行状态 stParams.wFaultBit : 16#0004; // bit2 故障状态 stParams.wCtrlRunMask : 16#0001; // bit0 启动 stParams.rFreqToRpmFactor : 50.0 * 60.0 / 1500.0; // 50Hz电机同步转速1500rpm DEV_MITSUBISHI_D700: stParams.uCtrlAddr : 16#0000; stParams.uStatusAddr : 16#0001; stParams.uFreqAddr : 16#0002; stParams.wRunBit : 16#0001; // bit0 运行中 stParams.wFaultBit : 16#0008; // bit3 故障 stParams.wCtrlRunMask : 16#0002; // bit1 正转启动 stParams.rFreqToRpmFactor : 50.0 * 60.0 / 1500.0; // 其余机型类似... END_CASE这样的好处是以后如果来了第9种机型只需要在枚举里加一个值然后在这个CASE里加一个分支填一张参数表主体逻辑完全不动。这就是前文说的“扩展只填参数表”的含义。3. ST语言实现从框架到代码一步步写出来3.1 变量定义的完整骨架先把FB的整体变量定义贴出来后面所有代码都基于这组变量。FUNCTION_BLOCK FB_DriveCtrl VAR_INPUT eDevType : DEV_TYPE; // 机型选择 bEnable : BOOL; // 使能控制 rSetSpeed : REAL; // 目标转速rpm bResetFault : BOOL; // 故障复位请求 END_VAR VAR_OUTPUT bRunning : BOOL; // 统一运行状态 bFault : BOOL; // 统一故障状态 wDriveStatus : WORD; // 原始状态字 rActSpeed : REAL; // 实际转速反馈rpm eFaultCode : INT; // FB内部故障码0无故障 END_VAR VAR stParams : DRIVE_PARAMS; // 机型参数映射表 eState : INT; // 内部状态机 bCmdRun : BOOL; // 发送给驱动器的启动命令 bCmdActive : BOOL; // 驱动器已激活 rCmdFreq : REAL; // 发送给驱动器的频率设定值Hz tonStartTimeout : TON; // 启动超时定时器 tonStopTimeout : TON; // 停止超时定时器 wCtrlWord : WORD; // 组成后的控制字 rActFreq : REAL; // 从驱动器读到的实际频率反馈Hz bInitDone : BOOL; // 首次扫描初始化完成标志 END_VAR注意这里用了TON定时器作为FB的局部变量。在IEC 61131-3里FB内部的定时器实例每个FB实例都会有一份自己的副本所以8台设备调用同一个FB8个定时器是各自独立的不会互相干扰。这正是FB实例化的价值所在。3.2 机型选择与初始化逻辑FB的初始化不能放在整个FB代码的最前面每次都执行因为CASE映射表只需要加载一次。这里我用了一个bInitDone标志位。IF NOT bInitDone THEN // 加载机型映射参数 CASE eDevType OF DEV_TAIDA_VFD_M: stParams.uCtrlAddr : 16#2000; stParams.uStatusAddr : 16#2100; stParams.uFreqAddr : 16#2001; stParams.uFreqActAddr : 16#2101; stParams.wRunBit : 16#0002; stParams.wFaultBit : 16#0004; stParams.wReadyBit : 16#0001; stParams.wCtrlRunMask : 16#0001; stParams.wCtrlReadyMask : 16#0000; stParams.rFreqToRpmFactor : (50.0 * 60.0) / 1500.0; DEV_MITSUBISHI_D700: stParams.uCtrlAddr : 16#0000; stParams.uStatusAddr : 16#0001; stParams.uFreqAddr : 16#0002; stParams.uFreqActAddr : 16#0003; stParams.wRunBit : 16#0001; stParams.wFaultBit : 16#0008; stParams.wReadyBit : 16#0010; stParams.wCtrlRunMask : 16#0002; stParams.wCtrlReadyMask : 16#0001; stParams.rFreqToRpmFactor : (50.0 * 60.0) / 1500.0; // DEV_SIEMENS_MM420, DEV_INOVANCE_MD500 等 // 每个机型一个分支填对应的寄存器地址和位掩码 END_CASE // 默认进入待机状态 eState : 10; // STANDBY bInitDone : TRUE; END_IF这里有个细节我在代码里用状态值10、20、30、40、50而不是直接用枚举是为了在调试界面里看变量时能直观看到当前状态。10STANDBY20STARTING30RUNNING40STOPPING50FAULT。如果你用的是CODESYS或者TwinCAT也可以用枚举类型做状态值调试器里的可读性更好。我个人习惯用数字并做一张注释表写清楚大家按自己喜好来就行。3.3 控制字生成逻辑控制字的生成是整个FB里最容易乱的部分。不同品牌的控制字定义差异很大但归到底无非就是几个动作的组合运行、停止、复位、紧急停车。我用一个统一的标准动作模型再通过映射掩码翻译成各机型的控制字。// 根据当前状态和使能信号决定底层控制命令 IF bEnable AND eState 50 THEN bCmdRun : TRUE; // 使能期间持续发送运行命令 ELSE bCmdRun : FALSE; END_IF // 组装控制字不同机型通过掩码映射 wCtrlWord : 0; IF (stParams.wCtrlReadyMask 0) THEN wCtrlWord : wCtrlWord OR stParams.wCtrlReadyMask; END_IF IF bCmdRun THEN wCtrlWord : wCtrlWord OR stParams.wCtrlRunMask; END_IF IF bResetFault THEN // 复位命令通常需要同时置位专门的复位位 // 不同机型的复位位也可能不同这里通过单独一个映射位实现 wCtrlWord : wCtrlWord OR stParams.wCtrlResetMask; END_IF // 将wCtrlWord写入驱动器的uCtrlAddr地址 // 这一步不是FB内部能完成的需要外部通信接口把wCtrlWord搬运到实际通信报文里 // 我通常把wCtrlWord做成公共内存变量或者在FB内部触发通信发送标志这里要强调一个我在项目中踩过的大坑不要在主程序里对同一个控制字布尔位做“或逻辑”。比如有人习惯在OB1里写“如果某个条件满足就把控制字第0位置1如果另一个条件满足再把控制字第1位置1”这样两条独立的逻辑线极容易产生双线圈和多处写同一地址的混乱。正确做法是像上面这段ST代码一样控制字的每一位都在FB内部集中计算一次赋值外部只负责把这个word发出去。另外很多品牌的驱动器控制字里还有一个“使能”和“运行”的区别。西门子MM420的操作模式就有点特殊它的启停有时候通过端子控制有时候通过通信控制需要通过参数P0700/P1000来设定。我的FB里默认所有机型都设置为纯通信控制模式这个是在驱动器侧初始化时设好的FB内部不处理。如果你在现场碰到“发命令没反应但地址和点位都对”的问题先去查驱动器的控制源参数是不是通信模式这个比调程序重要得多。3.4 状态字解析与统一输出状态字的解析方案也很关键。各品牌驱动器的状态字内容五花八门我需要从中提取出两个对外统一的信息运行中、故障。外加保留一个原始状态字给上位机显示方便调试。// 从通信接口读取原始状态字到wDriveStatus // 这个读取过程一般由外部通信任务完成FB内只需解析 // 提取统一状态位 bRunning : (wDriveStatus AND stParams.wRunBit) 0; bFault : (wDriveStatus AND stParams.wFaultBit) 0; // 实际转速换算 // 从驱动器读到的实际频率值rActFreq单位是0.01Hz或0.01rpm因机型而异 // 这里通过映射参数折算成标准的rpm输出 rActSpeed : rActFreq * stParams.rFreqToRpmFactor;单位换算这里必须多写几句这是最容易出“灵异事件”的点。有的驱动器频率寄存器分辨率是0.01Hz有的是0.1Hz有的是0.001Hz有的转速寄存器直接就是0.01rpm有的则是编码器反馈的脉冲计数需要你再除以编码器分辨率才能得到转速。我建议在映射表里直接把换算系数都算好不要到FB外面再二次换算。道理跟前面说的一样差异要关在FB内部对外统一物理量。如果你在调用FB的地方再做一次“不同机型不同系数”的补丁那就违背了封装的初衷等于把差异又漏出去了。状态解析还有一个细节值得注意运行状态和故障状态不要直接读一个位就完事。有些驱动器的运行信号在减速过程中会保持停车完毕后才掉有些驱动器在故障瞬间运行位会同时失效。如果你在判断里依赖“运行位和故障位同时为真”或者“非运行位即故障位”在现场会出很多诡异问题。我的做法是bFault直接看故障位bRunning直接看运行位两者互不推导。至于“状态机如何从STARTING超时跳转到FAULT”那是我FB内部基于定时器和状态判断算出来的也不依赖外部位信号。3.5 状态机的完整实现状态机是FB的主干所有命令和输出都围绕状态转。CASE eState OF 10: // STANDBY // 使能有效且有启动请求开始启动过程 IF bEnable THEN eState : 20; tonStartTimeout(IN : TRUE, PT : T#5S); END_IF 20: // STARTING // 等待驱动器反馈运行状态 IF bRunning THEN eState : 30; tonStartTimeout(IN : FALSE); ELSIF tonStartTimeout.Q THEN // 启动超时转入故障 eFaultCode : 1; // 启动超时 eState : 50; END_IF 30: // RUNNING // 使能断开则开始停止 IF NOT bEnable THEN eState : 40; tonStopTimeout(IN : TRUE, PT : T#5S); END_IF 40: // STOPPING // 等待驱动器停止运行 IF NOT bRunning THEN eState : 10; tonStopTimeout(IN : FALSE); ELSIF tonStopTimeout.Q THEN eState : 10; // 就算没停到位也不阻塞报警留给上位机处理 END_IF 50: // FAULT // 故障状态下等待复位 IF bResetFault AND NOT bFault THEN eFaultCode : 0; eState : 10; END_IF END_CASE状态机的代码看起来很简单但是有几个细节要说明第一STANDBY到STARTING的转换只需要bEnable为真不需要等驱动器反馈“就绪”。这样做是刻意的。有些驱动器的就绪位在使能命令发出后才置位如果你在启动前死等就绪位会出现“越等越启动不了”的死锁。更稳的做法是发命令然后用超时检测兜底。第二STARTING状态下要持续发送运行命令不是发一个脉冲就完了。有些变频器的通信控制方式要求运行信号一直保持信号消失就等于停机。这一点在写通信控制的时候尤其重要跟按钮自锁电路是一个道理。第三STOPPING超时后不要强行报故障。变频器滑行停车有时候停得很慢尤其是带大惯量负载的时候有可能超过你设定的5秒。如果一超时就报故障会导致很多无谓的停机。我这里的处理是超时后强制回到STANDBY但把“停车超时”作为一个标志存储到eFaultCode的bit里给上位机提示而不是直接触发故障停机。第四FAULT状态下bCmdRun必须为FALSE。这个不是想起来才写的而是要在整个CASE前面统一清零不能依赖状态分支里忘记清零。我在实际工程里会在状态机代码上方先加一句bCmdRun : FALSE; wCtrlWord : 0;然后再进CASE。这样确保每次扫描控制命令都从零开始计算不会因为上一次扫描的历史残留导致动作混乱。这也是一个非常重要的ST编程习惯组合输出先清零再按需置位避免锁存残留。3.6 在PLC主程序中的调用效果写好的FB怎么用直接实例化8个每个轴的调用部分极其简洁// 轴1 - 台达变频器 fb_Drive_A1(eDevType : DEV_TAIDA_VFD_M, bEnable : bAxis1_RunCmd, rSetSpeed : rAxis1_Speed, bResetFault : bReset_A1, bRunning bAxis1_IsRunning, bFault bAxis1_IsFault, wDriveStatus wAxis1_Status, rActSpeed rAxis1_ActSpeed, eFaultCode iAxis1_FaultCode); // 轴2 - 三菱变频器 fb_Drive_A2(eDevType : DEV_MITSUBISHI_D700, bEnable : bAxis2_RunCmd, rSetSpeed : rAxis2_Speed, bResetFault : bReset_A2, bRunning bAxis2_IsRunning, bFault bAxis2_IsFault, wDriveStatus wAxis2_Status, rActSpeed rAxis2_ActSpeed, eFaultCode iAxis2_FaultCode);如果以后某台设备的品牌变了只需要改eDevType这一行连调用代码都不用改。我在项目调试中途把其中一台三菱换成了施耐德现场改完那一行参数重新下载设备直接跑起来通信数据全部正常。那一刻才真正体会到封装复用的甜头。有人可能会问为什么不让eDevType直接做成一个可修改的FUNCTION参数其实也可以但是用枚举的好处是即使在运行过程中切换eDevTypeFB内部要能重新初始化。我加了一个判断如果eDevType和上次初始化的机型不一致就重新加载参数并复位内部状态机。IF eDevType eInitDevType THEN bInitDone : FALSE; END_IF这个细节很实用。它允许你在HMI上写一个下拉框直接切换设备品牌做测试不用重新下载程序。非常适合做设备出厂前的兼容性测试。4. 实操排错这8种机型跑下来踩过的坑4.1 问题速查表跑了8种机型前后折腾了不少时间整理一份速查表给一直在和驱动器通信代码搏斗的同学参考问题现象大概率原因排查思路能运行但转速不对频率映射系数错误检查rFreqToRpmFactor确认频率分辨率、极对数、减速比是否覆盖状态一直显示就绪但没有运行控制字中启动位掩码错误对照该品牌手册确认启动位对应bit序号偶尔启动超时报警通信扫描周期和启动时序不匹配加大超时时间或把FB放到固定任务里执行实际反馈速度波动单位换算没有统一上位机二次换算外部不要做任何换算统一由FB输出物理量故障复位后半天才恢复复位时序不对复位命令保持时间不够检查该品牌对复位脉冲宽度或电平的要求对调品牌后程序启动就报故障初始化标志没失效检查eInitDevType比较逻辑确保换了机型后重新初始化运行中频繁掉状态通信总线上有其他设备干扰单独测点名率看是不是总线负载过高或通信超时4.2 实例化冲突与双线圈多实例化的时候最容易出的问题就是“双线圈”和“多处写同一个通信地址”。比如8个FB实例都要往同一个Modbus保持寄存器区域写数据如果你在程序中开了几个任务分别往同一个地址写那么最后执行的那个任务会覆盖前面的值。解决这个问题要看你的PLC平台怎么组织通信。一般来说驱动器通信都放在同一个周期任务里通信报文的发送缓冲区是独立的。每个FB只负责计算自己那台设备该发的报文字段然后把结果放到对应的发送缓冲区。这一点靠的是整个程序框架的梳理FB负责“算”通信任务负责“发”两者分离就不会互相覆盖。我在最初版本里为了省事让FB内部直接调用Modbus写入指令结果8个FB同时调用全局通信资源程序扫描周期直接拉长还出现了通信偶发超时的现象。后来把通信发送全部改到外部独立任务FB只输出要发的内容问题立刻消失。这是我踩过最贵的一个坑强烈建议大家做FB封装时底层通信不要写在FB内部。4.3 通信超时和扫描周期这个FB里我用了TON定时器做启动和停止超时。这里有个新手的常见误区定时器的时间基准是FB被调用所在的任务的周期。如果你把FB放在100ms的任务中调用那么PT:T#5S实际上的误差不是5秒而是5秒上下浮动一个任务周期。更麻烦的是如果某个扫描周期里PLC发生通信等待阻塞你说的5秒超时可能实际上走了好几秒钟。我建议把FB放在一个固定周期任务里调用比如10ms或20ms的任务不要放在自由扫描的OB1里。不仅定时更准通信报文和转速反馈的刷新也更平滑。如果PLC的CPU性能不行死等通信响应那定时器也会跟着卡住这种情况我一般都把通信超时检测单独挑出来做。总之FB放哪个任务、任务周期多少决定了内部定时器的可靠程度。还要注意不能指望把通信扫描周期缩到1ms通讯不是干得越频繁越好当年我为了追求响应速度把Modbus RTU轮询周期调到5ms结果通信链路在总线负载场合直接乱套后来老老实实按驱动器手册推荐的10ms~20ms来跑。做工程要稳不要为了“看起来快”而牺牲可靠性。4.4 数据类型与单位换算的隐性错误我在这个FB里所有对外接口都用REAL实型但ST语言里REAL在部分PLC上精度有限坐标运算没大问题。不过如果某台驱动器的频率寄存器需要的是0.01Hz精度那你REAL转成INT写入的时候就不能四舍五入而是要先乘以100再转INT否则会有累积误差。比如设定50.00Hz写入寄存器应该写5000如果你直接用REAL_TO_INT(50.0)结果是50而不是5000。这种低级错误其实很常见问题表现为“所有频率都小了100倍”。我排版时在代码里都会写上注释// 频率寄存器数值 物理频率(Hz) * 100防止过几个月回来看代码的时候怀疑人生。这个注释钱不能省。单位换算涉及减速比的时候还要更仔细。如果是伺服电机经减速机带负载需要在FB里再加一个减速比参数。我一般会在DRIVE_PARAMS结构体里加一个rGearRatio字段默认1.0有减速机的机型单独设置这样rActSpeed输出的是负载端转速而不是电机轴转速。上位机显示和动作逻辑都不用再操心减速比的问题。4.5 关于“复用到极致”的一些心得做完这个FB以后我又在其他项目里复用了同样的思路最大的体会就是真正的复用在设计阶段不在编码阶段。这一块多说几句。很多人觉得ST语言的优点就是“像C一样方便”可以把逻辑写得飞起。但正因为ST语言太灵活如果开头没有设计约束很容易整个程序变成一团乱麻。这个FB之所以能平稳地跑8种机型是因为我在动手写代码前先定义了数据结构和接口映射表、状态机、统一物理量这些都是预先想清楚的。代码写出来反而是水到渠成的事。这也引出一个实际建议接手一个多机型兼容项目第一步不要在键盘上敲字先拿出一张纸把那些“看起来不同、本质相同”的动作列出来。启、停、复位、调速、读状态这几件事就是FB的接口。你再把每个机型对应的寄存器地址、位掩码、换算系数填进去做成一张表代码照着表写后续扩展就是填一行新表的事。最后再分享一个小技巧。我在这个FB里留了一个专门的调试输出变量wDriveStatus这是驱动器的原始状态字。现场调试时我会把HMI上隐掉其他数据单独留一个调试页把这个原始状态字显示出来。因为这类问题90%出在“状态位映射错误”上你把原始的十六进制值摆在那对照手册一眼就能看出来哪一位不对比在程序里猜半天高效得多。这个小习惯帮我解决过很多次现场疑难杂症。如果你现在手头也有类似的“一个功能、多品牌设备”的痛点建议不要急着给每个品牌写一个FB花一天时间梳理接口和映射表写一个通用FB剩下的就是填表的体力活。这个投入回报率谁做谁知道。
返回列表